2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-08 15:42:25 +03:00

feat: make ssr work

feat: update build script

chore: use jiti instead of babel-node for examples
This commit is contained in:
pimlie
2021-01-25 00:50:46 +01:00
parent 2e2c08e1f8
commit 9cfde5b550
37 changed files with 825 additions and 1606 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
"@nuxtjs/eslint-config-typescript"
],
"globals": {
"__DEV__": true
"__DEV__": true,
"__BROWSER__": false,
},
"overrides": [
{
+1 -4
View File
@@ -1,9 +1,6 @@
MIT License
Copyright (c) 2016-2019
- Declan de Wet
- Sébastien Chopin
- All the amazing contributors (https://github.com/nuxt/vue-meta/graphs/contributors)
Copyright (c) 2021 - Pim (@pimlie)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
-43
View File
@@ -1,43 +0,0 @@
const path = require('path')
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
targets: {
ie: 9
}
}],
'@babel/preset-typescript',
],
plugins: [
'dynamic-import-node',
['global-define', {
'__DEV__': 'true'
}],
['module-resolver', {
root: '.',
extensions: ['.ts'],
alias: {
'vue-meta': path.resolve('./src/')
}
}],
],
env: {
test: {
plugins: [
'@babel/plugin-syntax-dynamic-import',
'dynamic-import-node'
],
presets: [
[ '@babel/preset-env', {
targets: {
node: 'current'
}
}],
'@babel/preset-typescript'
],
}
}
}
+157
View File
@@ -0,0 +1,157 @@
import path from 'path'
import alias from '@rollup/plugin-alias'
// import babel from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import nodeResolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import { terser } from 'rollup-plugin-terser'
import ts from 'rollup-plugin-typescript2'
import defaultsDeep from 'lodash/defaultsDeep'
const pkg = require('../package.json')
const banner = `/**
* ${pkg.name} v${pkg.version}
* (c) ${new Date().getFullYear()}
* - Pim (@pimlie)
* - All the amazing contributors
* @license MIT
*/
`
let didTS = false
function rollupConfig({
plugins = [],
external = [],
...config
}) {
const isBrowserBuild = !config.output || !config.output.format || config.output.format === 'iife' || config.output.file.includes('-browser.')
const isProductionBuild = config.output.file.includes('.prod.')
const replaceConfig = {
exclude: 'node_modules',
delimiters: ['', ''],
values: {
'process.server' : isBrowserBuild ? 'false' : 'true', // should not be used anymore
'__DEV__': config.output.format === 'es' && !isBrowserBuild ? "(process.env.NODE_ENV !== 'production')" : !isProductionBuild,
'__BROWSER__': isBrowserBuild,
}
}
if (isBrowserBuild) {
external = ['vue']
} else {
external = Object.keys(pkg.peerDependencies)
external.push('@vue/server-renderer')
}
const thisConfig = defaultsDeep({}, config, {
input: 'src/index.ts',
output: {
name: 'VueMeta',
format: 'iife',
sourcemap: false,
banner,
externalLiveBindings: false,
globals: {
vue: 'Vue'
}
},
external,
plugins: [
replace(replaceConfig),
nodeResolve(),
commonjs(),
ts({
check: !didTS,
tsconfig: path.resolve(__dirname, '../tsconfig.json'),
cacheRoot: path.resolve(__dirname, '../node_modules/.rts2_cache'),
tsconfigOverride: {
compilerOptions: {
sourceMap: true,
declaration: !didTS,
declarationMap: !didTS,
},
exclude: ['__tests__', 'test-dts'],
},
}),
].concat(plugins),
})
if (isBrowserBuild) {
// remove the ssr renderToString helper for browser builds
thisConfig.plugins.unshift(alias({
entries: [
{ find: '.\/ssr', replacement: path.resolve(__dirname, './stub.js') },
]
}))
}
if (config.output.file.includes('.min.')) {
const terserOpts = {
module: config.output.format === 'es',
compress: {
ecma: 2015,
pure_getters: true,
},
}
thisConfig.plugins.push(terser(terserOpts))
}
didTS = true
return thisConfig
}
export default [
// umd web build
{
output: {
file: pkg.unpkg,
},
},
// minimized umd web build
{
output: {
file: pkg.unpkg.replace('.js', '.min.js'),
},
},
// common js build
{
output: {
file: pkg.main,
format: 'cjs'
},
},
// common js build
{
output: {
file: pkg.main.replace('.js', '.prod.js'),
format: 'cjs'
},
},
// esm build
{
output: {
file: pkg.module,
format: 'es'
},
},
// browser esm build
{
output: {
file: pkg.module.replace('-bundler.js', '-browser.js'),
format: 'es'
},
},
// minimized browser esm build
{
output: {
file: pkg.module.replace('-bundler.js', '-browser.min.js'),
format: 'es'
},
}
].map(rollupConfig)
+1
View File
@@ -0,0 +1 @@
-42
View File
@@ -1,42 +0,0 @@
# Vue Meta Examples
## Prepare examples
To prepare the examples to run locally, please follow these steps:
```bash
git clone https://github.com/nuxt/vue-meta
cd examples
yarn install
```
## Run the examples
When the examples are installed locally, start the example server as follows
```js
yarn start
// or
HOST=0.0.0.0 PORT=8080 yarn start
```
and browse to `http://localhost:3000` or whatever you changed the host and port to
### SSR Example
The server side rendering example is available on the cli only, to run the SSR example just run
```bash
yarn ssr
```
## Developing
If you would like to use the examples while developing or debugging `vue-meta` features or issues, please do as follows
```js
git clone https://github.com/nuxt/vue-meta
yarn install
cd examples
yarn install
yarn dev
```
+5
View File
@@ -1,3 +1,8 @@
if (!window.users) {
window.users = []
console.warn('window.users was not set')
}
window.users.push({
id: 1,
name: 'Leanne Graham',
+5
View File
@@ -1,3 +1,8 @@
if (!window.users) {
window.users = []
console.warn('window.users was not set')
}
window.users.push({
id: 2,
name: 'Ervin Howell',
+5
View File
@@ -1,3 +1,8 @@
if (!window.users) {
window.users = []
console.warn('window.users was not set')
}
window.users.push({
id: 3,
name: 'Clementine Bauch',
+5
View File
@@ -1,3 +1,8 @@
if (!window.users) {
window.users = []
console.warn('window.users was not set')
}
window.users.push({
id: 4,
name: 'Patricia Lebsack',
+45
View File
@@ -0,0 +1,45 @@
const path = require('path')
const { transformSync } = require('@babel/core')
module.exports = require('jiti')(__filename, {
cache: false,
debug: false,
transform (opts) {
const _opts = {
babelrc: false,
configFile: false,
compact: false,
retainLines: typeof opts.retainLines === 'boolean' ? opts.retainLines : true,
filename: '',
cwd: '/',
plugins: [
[require('@babel/plugin-transform-modules-commonjs'), { allowTopLevelThis: true }],
[require('@babel/plugin-transform-typescript')],
[require('babel-plugin-dynamic-import-node'), { noInterop: true }],
[require('babel-plugin-global-define'), {
__DEV__: true,
__BROWSER__: false
}],
[require('babel-plugin-module-resolver'), {
root: '.',
extensions: ['.ts'],
alias: {
'vue-meta': path.resolve(__dirname, '../src/')
}
}]
]
}
try {
return transformSync(opts.source, _opts).code || ''
} catch (err) {
return 'exports.__JITI_ERROR__ = ' + JSON.stringify({
filename: opts.filename,
line: (err.loc && err.loc.line) || 0,
column: (err.loc && err.loc.column) || 0,
code: err.code && err.code.replace('BABEL_', '').replace('PARSE_ERROR', 'ParseError'),
message: err.message.replace('/: ', '').replace(/\(.+\)\s*$/, '')
})
}
}
})
-76
View File
@@ -1,76 +0,0 @@
const {
baseParse,
transform,
generate,
processIf,
getBaseTransformPreset,
createObjectExpression,
createObjectProperty
} = require('@vue/compiler-core')
const { parse } = require('@vue/compiler-dom')
function headTransform (node, context) {
console.log('NODE', node)
if (node.type === 1 /* NodeTypes.ELEMENT */) {
return () => {
if (!context.parent.codegenNode) {
context.parent.codegenNode = createObjectExpression([])
}
const options = context.parent.codegenNode
const option = createObjectProperty(
node.tag,
node.children.length === 1 ? node.children[0] : 'null'
)
// options.properties.push(option)
}
}
}
module.exports = function (source, map) {
// TODO: add options
const ast = parse(source)
// console.log('AST', ast)
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
prefixIdentifiers: true
})
transform(ast, {
prefixIdentifiers: true,
nodeTransforms: [...nodeTransforms, headTransform],
directiveTransforms
})
const result = generate(ast, { mode: 'module' })
console.log(result.code)
this.callback(
null,
`
import { computed } from 'vue'
${result.code}
export default function (component) {
const setup = component.setup
component.setup = function (...args) {
console.log(component)
const __htmlMetaData = computed(() => {
})
return {
...setup.apply(this, args),
__htmlMetaData
}
}
}`,
map
)
/**/
}
+13 -10
View File
@@ -1,17 +1,20 @@
import fs from 'fs'
import path from 'path'
import consola from 'consola'
import express from 'express'
import rewrite from 'express-urlrewrite'
import webpack from 'webpack'
import webpackDevMiddleware from 'webpack-dev-middleware'
import WebpackConfig from './webpack.config'
import { renderPage } from './ssr/server'
const fs = require('fs')
const path = require('path')
const consola = require('consola')
const express = require('express')
const rewrite = require('express-urlrewrite')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackConfig = require('./webpack.config')
const jiti = require('./jiti')
const { renderPage } = jiti('./ssr/server.js')
// const { renderPage } = require('./ssr/server')
const app = express()
app.use(
webpackDevMiddleware(webpack(WebpackConfig), {
webpackDevMiddleware(webpack(webpackConfig(true)), {
publicPath: '/__build__/',
writeToDisk: true,
stats: {
+6 -6
View File
@@ -1,16 +1,16 @@
<!doctype html>
<html {{ htmlAttrs.text(true) }}>
<head {{ headAttrs.text() }}>
{{ head(true) }}
<html {{ htmlAttrs }}>
<head {{ headAttrs }}>
{{ head }}
<link rel="stylesheet" href="/global.css">
</head>
<body {{ bodyAttrs.text() }}>
{{ bodyPrepend(true) }}
<body {{ bodyAttrs }}>
<body-prepend id="body-prepend">{{ bodyPrepend }}</body-prepend>
<a href="/">&larr; Examples index</a>
{{ app }}
<script src="/__build__/ssr.js"></script>
{{ bodyAppend(true) }}
{{ bodyAppend }}
</body>
</html>
+15 -24
View File
@@ -1,8 +1,9 @@
import path from 'path'
import fs from 'fs-extra'
import { createSSRApp } from 'vue'
import { renderToStringWithMeta } from 'vue-meta'
import template from 'lodash/template'
import { renderToString } from '@vue/server-renderer'
import { App, createRouter, metaManager } from '../vue-router/main'
const templateFile = path.resolve(__dirname, 'app.template.html')
@@ -15,41 +16,31 @@ process.server = true
export async function renderPage ({ url }) {
console.log('renderPage', url)
const app = createSSRApp(App)
const router = createRouter('/ssr', true)
app.use(router)
// app.use(metaManager)
app.use(metaManager)
console.log('renderPage', 'push')
await router.push(url.substr(4))
await router.isReady()
console.log('renderPage', 'eady')
/* console.log(router)
const matchedComponents = router.getMatchedComponents()
// no matched routes, reject with 404
if (!matchedComponents.length) {
return reject({ code: 404 })
} */
const appHtml = await renderToString(app)
const [appHtml, ctx] = await renderToStringWithMeta(app)
if (!ctx.teleports) {
ctx.teleports = {}
}
const pageHtml = compiled({
app: appHtml,
htmlAttrs: {
text: () => {}
},
headAttrs: {
text: () => {}
},
bodyAttrs: {
text: () => {}
},
head: () => {},
bodyPrepend: () => {},
bodyAppend: () => {}
// ...app.$meta().inject()
htmlAttrs: ctx.teleports.htmlAttrs || '',
headAttrs: ctx.teleports.headAttrs || '',
bodyAttrs: ctx.teleports.bodyAttrs || '',
head: ctx.teleports.head || '',
bodyPrepend: ctx.teleports['body-prepend'] || '',
bodyAppend: ctx.teleports.body || ''
})
return pageHtml
+14 -11
View File
@@ -29,7 +29,7 @@ export default {
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
]
},
body: 'body-script1.js',
body: 'body-script1.js', // TODO: fix
htmlAttrs: {
amp: true,
lang: ['en', 'nl']
@@ -44,7 +44,7 @@ export default {
// TODO { content: 'window.a = "<br/>"; </script><script>alert(\'asdasd\');' },
// TODO { rawContent: 'window.b = "<br/>"; </script><script> alert(\'123321\');' },
{ src: 'body-script2.js', to: 'body' },
{ src: 'body-script3.js', to: '#put-it-here' }
{ src: 'body-script3.js', to: 'body-prepend' }
]
/* esi: {
children: [
@@ -115,20 +115,15 @@ export default {
}
}
setTimeout(() => walk(metadata), 1000) */
return {
metadata
}
},
template: `
<metainfo>
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
/*
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
<template v-slot:og(title)="{ content, metainfo, og }">
{{ content }} - {{ og.description }} - {{ metainfo.description }} - Hello Again
</template>
<!-- // TODO: Using script triggers [Vue warn]: Template compilation error: Tags with side effect (<script> and <style>) are ignored in client component templates. -->
<!-- // TODO: Using script triggers [Vue warn]: Template compilation error: Tags with side effect (<script> and <style>) are i
gnored in client component templates. -->
<component is="script">window.users = []</component>
<component is="script" src="user-1.js"></component>
<component is="script" src="user-2.js"></component>
@@ -147,6 +142,14 @@ export default {
<template v-slot:body>
<component is="script" src="user-4.js"></component>
</template>
*/
return {
metadata
}
},
template: `
<metainfo>
<template v-slot:body><br/></template>
</metainfo>
<div id="app">
+1 -1
View File
@@ -12,9 +12,9 @@
</style>
</head>
<body>
<body-prepend id="body-prepend"></body-prepend>
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<div id="put-it-here"></div>
<script src="/__build__/vue-router.js"></script>
</body>
</html>
+2 -2
View File
@@ -1,6 +1,6 @@
import { h } from 'vue'
import { createRouter as createVueRouter, createMemoryHistory, createWebHistory } from 'vue-router'
import { createManager, defaultConfig, resolveOption, useMeta } from 'vue-meta'
import { createMetaManager, defaultConfig, resolveOption, useMeta } from 'vue-meta'
import App from './App'
import ChildComponent from './Child'
@@ -20,7 +20,7 @@ const decisionMaker5000000 = resolveOption((prevValue, context) => {
}
})
const metaManager = createManager({
const metaManager = createMetaManager({
...defaultConfig,
esi: {
group: true,
+81 -77
View File
@@ -1,89 +1,93 @@
import fs from 'fs'
import path from 'path'
import webpack from 'webpack'
import WebpackBar from 'webpackbar'
import { VueLoaderPlugin } from 'vue-loader'
const fs = require('fs')
const path = require('path')
const webpack = require('webpack')
const WebpackBar = require('webpackbar')
const { VueLoaderPlugin } = require('vue-loader')
// const srcDir = path.join(__dirname, '..', 'src')
export default {
devtool: 'inline-source-map',
mode: 'development',
entry: fs.readdirSync(__dirname)
.reduce((entries, dir) => {
const fullDir = path.join(__dirname, dir)
module.exports = (isBrowser) => {
const extraAliases = {}
if (isBrowser) {
extraAliases['./ssr$'] = path.resolve(__dirname, '../build/stub.js')
}
if (dir === 'ssr') {
entries[dir] = path.join(fullDir, 'browser.js')
} else if (dir === 'vue-router') {
const possibleEntries = ['browser', 'app']
for (const entryName of possibleEntries) {
const entry = path.join(fullDir, entryName + '.js')
return {
devtool: 'inline-source-map',
mode: 'development',
entry: fs.readdirSync(__dirname)
.reduce((entries, dir) => {
const fullDir = path.join(__dirname, dir)
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = entry
break
if (dir === 'ssr') {
entries[dir] = path.join(fullDir, 'browser.js')
} else if (dir === 'vue-router') {
const possibleEntries = ['browser', 'app']
for (const entryName of possibleEntries) {
const entry = path.join(fullDir, entryName + '.js')
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = entry
break
}
}
}
}
return entries
}, {}),
output: {
path: path.join(__dirname, '__build__'),
filename: '[name].js',
chunkFilename: '[id].chunk.js',
publicPath: '/__build__/'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.vue$/,
use: 'vue-loader'
return entries
}, {}),
output: {
path: path.join(__dirname, '__build__'),
filename: '[name].js',
chunkFilename: '[id].chunk.js',
publicPath: '/__build__/'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
resolve: {
extensions: ['.tsx', 'd.ts', '.ts', '.js', '.vue'],
alias: {
// this isn't technically needed, since the default `vue` entry for bundlers
// is a simple `export * = require(''@vue/runtime-dom`. However having this
// extra re-export somehow causes webpack to always invalidate the module
// on the first HMR update and causes the page to reload.
vue: 'vue/dist/vue.esm-bundler.js',
'vue-meta': path.resolve(__dirname, '../src/'),
...extraAliases
}
]
},
resolve: {
extensions: ['.tsx', 'd.ts', '.ts', '.js', '.vue'],
alias: {
// this isn't technically needed, since the default `vue` entry for bundlers
// is a simple `export * from '@vue/runtime-dom`. However having this
// extra re-export somehow causes webpack to always invalidate the module
// on the first HMR update and causes the page to reload.
vue: 'vue/dist/vue.esm-bundler.js',
'vue-meta': path.resolve(__dirname, '../src/')
},
// Expose __dirname to allow automatically setting basename.
context: __dirname,
node: {
__dirname: true
},
plugins: [
new WebpackBar(),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
__BROWSER__: JSON.stringify(true),
'process.client': JSON.stringify(true),
'process.server': JSON.stringify(false)
})
],
devServer: {
inline: true,
hot: true,
stats: 'minimal',
contentBase: __dirname,
overlay: true
}
},
// Expose __dirname to allow automatically setting basename.
context: __dirname,
node: {
__dirname: true
},
plugins: [
new WebpackBar(),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
__VUE_OPTIONS_API__: JSON.stringify(true),
__VUE_PROD_DEVTOOLS__: JSON.stringify(true)
})
],
devServer: {
inline: true,
hot: true,
stats: 'minimal',
contentBase: __dirname,
overlay: true
}
}
+3 -1
View File
@@ -1,5 +1,6 @@
module.exports = {
testEnvironment: 'jest-environment-jsdom-global',
preset: 'ts-jest',
expand: true,
@@ -41,6 +42,7 @@ module.exports = {
],
globals: {
__DEV__: true
__DEV__: true,
__BROWSER__: true,
}
}
+30 -28
View File
@@ -2,13 +2,16 @@
"name": "vue-meta",
"version": "2.3.3",
"description": "Manage HTML metadata in Vue.js components with SSR support",
"main": "dist/vue-meta.common.js",
"web": "dist/vue-meta.js",
"module": "dist/vue-meta.esm.js",
"typings": "types/index.d.ts",
"main": "dist/vue-meta.cjs.js",
"unpkg": "dist/vue-meta.global.js",
"jsdelivr": "dist/vue-meta.global.js",
"module": "dist/vue-meta.esm-bundler.js",
"typings": "dist/vue-meta.d.ts",
"sideEffects": false,
"files": [
"dist",
"types/*.d.ts"
"dist/*.js",
"types/*.d.ts",
"README.md"
],
"homepage": "https://github.com/nuxt/vue-meta",
"bugs": "https://github.com/nuxt/vue-meta/issues",
@@ -30,12 +33,12 @@
],
"author": "Pim (@pimlie)",
"scripts": {
"build": "rimraf dist && rollup -c scripts/rollup.config.js",
"build": "yarn clean && rollup -c build/rollup.config.js",
"clean": "rimraf dist/*",
"coverage": "codecov",
"dev": "babel-node examples/server.js",
"dev": "yarn clean && node examples/server.js",
"docs": "vuepress dev --host 0.0.0.0 --port 3000 docs",
"docs:build": "vuepress build docs",
"examples": "babel-node --extensions '.ts,.js' examples/server.js",
"lint": "eslint --ext .js,.ts src test examples",
"prerelease": "git checkout master && git pull -r",
"release": "yarn lint && yarn test && standard-version",
@@ -43,21 +46,25 @@
"test:e2e-ssr": "jest test/e2e/ssr",
"test:e2e-browser": "jest test/e2e/browser",
"test:unit": "jest test/unit",
"test:types": "tsc -p types/test"
"test:types": "tsc --build tsconfig.json"
},
"pperDependencies": {
"vue": "next"
"peerDependencies": {
"@vue/server-renderer": "^3.0.5",
"vue": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/preset-typescript": "^7.12.7",
"@nuxtjs/eslint-config-typescript": "^5.0.0",
"@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.1.0",
"@rollup/plugin-replace": "^2.3.4",
"@types/webpack": "^4.41.26",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"@vue/compiler-sfc": "^3.0.5",
"@vue/server-renderer": "^3.0.5",
"@vue/server-test-utils": "^1.1.2",
@@ -69,7 +76,7 @@
"babel-plugin-global-define": "^1.0.3",
"babel-plugin-module-resolver": "^4.1.0",
"browserstack-local": "^1.4.8",
"chromedriver": "^87.0.5",
"chromedriver": "^88.0.0",
"codecov": "^3.8.1",
"consola": "^2.15.0",
"eslint": "^7.18.0",
@@ -79,17 +86,13 @@
"jest": "^26.6.3",
"jest-environment-jsdom": "^26.6.2",
"jest-environment-jsdom-global": "^2.0.4",
"jiti": "^1.3.0",
"jsdom": "^16.4.0",
"lodash": "^4.17.20",
"node-env-file": "^0.1.8",
"puppeteer-core": "^5.5.0",
"rimraf": "^3.0.2",
"rollup": "^2.36.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0",
"rollup": "^2.38.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.29.0",
"selenium-webdriver": "^4.0.0-alpha.8",
@@ -97,15 +100,14 @@
"tib": "^0.7.5",
"ts-jest": "^26.4.4",
"ts-loader": "^8.0.14",
"ts-node": "^9.1.1",
"typescript": "^4.1.3",
"vue": "^3.0.0",
"vue": "^3.0.5",
"vue-jest": "^3.0.7",
"vue-loader": "^16.0.0",
"vue-router": "next",
"webpack": "^5.15.0",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-cli": "^4.3.1",
"webpack": "^5.17.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^3.11.2",
"webpackbar": "^4.0.0"
}
-181
View File
@@ -1,181 +0,0 @@
import commonjs from 'rollup-plugin-commonjs'
import nodeResolve from 'rollup-plugin-node-resolve'
import json from 'rollup-plugin-json'
import babel from 'rollup-plugin-babel'
import replace from 'rollup-plugin-replace'
import { terser } from 'rollup-plugin-terser'
import defaultsDeep from 'lodash/defaultsDeep'
import { defaultOptions } from '../src/shared/constants'
const pkg = require('../package.json')
const version = pkg.version
const banner = `/**
* vue-meta v${version}
* (c) ${new Date().getFullYear()}
* - Declan de Wet
* - Sébastien Chopin (@Atinux)
* - Pim (@pimlie)
* - All the amazing contributors
* @license MIT
*/
`
const babelConfig = () => ({
presets: [
['@babel/preset-env', {
/*useBuiltIns: 'usage',
corejs: 2,*/
targets: {
node: 8,
ie: 9,
safari: '5.1'
}
}]
]
})
const internalObjectProperties = [
// Plugin options
// NOTE, see shared/options for why/how this is possible to do
...Object.keys(defaultOptions),
'refreshOnceOnNavigation',
// Runtime state props on $root._vueMeta
'appId',
'pausing',
'navGuards',
'initialized',
'initializing',
'deprecationWarningShown',
// updateClientMetaInfo return props
'tagsAdded',
'tagsRemoved',
// escapeOptions
'doEscape',
// deepmerge
'isMergeableObject',
'arrayMerge'
]
const terserOpts = {
nameCache: {},
mangle: {
properties: {
//debug: '___DEBUGGGG___',
// minimize all object properties except when they are quotes like obj['prop']
keep_quoted: "strict",
// and minimize props listed in internalObjectProperties
regex: new RegExp(`^(${internalObjectProperties.join('|')})$`)
}
}
}
function rollupConfig({
plugins = [],
...config
}) {
const isBrowserBuild = !config.output || !config.output.format || config.output.format === 'umd' || config.output.file.includes('.browser.')
const replaceConfig = {
exclude: 'node_modules/(?!is-mergeable-object)',
delimiters: ['', ''],
values: {
// replaceConfig needs to have some values
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
'process.env.VERSION': `"${version}"`,
'process.server' : isBrowserBuild ? 'false' : 'true',
/* remove unused stuff from deepmerge */
// remove react stuff from is-mergeable-object
'|| isReactElement(value)': '|| false',
// we always provide an arrayMerge, remove default
'|| defaultArrayMerge' : '',
// clone is a deprecated option we dont use
'options.clone ' : 'false ',
// we dont provide a custom merge
'options.customMerge)' : 'false)',
// we dont use this helper
'deepmerge.all = ' : 'false;',
// we dont use symbols on our objects
'.concat(getEnumerableOwnPropertySymbols(target))': ''
}
}
/* / keep simple polyfills when babel plugin is used for build
if (plugins && plugins.some(p => p.name === 'babel')) {
replaceConfig.values = {
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
}
}*/
return defaultsDeep({}, config, {
input: 'src/index.js',
output: {
name: 'VueMeta',
format: 'umd',
sourcemap: false,
banner
},
plugins: [
json(),
nodeResolve(),
replace(replaceConfig),
commonjs(),
babel(babelConfig()),
].concat(plugins),
})
}
export default [
// umd web build
{
output: {
file: pkg.web,
}
},
// minimized umd web build
{
output: {
file: pkg.web.replace('.js', '.min.js'),
},
plugins: [
terser(terserOpts)
]
},
// common js build
{
output: {
file: pkg.main,
format: 'cjs'
},
external: Object.keys(pkg.dependencies)
},
// esm build
{
output: {
file: pkg.web.replace('.js', '.esm.js'),
format: 'es'
},
external: Object.keys(pkg.dependencies)
},
// browser esm build
{
output: {
file: pkg.web.replace('.js', '.esm.browser.js'),
format: 'es'
},
external: Object.keys(pkg.dependencies)
},
// minimized browser esm build
{
output: {
file: pkg.web.replace('.js', '.esm.browser.min.js'),
format: 'es'
},
plugins: [
terser(terserOpts)
],
external: Object.keys(pkg.dependencies)
}
].map(rollupConfig)
-36
View File
@@ -1,36 +0,0 @@
import { readFileSync, writeFileSync } from 'fs'
import updateSection from 'update-section'
import { name, main, version } from '../package.json'
console.log(`Updating CDN info to latest v${version} release...`)
const readmePath = './README.md'
const cdnUrl = `https://unpkg.com/${name}@${version}/${main}`
const minifiedUrl = cdnUrl.replace('.js', '.min.js')
const content = readFileSync(readmePath, 'utf-8')
const update = `
<!-- start CDN generator - do **NOT** remove this comment -->
**Uncompressed:**
\`\`\`html
<script src="${cdnUrl}"></script>
\`\`\`
**Minified:**
\`\`\`html
<script src="${minifiedUrl}"></script>
\`\`\`
<!-- end CDN generator - do **NOT** remove this comment -->
`.trim().replace(/ {2}/gm, '')
const updated = updateSection(
content,
update,
(line) => (/<!-- start CDN generator/).test(line),
(line) => (/<!-- end CDN generator/).test(line)
)
writeFileSync(readmePath, updated)
console.log('CDN info updated.')
+3 -59
View File
@@ -1,78 +1,22 @@
import { h, defineComponent, Teleport, VNode, VNodeProps } from 'vue'
import { isArray, isFunction } from '@vue/shared'
import { renderMeta } from './render'
import { useMetainfo, getCurrentManager } from './useApi'
import { defineComponent, VNodeProps } from 'vue'
import { getCurrentManager } from './useApi'
import { MetainfoActive } from './types'
export interface MetainfoProps {
metainfo: MetainfoActive
}
export function addVnode (teleports: any, to: string, vnode: VNode | Array<VNode>) {
if (!teleports[to]) {
teleports[to] = []
}
if (isArray(vnode)) {
teleports[to].push(...vnode)
return
}
teleports[to].push(vnode)
}
export const MetainfoImpl = defineComponent({
name: 'Metainfo',
inheritAttrs: false,
setup (_, { slots }) {
const metainfo = useMetainfo()
return () => {
const teleports: any = {}
const manager = getCurrentManager()
if (!manager) {
return
}
for (const key in metainfo) {
const config = manager.config[key] || {}
const vnodes = renderMeta(
{ metainfo, slots },
key,
metainfo[key],
config
)
if (!vnodes) {
continue
}
const defaultTo =
(key !== 'base' && metainfo[key].to) || config.to || 'head'
if (isArray(vnodes)) {
for (const { to, vnode } of vnodes) {
addVnode(teleports, to || defaultTo, vnode)
}
continue
}
const { to, vnode } = vnodes
addVnode(teleports, to || defaultTo, vnode)
}
for (const tag of ['default', 'head', 'body']) {
const slotFn = slots[tag]
if (isFunction(slotFn)) {
addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo }))
}
}
return Object.keys(teleports).map((to) => {
return h(Teleport, { to }, teleports[to])
})
return manager.render({ slots })
}
}
})
+1
View File
@@ -1,2 +1,3 @@
// Global compile-time constants
declare let __DEV__: boolean
declare let __BROWSER__: boolean
+10 -2
View File
@@ -1,5 +1,13 @@
import * as deepestResolver from './resolvers/deepest'
export { defaultConfig } from './config'
export { createManager } from './manager'
export { createMetaManager } from './manager'
export { resolveOption } from './resolvers'
export * from './useApi'
export * from './ssr'
export * from './types'
export * from './useApi'
export {
deepestResolver
}
-17
View File
@@ -1,17 +0,0 @@
import { App } from 'vue'
import { Metainfo } from './Metainfo'
import { metaInfoKey } from './symbols'
import { Manager } from './manager'
declare module '@vue/runtime-core' {
interface ComponentInternalInstance {
$metaManager: Manager
}
}
export function applyMetaPlugin (app: App, manager: Manager, active: Object) {
app.component('Metainfo', Metainfo)
app.config.globalProperties.$metaManager = manager
app.provide(metaInfoKey, active)
}
+99 -13
View File
@@ -1,21 +1,46 @@
import { App, reactive, onUnmounted, ComponentInternalInstance } from 'vue'
import { isFunction } from '@vue/shared'
import { h, reactive, onUnmounted, Teleport, VNode, Comment } from 'vue'
import { isArray, isFunction } from '@vue/shared'
import { createMergedObject } from './object-merge'
import { applyMetaPlugin } from './install'
// import * as deepestResolver from './resolvers/deepest'
import { Config, Resolver, MetainfoInput, MetaContext, MetaProxy } from './types'
import { renderMeta } from './render'
import { metaInfoKey } from './symbols'
import { Metainfo } from './Metainfo'
import type { ResolveMethod } from './object-merge'
import type { Manager, Config, Resolver, MetaContext, MetainfoActive } from './types'
export type Manager = {
readonly config: Config
export const ssrAttribute = 'data-vm-ssr'
install(app: App): void
addMeta(obj: MetainfoInput, vm?: ComponentInternalInstance): MetaProxy
export const active: MetainfoActive = reactive({})
export function addVnode (teleports: any, to: string, _vnodes: VNode | Array<VNode>) {
const vnodes = (isArray(_vnodes) ? _vnodes : [_vnodes]) as Array<VNode>
if (!__BROWSER__) {
// dont add ssrAttribute for attribute vnode placeholder
if (!to.endsWith('Attrs')) {
vnodes.forEach((vnode) => {
if (!vnode.props) {
vnode.props = {}
}
vnode.props[ssrAttribute] = true
})
}
} else {
// Comments shouldnt have any use on the client as they are not reactive anyway
vnodes.forEach((vnode, idx) => {
if (vnode.type === Comment) {
vnodes.splice(idx, 1)
}
})
}
if (!teleports[to]) {
teleports[to] = []
}
teleports[to].push(...vnodes)
}
export const active = reactive({})
export function createManager (config: Config, resolver: Resolver | ResolveMethod): Manager {
export function createMetaManager (config: Config, resolver: Resolver | ResolveMethod): Manager {
const resolve: ResolveMethod = (options, contexts, active, key, pathSegments) => {
if (isFunction(resolver)) {
return resolver(options, contexts, active, key, pathSegments)
@@ -26,12 +51,17 @@ export function createManager (config: Config, resolver: Resolver | ResolveMetho
const { addSource, delSource } = createMergedObject(resolve, active)
let cleanedUpSsr = false
// TODO: validate resolver
const manager: Manager = {
config,
install (app) {
applyMetaPlugin(app, this, active)
app.component('Metainfo', Metainfo)
app.config.globalProperties.$metaManager = manager
app.provide(metaInfoKey, active)
},
addMeta (metaObj, vm) {
@@ -52,6 +82,62 @@ export function createManager (config: Config, resolver: Resolver | ResolveMetho
meta,
unmount
}
},
render ({ slots }: any = {}): Array<VNode> {
// cleanup ssr tags if not yet done
if (__BROWSER__ && !cleanedUpSsr) {
cleanedUpSsr = true
// Listen for DOM loaded because tags in the body couldnt be loaded
// yet once the manager does it first render
// (preferable there should only be one render on hydration)
window.addEventListener('DOMContentLoaded', () => {
const ssrTags = document.querySelectorAll(`[${ssrAttribute}]`)
if (ssrTags && ssrTags.length) {
Array.from(ssrTags).forEach(el => el.parentNode && el.parentNode.removeChild(el))
}
})
}
const teleports: { [key: string]: VNode | Array<VNode> } = {}
for (const key in active) {
const config = this.config[key] || {}
const vnode = renderMeta(
{ metainfo: active, slots },
key,
active[key],
config
)
if (!vnode) {
continue
}
const vnodes = isArray(vnode) ? vnode : [vnode]
const defaultTo = (key !== 'base' && active[key].to) || config.to || (config.attributesFor ? key : 'head')
for (const { to, vnode } of vnodes) {
addVnode(teleports, to || defaultTo, vnode)
}
}
if (slots) {
for (const tag in slots) {
const slotFn = slots[tag]
if (isFunction(slotFn)) {
addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo: active }))
}
}
}
return Object.keys(teleports).map((to) => {
return h(Teleport, { to }, teleports[to])
})
}
}
+1
View File
@@ -6,6 +6,7 @@ export type MergeSource = {
[key: string]: any
}
// eslint-disable-next-line no-use-before-define
export type MergedObjectValue = boolean | number | string | MergedObject | any
export type MergedObject = {
+5 -5
View File
@@ -51,7 +51,7 @@ export const createHandler: (context: MergeContext, resolveContext: ResolveConte
},
set: (target, key, value) => {
const success = Reflect.set(target, key, value)
console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context)
// console.warn(success, 'PROXY SET\nkey:', key, '\npath:', pathSegments, '\ntarget:', isArray(target), target, '\ncontext:\n', context)
if (success) {
const isArrayItem = isArray(target)
@@ -113,7 +113,7 @@ export const createHandler: (context: MergeContext, resolveContext: ResolveConte
resolved = clone(resolved)
}
console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active))
// console.log('SET VALUE', isArrayItem, key, '\nresolved:\n', resolved, '\nsources:\n', context.sources, '\nactive:\n', active, Object.keys(active))
if (isArrayItem && activeSegmentKey) {
active[activeSegmentKey] = resolved
@@ -122,12 +122,12 @@ export const createHandler: (context: MergeContext, resolveContext: ResolveConte
}
}
console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target)
// console.log('CONTEXT.ACTIVE', context.active, '\nparent:\n', target)
return success
},
deleteProperty: (target, key) => {
const success = Reflect.deleteProperty(target, key)
console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target)
// console.warn('PROXY DELETE\nkey:', key, '\npath:', pathSegments, '\nparent:', isArray(target), target)
if (success) {
const isArrayItem = isArray(target)
@@ -174,7 +174,7 @@ export const createHandler: (context: MergeContext, resolveContext: ResolveConte
resolved = clone(resolved)
}
console.log('SET VALUE', resolved)
// console.log('SET VALUE', resolved)
if (isArrayItem && activeSegmentKey) {
active[activeSegmentKey] = resolved
} else {
+26 -8
View File
@@ -233,19 +233,37 @@ export function renderAttributes (
key: string,
data: TODO,
config: TODO = {}
): void {
): RenderedMetainfoNode | void {
// console.info('renderAttributes', key, data, config)
const { attributesFor } = config
if (!cachedElements[attributesFor]) {
const el = document.querySelector(attributesFor)
if (!__BROWSER__) {
// render attributes in a placeholder vnode so Vue
// will render the string for us
return {
to: '',
vnode: h(`ssr-${attributesFor}`, data)
}
}
if (el) {
cachedElements[attributesFor] = {
el,
attrs: []
}
if (!cachedElements[attributesFor]) {
const [el, el2] = Array.from(document.querySelectorAll(attributesFor))
if (__DEV__ && !el) {
// eslint-disable-next-line no-console
console.error('Could not find element with selector', attributesFor, ', won\'t render attributes')
return
}
if (__DEV__ && el2) {
// eslint-disable-next-line no-console
console.warn('Found multiple elements with selector', attributesFor)
}
cachedElements[attributesFor] = {
el,
attrs: []
}
}
+29
View File
@@ -0,0 +1,29 @@
import type { App } from 'vue'
import type { SSRContext } from '@vue/server-renderer'
// rollup doesnt like an import, cant find export so use require
const { renderToString } = require('@vue/server-renderer')
export async function renderToStringWithMeta (app: App): Promise<[string, SSRContext]> {
const ctx: SSRContext = {}
const html = await renderToString(app, ctx)
// TODO: better way of determining whether meta was rendered with the component or not
if (!ctx.teleports || !ctx.teleports.head) {
const teleports = app.config.globalProperties.$metaManager.render()
await Promise.all(teleports.map((teleport: any) => renderToString(teleport, ctx)))
}
const { teleports } = ctx
for (const target in teleports) {
if (target.endsWith('Attrs')) {
const str = teleports[target]
// match from first space to first >, these should be all rendered attributes
teleports[target] = str.slice(str.indexOf(' ') + 1, str.indexOf('>'))
}
}
return [html, ctx]
}
+25 -5
View File
@@ -1,10 +1,6 @@
import { ComponentInternalInstance } from 'vue'
import type { App, VNode, ComponentInternalInstance } from 'vue'
import type { MergedObject, ResolveContext, ResolveMethod } from '../object-merge'
export type Immutable<T> = {
readonly [P in keyof T]: Immutable<T[P]>
}
export type TODO = any
export type MetainfoInput = {
@@ -50,3 +46,27 @@ export type Resolver = {
setup?: ResolveSetup
resolve: ResolveMethod
}
export type Manager = {
readonly config: Config
install(app: App): void
addMeta(obj: MetainfoInput, vm?: ComponentInternalInstance): MetaProxy
render(ctx: { slots?: any }): Array<VNode>
}
declare module '@vue/runtime-core' {
interface ComponentInternalInstance {
$metaManager: Manager
}
}
declare global {
namespace NodeJS {
interface Process {
client: boolean
server: boolean
}
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
import { inject, getCurrentInstance, ComponentInternalInstance } from 'vue'
import { Manager } from './manager'
import { metaInfoKey } from './symbols'
import { MetainfoActive, MetainfoInput, MetaProxy } from './types'
import type { Manager, MetainfoActive, MetainfoInput, MetaProxy } from './types'
export function getCurrentManager (vm?: ComponentInternalInstance): Manager {
if (!vm) {
+2 -2
View File
@@ -478,10 +478,10 @@ describe('render', () => {
const setAttribute = jest.fn()
const removeAttribute = jest.fn()
const doc = jest.spyOn(document, 'querySelector').mockReturnValue({
const doc = jest.spyOn(document, 'querySelectorAll').mockReturnValue([{
setAttribute,
removeAttribute
})
}])
const context = {}
+1 -1
View File
@@ -30,6 +30,6 @@
"include": [
"src/global.d.ts",
"src/**/*.ts",
"__tests__/**/*.ts"
"test/**/*.ts"
],
}
+231 -949
View File
File diff suppressed because it is too large Load Diff