mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-23 21:30:34 +03:00
feat: major refactor, cleanup and jest tests
This commit is contained in:
@@ -1,8 +1,3 @@
|
|||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"presets": ["@babel/preset-env"]
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": ["istanbul"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"@nuxtjs"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"Vue": "readable"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,3 +38,7 @@ jspm_packages
|
|||||||
|
|
||||||
# built code
|
# built code
|
||||||
lib
|
lib
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
|
|||||||
@@ -847,4 +847,6 @@ If this were not the case, you would have to instruct Babel to convert `default`
|
|||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
To run the examples; clone this repository & run `npm install` in the root directory, and then run `npm run dev`. Head to http://localhost:8080.
|
To run the examples; clone this repository & run `cd examples && npm install` in the root directory, and then run `npm run start`. Head to http://localhost:8080.
|
||||||
|
|
||||||
|
If you would like to help to develop vue-meta then run `npm install` both in the root and examples dir and run `npm run dev`
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env"]
|
||||||
|
}
|
||||||
Generated
+7010
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-meta-examples",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Examples for vue-meta",
|
||||||
|
"main": "server.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=dev babel-node server.js",
|
||||||
|
"start": "babel-node server.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/nuxt/vue-meta.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/nuxt/vue-meta/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/nuxt/vue-meta#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.2.2",
|
||||||
|
"@babel/node": "^7.2.2",
|
||||||
|
"@babel/preset-env": "^7.3.1",
|
||||||
|
"babel-loader": "^8.0.5",
|
||||||
|
"cross-env": "^5.2.0",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"express-urlrewrite": "^1.2.0",
|
||||||
|
"vue": "^2.6.3",
|
||||||
|
"vue-loader": "^15.6.2",
|
||||||
|
"vue-meta": "^1.5.8",
|
||||||
|
"vue-router": "^3.0.2",
|
||||||
|
"vue-template-compiler": "^2.6.3",
|
||||||
|
"vuex": "^3.1.0",
|
||||||
|
"webpack": "^4.26.1",
|
||||||
|
"webpack-dev-server": "^3.1.10",
|
||||||
|
"webpackbar": "^3.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -24,7 +24,9 @@ fs.readdirSync(__dirname).forEach(file => {
|
|||||||
|
|
||||||
app.use(express.static(__dirname))
|
app.use(express.static(__dirname))
|
||||||
|
|
||||||
|
const host = process.env.HOST || 'localhost'
|
||||||
const port = process.env.PORT || 8080
|
const port = process.env.PORT || 8080
|
||||||
module.exports = app.listen(port, () => {
|
|
||||||
console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`)
|
module.exports = app.listen(port, host, () => {
|
||||||
|
console.log(`Server listening on http://${host}:${port}, Ctrl+C to stop`)
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
const Vue = require('vue')
|
const Vue = require('vue')
|
||||||
const renderer = require('vue-server-renderer').createRenderer()
|
const renderer = require('vue-server-renderer').createRenderer()
|
||||||
const VueMeta = require('../')
|
const VueMeta = require(process.env.NODE_ENV === 'development' ? '../' : 'vue-meta')
|
||||||
|
|
||||||
Vue.use(VueMeta, {
|
Vue.use(VueMeta, {
|
||||||
tagIDKeyName: 'hid'
|
tagIDKeyName: 'hid'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const Vue = require('vue')
|
const Vue = require('vue')
|
||||||
const renderer = require('vue-server-renderer').createRenderer()
|
const renderer = require('vue-server-renderer').createRenderer()
|
||||||
const VueMeta = require('../')
|
const VueMeta = require(process.env.NODE_ENV === 'development' ? '../' : 'vue-meta')
|
||||||
|
|
||||||
Vue.use(VueMeta, {
|
Vue.use(VueMeta, {
|
||||||
tagIDKeyName: 'hid'
|
tagIDKeyName: 'hid'
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import assign from 'object-assign'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueMeta from 'vue-meta'
|
import VueMeta from 'vue-meta'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
@@ -41,6 +40,7 @@ const router = new Router({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const App = {
|
const App = {
|
||||||
|
router,
|
||||||
template: `
|
template: `
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<h1>vue-router</h1>
|
<h1>vue-router</h1>
|
||||||
@@ -54,6 +54,6 @@ const App = {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new Vue(assign(App, { router }))
|
const app = new Vue(App)
|
||||||
|
|
||||||
app.$mount('#app')
|
app.$mount('#app')
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import assign from 'object-assign'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
const app = new Vue(assign(App, { router, store }))
|
App.router = router
|
||||||
|
App.store = store
|
||||||
|
|
||||||
app.$mount('#app')
|
new Vue(App).$mount('#app')
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import assign from 'object-assign'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
const app = new Vue(assign(App, { router, store }))
|
App.router = router
|
||||||
|
App.store = store
|
||||||
|
|
||||||
app.$mount('#app')
|
new Vue(App).$mount('#app')
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
|
import WebpackBar from 'webpackbar'
|
||||||
|
import VueLoaderPlugin from 'vue-loader/lib/plugin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
@@ -28,7 +30,7 @@ export default {
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'vue': 'vue/dist/vue.js',
|
'vue': 'vue/dist/vue.js',
|
||||||
'vue-meta': path.join(__dirname, '..', 'src')
|
'vue-meta': process.env.NODE_ENV === 'development' ? path.join(__dirname, '..', 'src') : 'vue-meta'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Expose __dirname to allow automatically setting basename.
|
// Expose __dirname to allow automatically setting basename.
|
||||||
@@ -37,7 +39,8 @@ export default {
|
|||||||
__dirname: true
|
__dirname: true
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// new webpack.optimize.CommonsChunkPlugin('shared.js'),
|
new WebpackBar(),
|
||||||
|
new VueLoaderPlugin(),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
|
||||||
|
expand: true,
|
||||||
|
|
||||||
|
forceExit: false,
|
||||||
|
|
||||||
|
// https://github.com/facebook/jest/pull/6747 fix warning here
|
||||||
|
// But its performance overhead is pretty bad (30+%).
|
||||||
|
// detectOpenHandles: true
|
||||||
|
|
||||||
|
setupFilesAfterEnv: ['./test/utils/setup'],
|
||||||
|
|
||||||
|
coverageDirectory: './coverage',
|
||||||
|
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'**/src/**/*.js'
|
||||||
|
],
|
||||||
|
|
||||||
|
coveragePathIgnorePatterns: [
|
||||||
|
'node_modules'
|
||||||
|
],
|
||||||
|
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
'node_modules',
|
||||||
|
'old'
|
||||||
|
],
|
||||||
|
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'node_modules'
|
||||||
|
],
|
||||||
|
|
||||||
|
transform: {
|
||||||
|
'^.+\\.js$': 'babel-jest',
|
||||||
|
'.*\\.(vue)$': 'vue-jest'
|
||||||
|
},
|
||||||
|
|
||||||
|
moduleFileExtensions: [
|
||||||
|
'ts',
|
||||||
|
'js',
|
||||||
|
'json'
|
||||||
|
],
|
||||||
|
|
||||||
|
reporters: [
|
||||||
|
'default'
|
||||||
|
].concat(process.env.JEST_JUNIT_OUTPUT ? ['jest-junit'] : [])
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import webpackConfig from './examples/webpack.config.babel'
|
|
||||||
|
|
||||||
delete webpackConfig.entry
|
|
||||||
|
|
||||||
export default (config) => {
|
|
||||||
config.set({
|
|
||||||
browsers: ['PhantomJS'],
|
|
||||||
frameworks: ['mocha', 'chai'],
|
|
||||||
reporters: ['mocha', 'coverage'],
|
|
||||||
files: ['test/index.js'],
|
|
||||||
preprocessors: {
|
|
||||||
'test/index.js': ['webpack', 'sourcemap']
|
|
||||||
},
|
|
||||||
coverageReporter: {
|
|
||||||
reporters: [
|
|
||||||
{ type: 'lcov' },
|
|
||||||
{ type: 'text' }
|
|
||||||
],
|
|
||||||
includeAllSources: true,
|
|
||||||
dir: 'coverage',
|
|
||||||
subdir: '.'
|
|
||||||
},
|
|
||||||
webpack: webpackConfig,
|
|
||||||
webpackMiddleware: {
|
|
||||||
noInfo: true
|
|
||||||
},
|
|
||||||
mochaReporter: {
|
|
||||||
showDiff: true,
|
|
||||||
output: 'full'
|
|
||||||
},
|
|
||||||
singleRun: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
require('@babel/register')
|
|
||||||
module.exports = require('./karma.conf.babel').default
|
|
||||||
+35
-65
@@ -1,87 +1,73 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-meta",
|
"name": "vue-meta",
|
||||||
"description": "manage page meta info in Vue 2.0 server-rendered apps",
|
"description": "Manage page meta info in Vue 2.0 server-rendered apps",
|
||||||
"version": "1.5.8",
|
"version": "1.5.8",
|
||||||
"author": "Declan de Wet <declandewet@me.com>",
|
"author": "Declan de Wet <declandewet@me.com>",
|
||||||
"bugs": "https://github.com/declandewet/vue-meta/issues",
|
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rimraf lib && rollup -c",
|
||||||
"codecov": "codecov",
|
"codecov": "codecov",
|
||||||
"deploy": "npm version",
|
"deploy": "npm version",
|
||||||
"dev": "babel-node examples/server.js",
|
"dev": "cd examples && npm run dev && cd ..",
|
||||||
"lint": "standard --verbose | snazzy",
|
"lint": "eslint src test",
|
||||||
"minify": "uglifyjs lib/vue-meta.js -cm --comments -o lib/vue-meta.min.js",
|
|
||||||
"postbuild": "npm run minify",
|
|
||||||
"postdeploy": "git push origin master --follow-tags && npm run release",
|
"postdeploy": "git push origin master --follow-tags && npm run release",
|
||||||
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
|
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
|
||||||
"prebuild": "rimraf lib",
|
|
||||||
"predeploy": "git checkout master && git pull -r",
|
"predeploy": "git checkout master && git pull -r",
|
||||||
"prerelease": "npm run build",
|
"prerelease": "npm run build",
|
||||||
"pretest": "npm run lint",
|
|
||||||
"preversion": "npm run toc",
|
"preversion": "npm run toc",
|
||||||
"release": "npm publish",
|
"release": "npm publish",
|
||||||
"test": "cross-env NODE_ENV=test karma start karma.conf.js",
|
"test": "jest",
|
||||||
"toc": "doctoc README.md --title '# Table of Contents'",
|
"toc": "doctoc README.md --title '# Table of Contents'",
|
||||||
"update-cdn": "babel-node scripts/update-cdn.js"
|
"update-cdn": "babel-node scripts/update-cdn.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deepmerge": "^3.0.0",
|
"deepmerge": "^3.0.0",
|
||||||
"lodash.isplainobject": "^4.0.6",
|
"lodash.isplainobject": "^4.0.6",
|
||||||
"lodash.uniqueid": "^4.0.1",
|
"lodash.uniqueid": "^4.0.1"
|
||||||
"object-assign": "^4.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.1.5",
|
|
||||||
"@babel/core": "^7.1.6",
|
"@babel/core": "^7.1.6",
|
||||||
"@babel/node": "^7.0.0",
|
"@babel/node": "^7.2.2",
|
||||||
"@babel/preset-env": "^7.1.6",
|
"@babel/preset-env": "^7.1.6",
|
||||||
"@babel/register": "^7.0.0",
|
"@nuxtjs/eslint-config": "^0.0.1",
|
||||||
|
"@vue/server-test-utils": "^1.0.0-beta.29",
|
||||||
|
"@vue/test-utils": "^1.0.0-beta.29",
|
||||||
|
"babel-core": "^7.0.0-bridge",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"babel-jest": "^24.1.0",
|
||||||
"babel-loader": "^8.0.4",
|
"babel-loader": "^8.0.4",
|
||||||
"babel-plugin-istanbul": "^5.1.0",
|
|
||||||
"chai": "^4.2.0",
|
|
||||||
"codecov": "^3.1.0",
|
"codecov": "^3.1.0",
|
||||||
"cross-env": "^5.2.0",
|
|
||||||
"css-loader": "^2.0.0",
|
|
||||||
"doctoc": "^1.4.0",
|
"doctoc": "^1.4.0",
|
||||||
"es6-promise": "^4.2.5",
|
"eslint": "^5.13.0",
|
||||||
"express": "^4.16.4",
|
"eslint-config-standard": "^12.0.0",
|
||||||
"express-urlrewrite": "^1.2.0",
|
"eslint-plugin-import": "^2.16.0",
|
||||||
"file-loader": "^3.0.0",
|
"eslint-plugin-jest": "^22.2.2",
|
||||||
"karma": "^3.1.1",
|
"eslint-plugin-node": "^8.0.1",
|
||||||
"karma-chai": "^0.1.0",
|
"eslint-plugin-promise": "^4.0.1",
|
||||||
"karma-coverage": "^1.1.2",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"karma-mocha": "^1.3.0",
|
"eslint-plugin-vue": "^5.1.0",
|
||||||
"karma-mocha-reporter": "^2.2.5",
|
"jest": "^24.1.0",
|
||||||
"karma-phantomjs-launcher": "^1.0.4",
|
"jsdom": "^13.2.0",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"jsdom-global": "^3.0.2",
|
||||||
"karma-webpack": "^3.0.5",
|
|
||||||
"mocha": "^5.2.0",
|
|
||||||
"phantomjs-prebuilt": "^2.1.16",
|
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"rollup": "^1.0.0",
|
"rollup": "^1.0.0",
|
||||||
"rollup-plugin-buble": "^0.19.4",
|
"rollup-plugin-buble": "^0.19.4",
|
||||||
"rollup-plugin-commonjs": "^9.2.0",
|
"rollup-plugin-commonjs": "^9.2.0",
|
||||||
"rollup-plugin-json": "^3.1.0",
|
"rollup-plugin-json": "^3.1.0",
|
||||||
"rollup-plugin-node-resolve": "^4.0.0",
|
"rollup-plugin-node-resolve": "^4.0.0",
|
||||||
"snazzy": "^8.0.0",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"standard": "^12.0.1",
|
|
||||||
"uglify-js": "^3.4.9",
|
|
||||||
"update-section": "^0.3.3",
|
"update-section": "^0.3.3",
|
||||||
"vue": "^2.5.17",
|
"vue": "^2.6.3",
|
||||||
"vue-loader": "^15.4.2",
|
"vue-jest": "^3.0.2",
|
||||||
"vue-router": "^3.0.2",
|
"vue-server-renderer": "^2.6.3",
|
||||||
"vue-server-renderer": "^2.5.17",
|
"vue-template-compiler": "^2.6.3"
|
||||||
"vue-template-compiler": "^2.5.17",
|
|
||||||
"vuex": "^3.0.1",
|
|
||||||
"webpack": "^4.26.1",
|
|
||||||
"webpack-dev-server": "^3.1.10"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib",
|
"lib",
|
||||||
"types/index.d.ts",
|
"types/index.d.ts",
|
||||||
"types/vue.d.ts"
|
"types/vue.d.ts"
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/declandewet/vue-meta",
|
"homepage": "https://github.com/nuxt/vue-meta",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"attribute",
|
"attribute",
|
||||||
"google",
|
"google",
|
||||||
@@ -97,28 +83,12 @@
|
|||||||
"vue"
|
"vue"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/vue-meta.js",
|
"main": "lib/vue-meta.common.js",
|
||||||
|
"web": "lib/vue-meta.js",
|
||||||
|
"module": "src/index.js",
|
||||||
"typings": "types/index.d.ts",
|
"typings": "types/index.d.ts",
|
||||||
"nyc": {
|
|
||||||
"exclude": [
|
|
||||||
"test/**/*.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "git@github.com:declandewet/vue-meta.git",
|
"url": "git@github.com/nuxt/vue-meta.git",
|
||||||
"type": "git"
|
"type": "git"
|
||||||
},
|
|
||||||
"standard": {
|
|
||||||
"globals": [
|
|
||||||
"Vue",
|
|
||||||
"define",
|
|
||||||
"describe",
|
|
||||||
"it",
|
|
||||||
"expect",
|
|
||||||
"before",
|
|
||||||
"beforeEach",
|
|
||||||
"after",
|
|
||||||
"afterEach"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-11
@@ -1,27 +1,53 @@
|
|||||||
import commonjs from 'rollup-plugin-commonjs'
|
import commonJs from 'rollup-plugin-commonjs'
|
||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||||
import json from 'rollup-plugin-json'
|
import json from 'rollup-plugin-json'
|
||||||
import buble from 'rollup-plugin-buble'
|
import buble from 'rollup-plugin-buble'
|
||||||
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
|
||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
export default {
|
const banner = `/**
|
||||||
input: './src/index.js',
|
|
||||||
output: {
|
|
||||||
file: pkg.main,
|
|
||||||
format: 'umd',
|
|
||||||
name: 'VueMeta',
|
|
||||||
banner: `/**
|
|
||||||
* vue-meta v${pkg.version}
|
* vue-meta v${pkg.version}
|
||||||
* (c) ${new Date().getFullYear()} Declan de Wet & Sébastien Chopin (@Atinux)
|
* (c) ${new Date().getFullYear()} Declan de Wet & Sébastien Chopin (@Atinux)
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
`.replace(/ {4}/gm, '').trim()
|
`.replace(/ {4}/gm, '').trim()
|
||||||
|
|
||||||
|
const baseConfig = {
|
||||||
|
input: './src/index.js',
|
||||||
|
output: {
|
||||||
|
file: pkg.web,
|
||||||
|
format: 'umd',
|
||||||
|
name: 'VueMeta',
|
||||||
|
sourcemap: false,
|
||||||
|
banner
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
json(),
|
json(),
|
||||||
nodeResolve({ jsnext: true }),
|
nodeResolve(),
|
||||||
commonjs(),
|
commonJs(),
|
||||||
buble()
|
buble(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
...baseConfig,
|
||||||
|
}, {
|
||||||
|
...baseConfig,
|
||||||
|
output: {
|
||||||
|
...baseConfig.output,
|
||||||
|
file: pkg.web.replace('.js', '.min.js'),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
...baseConfig.plugins,
|
||||||
|
terser()
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
...baseConfig,
|
||||||
|
output: {
|
||||||
|
...baseConfig.output,
|
||||||
|
file: pkg.main,
|
||||||
|
format: 'cjs'
|
||||||
|
},
|
||||||
|
external: Object.keys(pkg.dependencies)
|
||||||
|
}]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// fallback to timers if rAF not present
|
// fallback to timers if rAF not present
|
||||||
const stopUpdate = (typeof window !== 'undefined' ? window.cancelAnimationFrame : null) || clearTimeout
|
const stopUpdate = (typeof window !== 'undefined' ? window.cancelAnimationFrame : null) || clearTimeout
|
||||||
const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFrame : null) || ((cb) => setTimeout(cb, 0))
|
const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a batched update. Uses requestAnimationFrame to prevent
|
* Performs a batched update. Uses requestAnimationFrame to prevent
|
||||||
@@ -12,7 +12,7 @@ const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFram
|
|||||||
* @param {Function} callback - the update to perform
|
* @param {Function} callback - the update to perform
|
||||||
* @return {Number} id - a new ID
|
* @return {Number} id - a new ID
|
||||||
*/
|
*/
|
||||||
export default function batchUpdate (id, callback) {
|
export default function batchUpdate(id, callback) {
|
||||||
stopUpdate(id)
|
stopUpdate(id)
|
||||||
return startUpdate(() => {
|
return startUpdate(() => {
|
||||||
id = null
|
id = null
|
||||||
|
|||||||
+12
-5
@@ -1,7 +1,7 @@
|
|||||||
import getMetaInfo from '../shared/getMetaInfo'
|
import getMetaInfo from '../shared/getMetaInfo'
|
||||||
import updateClientMetaInfo from './updateClientMetaInfo'
|
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||||
|
|
||||||
export default function _refresh (options = {}) {
|
export default function _refresh(options = {}) {
|
||||||
/**
|
/**
|
||||||
* When called, will update the current meta info with new meta info.
|
* When called, will update the current meta info with new meta info.
|
||||||
* Useful when updating meta info as the result of an asynchronous
|
* Useful when updating meta info as the result of an asynchronous
|
||||||
@@ -12,9 +12,16 @@ export default function _refresh (options = {}) {
|
|||||||
*
|
*
|
||||||
* @return {Object} - new meta info
|
* @return {Object} - new meta info
|
||||||
*/
|
*/
|
||||||
return function refresh () {
|
return function refresh() {
|
||||||
const info = getMetaInfo(options)(this.$root)
|
const metaInfo = getMetaInfo(options, this.$root)
|
||||||
updateClientMetaInfo(options).call(this, info)
|
|
||||||
return info
|
const tags = updateClientMetaInfo(options, metaInfo)
|
||||||
|
|
||||||
|
// emit "event" with new info
|
||||||
|
if (tags && typeof metaInfo.changed === 'function') {
|
||||||
|
metaInfo.changed.call(this, metaInfo, tags.addedTags, tags.removedTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metaInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,75 @@
|
|||||||
import updateTitle from './updaters/updateTitle'
|
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||||
import updateTagAttributes from './updaters/updateTagAttributes'
|
|
||||||
import updateTags from './updaters/updateTags'
|
|
||||||
|
|
||||||
export default function _updateClientMetaInfo (options = {}) {
|
const getTag = (tags, tag) => {
|
||||||
|
if (!tags[tag]) {
|
||||||
|
tags[tag] = document.getElementsByTagName(tag)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags[tag]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs client-side updates when new meta info is received
|
||||||
|
*
|
||||||
|
* @param {Object} newInfo - the meta info to update to
|
||||||
|
*/
|
||||||
|
export default function updateClientMetaInfo(options = {}, newInfo) {
|
||||||
const { ssrAttribute } = options
|
const { ssrAttribute } = options
|
||||||
|
|
||||||
/**
|
// only cache tags for current update
|
||||||
* Performs client-side updates when new meta info is received
|
const tags = {}
|
||||||
*
|
|
||||||
* @param {Object} newInfo - the meta info to update to
|
|
||||||
*/
|
|
||||||
return function updateClientMetaInfo (newInfo) {
|
|
||||||
const htmlTag = document.getElementsByTagName('html')[0]
|
|
||||||
// if this is not a server render, then update
|
|
||||||
if (htmlTag.getAttribute(ssrAttribute) === null) {
|
|
||||||
// initialize tracked changes
|
|
||||||
const addedTags = {}
|
|
||||||
const removedTags = {}
|
|
||||||
|
|
||||||
Object.keys(newInfo).forEach((key) => {
|
const htmlTag = getTag(tags, 'html')
|
||||||
switch (key) {
|
|
||||||
// update the title
|
|
||||||
case 'title':
|
|
||||||
updateTitle(options)(newInfo.title)
|
|
||||||
break
|
|
||||||
// update attributes
|
|
||||||
case 'htmlAttrs':
|
|
||||||
updateTagAttributes(options)(newInfo[key], htmlTag)
|
|
||||||
break
|
|
||||||
case 'bodyAttrs':
|
|
||||||
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('body')[0])
|
|
||||||
break
|
|
||||||
case 'headAttrs':
|
|
||||||
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('head')[0])
|
|
||||||
break
|
|
||||||
// ignore these
|
|
||||||
case 'titleChunk':
|
|
||||||
case 'titleTemplate':
|
|
||||||
case 'changed':
|
|
||||||
case '__dangerouslyDisableSanitizers':
|
|
||||||
break
|
|
||||||
// catch-all update tags
|
|
||||||
default:
|
|
||||||
const headTag = document.getElementsByTagName('head')[0]
|
|
||||||
const bodyTag = document.getElementsByTagName('body')[0]
|
|
||||||
const { oldTags, newTags } = updateTags(options)(key, newInfo[key], headTag, bodyTag)
|
|
||||||
if (newTags.length) {
|
|
||||||
addedTags[key] = newTags
|
|
||||||
removedTags[key] = oldTags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// emit "event" with new info
|
// if this is not a server render, then update
|
||||||
if (typeof newInfo.changed === 'function') {
|
if (htmlTag.getAttribute(ssrAttribute) === null) {
|
||||||
newInfo.changed.call(this, newInfo, addedTags, removedTags)
|
// initialize tracked changes
|
||||||
|
const addedTags = {}
|
||||||
|
const removedTags = {}
|
||||||
|
|
||||||
|
Object.keys(newInfo).forEach((type) => {
|
||||||
|
// ignore these
|
||||||
|
if ([
|
||||||
|
'titleChunk',
|
||||||
|
'titleTemplate',
|
||||||
|
'changed',
|
||||||
|
'__dangerouslyDisableSanitizers',
|
||||||
|
'__dangerouslyDisableSanitizersByTagID'
|
||||||
|
].includes(type)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// remove the server render attribute so we can update on changes
|
if (type === 'title') {
|
||||||
htmlTag.removeAttribute(ssrAttribute)
|
// update the title
|
||||||
}
|
updateTitle(newInfo.title)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||||
|
const tagName = type.substr(0, 4)
|
||||||
|
updateAttribute(options, newInfo[type], getTag(tags, tagName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { oldTags, newTags } = updateTag(
|
||||||
|
options,
|
||||||
|
type,
|
||||||
|
newInfo[type],
|
||||||
|
getTag(tags, 'head'),
|
||||||
|
getTag(tags, 'body')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newTags.length) {
|
||||||
|
addedTags[type] = newTags
|
||||||
|
removedTags[type] = oldTags
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { addedTags, removedTags }
|
||||||
|
} else {
|
||||||
|
// remove the server render attribute so we can update on changes
|
||||||
|
htmlTag.removeAttribute(ssrAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Updates the document's html tag attributes
|
||||||
|
*
|
||||||
|
* @param {Object} attrs - the new document html attributes
|
||||||
|
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
||||||
|
*/
|
||||||
|
export default function updateAttribute({ attribute } = {}, attrs, tag) {
|
||||||
|
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||||
|
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
||||||
|
const toRemove = [].concat(vueMetaAttrs)
|
||||||
|
|
||||||
|
for (const attr in attrs) {
|
||||||
|
if (attrs.hasOwnProperty(attr)) {
|
||||||
|
const val = attrs[attr] || ''
|
||||||
|
tag.setAttribute(attr, val)
|
||||||
|
|
||||||
|
if (vueMetaAttrs.indexOf(attr) === -1) {
|
||||||
|
vueMetaAttrs.push(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepIndex = toRemove.indexOf(attr)
|
||||||
|
if (keepIndex !== -1) {
|
||||||
|
toRemove.splice(keepIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemove.forEach(attr => tag.removeAttribute(attr))
|
||||||
|
|
||||||
|
if (vueMetaAttrs.length === toRemove.length) {
|
||||||
|
tag.removeAttribute(attribute)
|
||||||
|
} else {
|
||||||
|
tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(','))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as updateAttribute } from './attribute'
|
||||||
|
export { default as updateTitle } from './title'
|
||||||
|
export { default as updateTag } from './tag'
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// borrow the slice method
|
||||||
|
const toArray = Function.prototype.call.bind(Array.prototype.slice)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
||||||
|
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
||||||
|
*
|
||||||
|
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
||||||
|
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||||
|
* @return {Object} - a representation of what tags changed
|
||||||
|
*/
|
||||||
|
export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) {
|
||||||
|
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
|
||||||
|
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
|
||||||
|
const newTags = []
|
||||||
|
|
||||||
|
if (tags.length > 1) {
|
||||||
|
// remove duplicates that could have been found by merging tags
|
||||||
|
// which include a mixin with metaInfo and that mixin is used
|
||||||
|
// by multiple components on the same page
|
||||||
|
const found = []
|
||||||
|
tags = tags.filter((x) => {
|
||||||
|
const k = JSON.stringify(x)
|
||||||
|
const res = !found.includes(k)
|
||||||
|
found.push(k)
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags && tags.length) {
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
const newElement = document.createElement(type)
|
||||||
|
newElement.setAttribute(attribute, 'true')
|
||||||
|
|
||||||
|
const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags
|
||||||
|
|
||||||
|
for (const attr in tag) {
|
||||||
|
if (tag.hasOwnProperty(attr)) {
|
||||||
|
if (attr === 'innerHTML') {
|
||||||
|
newElement.innerHTML = tag.innerHTML
|
||||||
|
} else if (attr === 'cssText') {
|
||||||
|
if (newElement.styleSheet) {
|
||||||
|
newElement.styleSheet.cssText = tag.cssText
|
||||||
|
} else {
|
||||||
|
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||||
|
}
|
||||||
|
} else if ([tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
||||||
|
const _attr = `data-${attr}`
|
||||||
|
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||||
|
newElement.setAttribute(_attr, value)
|
||||||
|
} else {
|
||||||
|
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||||
|
newElement.setAttribute(attr, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
||||||
|
let indexToDelete
|
||||||
|
const hasEqualElement = oldTags.some((existingTag, index) => {
|
||||||
|
indexToDelete = index
|
||||||
|
return newElement.isEqualNode(existingTag)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasEqualElement && (indexToDelete || indexToDelete === 0)) {
|
||||||
|
oldTags.splice(indexToDelete, 1)
|
||||||
|
} else {
|
||||||
|
newTags.push(newElement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTags = oldHeadTags.concat(oldBodyTags)
|
||||||
|
oldTags.forEach(tag => tag.parentNode.removeChild(tag))
|
||||||
|
newTags.forEach((tag) => {
|
||||||
|
if (tag.getAttribute('data-body') === 'true') {
|
||||||
|
bodyTag.appendChild(tag)
|
||||||
|
} else {
|
||||||
|
headTag.appendChild(tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { oldTags, newTags }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Updates the document title
|
||||||
|
*
|
||||||
|
* @param {String} title - the new title of the document
|
||||||
|
*/
|
||||||
|
export default function updateTitle(title = document.title) {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export default function _updateTagAttributes (options = {}) {
|
|
||||||
const { attribute } = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the document's html tag attributes
|
|
||||||
*
|
|
||||||
* @param {Object} attrs - the new document html attributes
|
|
||||||
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
|
||||||
*/
|
|
||||||
return function updateTagAttributes (attrs, tag) {
|
|
||||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
|
||||||
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
|
||||||
const toRemove = [].concat(vueMetaAttrs)
|
|
||||||
for (let attr in attrs) {
|
|
||||||
if (attrs.hasOwnProperty(attr)) {
|
|
||||||
const val = attrs[attr] || ''
|
|
||||||
tag.setAttribute(attr, val)
|
|
||||||
if (vueMetaAttrs.indexOf(attr) === -1) {
|
|
||||||
vueMetaAttrs.push(attr)
|
|
||||||
}
|
|
||||||
const saveIndex = toRemove.indexOf(attr)
|
|
||||||
if (saveIndex !== -1) {
|
|
||||||
toRemove.splice(saveIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let i = toRemove.length - 1
|
|
||||||
for (; i >= 0; i--) {
|
|
||||||
tag.removeAttribute(toRemove[i])
|
|
||||||
}
|
|
||||||
if (vueMetaAttrs.length === toRemove.length) {
|
|
||||||
tag.removeAttribute(attribute)
|
|
||||||
} else {
|
|
||||||
tag.setAttribute(attribute, vueMetaAttrs.join(','))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// borrow the slice method
|
|
||||||
const toArray = Function.prototype.call.bind(Array.prototype.slice)
|
|
||||||
|
|
||||||
export default function _updateTags (options = {}) {
|
|
||||||
const { attribute } = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
|
||||||
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
|
||||||
*
|
|
||||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
|
||||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
|
||||||
* @return {Object} - a representation of what tags changed
|
|
||||||
*/
|
|
||||||
return function updateTags (type, tags, headTag, bodyTag) {
|
|
||||||
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
|
|
||||||
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
|
|
||||||
const newTags = []
|
|
||||||
let indexToDelete
|
|
||||||
|
|
||||||
if (tags.length > 1) {
|
|
||||||
// remove duplicates that could have been found by merging tags
|
|
||||||
// which include a mixin with metaInfo and that mixin is used
|
|
||||||
// by multiple components on the same page
|
|
||||||
const found = []
|
|
||||||
tags = tags.map(x => {
|
|
||||||
const k = JSON.stringify(x)
|
|
||||||
if (found.indexOf(k) < 0) {
|
|
||||||
found.push(k)
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
}).filter(x => x)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags && tags.length) {
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
const newElement = document.createElement(type)
|
|
||||||
const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags
|
|
||||||
|
|
||||||
for (const attr in tag) {
|
|
||||||
if (tag.hasOwnProperty(attr)) {
|
|
||||||
if (attr === 'innerHTML') {
|
|
||||||
newElement.innerHTML = tag.innerHTML
|
|
||||||
} else if (attr === 'cssText') {
|
|
||||||
if (newElement.styleSheet) {
|
|
||||||
newElement.styleSheet.cssText = tag.cssText
|
|
||||||
} else {
|
|
||||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
|
||||||
}
|
|
||||||
} else if ([options.tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
|
||||||
const _attr = `data-${attr}`
|
|
||||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
|
||||||
newElement.setAttribute(_attr, value)
|
|
||||||
} else {
|
|
||||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
|
||||||
newElement.setAttribute(attr, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newElement.setAttribute(attribute, 'true')
|
|
||||||
|
|
||||||
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
|
||||||
if (oldTags.some((existingTag, index) => {
|
|
||||||
indexToDelete = index
|
|
||||||
return newElement.isEqualNode(existingTag)
|
|
||||||
})) {
|
|
||||||
oldTags.splice(indexToDelete, 1)
|
|
||||||
} else {
|
|
||||||
newTags.push(newElement)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const oldTags = oldHeadTags.concat(oldBodyTags)
|
|
||||||
oldTags.forEach((tag) => tag.parentNode.removeChild(tag))
|
|
||||||
newTags.forEach((tag) => {
|
|
||||||
if (tag.getAttribute('data-body') === 'true') {
|
|
||||||
bodyTag.appendChild(tag)
|
|
||||||
} else {
|
|
||||||
headTag.appendChild(tag)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { oldTags, newTags }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export default function _updateTitle () {
|
|
||||||
/**
|
|
||||||
* Updates the document title
|
|
||||||
*
|
|
||||||
* @param {String} title - the new title of the document
|
|
||||||
*/
|
|
||||||
return function updateTitle (title = document.title) {
|
|
||||||
document.title = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import install from './shared/plugin'
|
|
||||||
import { version } from '../package.json'
|
import { version } from '../package.json'
|
||||||
|
import install from './shared/plugin'
|
||||||
|
|
||||||
install.version = version
|
install.version = version
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import titleGenerator from './generators/titleGenerator'
|
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
|
||||||
import attrsGenerator from './generators/attrsGenerator'
|
|
||||||
import tagGenerator from './generators/tagGenerator'
|
|
||||||
|
|
||||||
export default function _generateServerInjector (options = {}) {
|
/**
|
||||||
/**
|
* Converts a meta info property to one that can be stringified on the server
|
||||||
* Converts a meta info property to one that can be stringified on the server
|
*
|
||||||
*
|
* @param {String} type - the type of data to convert
|
||||||
* @param {String} type - the type of data to convert
|
* @param {(String|Object|Array<Object>)} data - the data value
|
||||||
* @param {(String|Object|Array<Object>)} data - the data value
|
* @return {Object} - the new injector
|
||||||
* @return {Object} - the new injector
|
*/
|
||||||
*/
|
|
||||||
return function generateServerInjector (type, data) {
|
export default function generateServerInjector(options, type, data) {
|
||||||
switch (type) {
|
if (type === 'title') {
|
||||||
case 'title':
|
return titleGenerator(options, type, data)
|
||||||
return titleGenerator(options)(type, data)
|
|
||||||
case 'htmlAttrs':
|
|
||||||
case 'bodyAttrs':
|
|
||||||
case 'headAttrs':
|
|
||||||
return attrsGenerator(options)(type, data)
|
|
||||||
default:
|
|
||||||
return tagGenerator(options)(type, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||||
|
return attributeGenerator(options, type, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagGenerator(options, type, data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Generates tag attributes for use on the server.
|
||||||
|
*
|
||||||
|
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
||||||
|
* @param {Object} data - the attributes to generate
|
||||||
|
* @return {Object} - the attribute generator
|
||||||
|
*/
|
||||||
|
export default function attributeGenerator({ attribute } = {}, type, data) {
|
||||||
|
return {
|
||||||
|
text() {
|
||||||
|
let attributeStr = ''
|
||||||
|
const watchedAttrs = []
|
||||||
|
|
||||||
|
for (const attr in data) {
|
||||||
|
if (data.hasOwnProperty(attr)) {
|
||||||
|
watchedAttrs.push(attr)
|
||||||
|
|
||||||
|
attributeStr += typeof data[attr] !== 'undefined'
|
||||||
|
? `${attr}="${data[attr]}"`
|
||||||
|
: attr
|
||||||
|
|
||||||
|
attributeStr += ' '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
|
||||||
|
return attributeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export default function _attrsGenerator (options = {}) {
|
|
||||||
const { attribute } = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates tag attributes for use on the server.
|
|
||||||
*
|
|
||||||
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
|
||||||
* @param {Object} data - the attributes to generate
|
|
||||||
* @return {Object} - the attribute generator
|
|
||||||
*/
|
|
||||||
return function attrsGenerator (type, data) {
|
|
||||||
return {
|
|
||||||
text () {
|
|
||||||
let attributeStr = ''
|
|
||||||
let watchedAttrs = []
|
|
||||||
for (let attr in data) {
|
|
||||||
if (data.hasOwnProperty(attr)) {
|
|
||||||
watchedAttrs.push(attr)
|
|
||||||
attributeStr += `${
|
|
||||||
typeof data[attr] !== 'undefined'
|
|
||||||
? `${attr}="${data[attr]}"`
|
|
||||||
: attr
|
|
||||||
} `
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attributeStr += `${attribute}="${watchedAttrs.join(',')}"`
|
|
||||||
return attributeStr.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as attributeGenerator } from './attribute'
|
||||||
|
export { default as titleGenerator } from './title'
|
||||||
|
export { default as tagGenerator } from './tag'
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Generates meta, base, link, style, script, noscript tags for use on the server
|
||||||
|
*
|
||||||
|
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
|
||||||
|
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||||
|
* @return {Object} - the tag generator
|
||||||
|
*/
|
||||||
|
export default function tagGenerator({ attribute, tagIDKeyName } = {}, type, tags) {
|
||||||
|
return {
|
||||||
|
text({ body = false } = {}) {
|
||||||
|
// build a string containing all tags of this type
|
||||||
|
return tags.reduce((tagsStr, tag) => {
|
||||||
|
if (Object.keys(tag).length === 0) {
|
||||||
|
return tagsStr // Bail on empty tag object
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean(tag.body) !== body) {
|
||||||
|
return tagsStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// build a string containing all attributes of this tag
|
||||||
|
const attrs = Object.keys(tag).reduce((attrsStr, attr) => {
|
||||||
|
// these attributes are treated as children on the tag
|
||||||
|
if (['innerHTML', 'cssText', 'once'].includes(attr)) {
|
||||||
|
return attrsStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// these form the attribute list for this tag
|
||||||
|
let prefix = ''
|
||||||
|
if ([tagIDKeyName, 'body'].includes(attr)) {
|
||||||
|
prefix = 'data-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof tag[attr] === 'undefined'
|
||||||
|
? `${attrsStr} ${prefix}${attr}`
|
||||||
|
: `${attrsStr} ${prefix}${attr}="${tag[attr]}"`
|
||||||
|
}, '')
|
||||||
|
|
||||||
|
// grab child content from one of these attributes, if possible
|
||||||
|
const content = tag.innerHTML || tag.cssText || ''
|
||||||
|
|
||||||
|
// generate tag exactly without any other redundant attribute
|
||||||
|
const observeTag = tag.once
|
||||||
|
? ''
|
||||||
|
: `${attribute}="true"`
|
||||||
|
|
||||||
|
// these tags have no end tag
|
||||||
|
const hasEndTag = !['base', 'meta', 'link'].includes(type)
|
||||||
|
|
||||||
|
// these tag types will have content inserted
|
||||||
|
const hasContent = hasEndTag && ['noscript', 'script', 'style'].includes(type)
|
||||||
|
|
||||||
|
// the final string for this specific tag
|
||||||
|
return !hasContent
|
||||||
|
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
|
||||||
|
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
export default function _tagGenerator (options = {}) {
|
|
||||||
const { attribute } = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates meta, base, link, style, script, noscript tags for use on the server
|
|
||||||
*
|
|
||||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
|
|
||||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
|
||||||
* @return {Object} - the tag generator
|
|
||||||
*/
|
|
||||||
return function tagGenerator (type, tags) {
|
|
||||||
return {
|
|
||||||
text ({ body = false } = {}) {
|
|
||||||
// build a string containing all tags of this type
|
|
||||||
return tags.reduce((tagsStr, tag) => {
|
|
||||||
if (Object.keys(tag).length === 0) return tagsStr // Bail on empty tag object
|
|
||||||
if (!!tag.body !== body) return tagsStr
|
|
||||||
// build a string containing all attributes of this tag
|
|
||||||
const attrs = Object.keys(tag).reduce((attrsStr, attr) => {
|
|
||||||
switch (attr) {
|
|
||||||
// these attributes are treated as children on the tag
|
|
||||||
case 'innerHTML':
|
|
||||||
case 'cssText':
|
|
||||||
case 'once':
|
|
||||||
return attrsStr
|
|
||||||
// these form the attribute list for this tag
|
|
||||||
default:
|
|
||||||
if ([options.tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
|
||||||
return `${attrsStr} data-${attr}="${tag[attr]}"`
|
|
||||||
}
|
|
||||||
return typeof tag[attr] === 'undefined'
|
|
||||||
? `${attrsStr} ${attr}`
|
|
||||||
: `${attrsStr} ${attr}="${tag[attr]}"`
|
|
||||||
}
|
|
||||||
}, '').trim()
|
|
||||||
|
|
||||||
// grab child content from one of these attributes, if possible
|
|
||||||
const content = tag.innerHTML || tag.cssText || ''
|
|
||||||
|
|
||||||
// generate tag exactly without any other redundant attribute
|
|
||||||
const observeTag = tag.once
|
|
||||||
? ''
|
|
||||||
: `${attribute}="true" `
|
|
||||||
|
|
||||||
// these tags have no end tag
|
|
||||||
const hasEndTag = !['base', 'meta', 'link'].includes(type)
|
|
||||||
|
|
||||||
// these tag types will have content inserted
|
|
||||||
const hasContent = hasEndTag && ['noscript', 'script', 'style'].includes(type)
|
|
||||||
|
|
||||||
// the final string for this specific tag
|
|
||||||
return !hasContent
|
|
||||||
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
|
|
||||||
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
|
|
||||||
}, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Generates title output for the server
|
||||||
|
*
|
||||||
|
* @param {'title'} type - the string "title"
|
||||||
|
* @param {String} data - the title text
|
||||||
|
* @return {Object} - the title generator
|
||||||
|
*/
|
||||||
|
export default function titleGenerator({ attribute } = {}, type, data) {
|
||||||
|
return {
|
||||||
|
text() {
|
||||||
|
return `<${type} ${attribute}="true">${data}</${type}>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
export default function _titleGenerator (options = {}) {
|
|
||||||
const { attribute } = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates title output for the server
|
|
||||||
*
|
|
||||||
* @param {'title'} type - the string "title"
|
|
||||||
* @param {String} data - the title text
|
|
||||||
* @return {Object} - the title generator
|
|
||||||
*/
|
|
||||||
return function titleGenerator (type, data) {
|
|
||||||
return {
|
|
||||||
text () {
|
|
||||||
return `<${type} ${attribute}="true">${data}</${type}>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import getMetaInfo from '../shared/getMetaInfo'
|
import getMetaInfo from '../shared/getMetaInfo'
|
||||||
import generateServerInjector from './generateServerInjector'
|
import generateServerInjector from './generateServerInjector'
|
||||||
|
|
||||||
export default function _inject (options = {}) {
|
export default function _inject(options = {}) {
|
||||||
/**
|
/**
|
||||||
* Converts the state of the meta info object such that each item
|
* Converts the state of the meta info object such that each item
|
||||||
* can be compiled to a tag string on the server
|
* can be compiled to a tag string on the server
|
||||||
@@ -9,17 +9,18 @@ export default function _inject (options = {}) {
|
|||||||
* @this {Object} - Vue instance - ideally the root component
|
* @this {Object} - Vue instance - ideally the root component
|
||||||
* @return {Object} - server meta info with `toString` methods
|
* @return {Object} - server meta info with `toString` methods
|
||||||
*/
|
*/
|
||||||
return function inject () {
|
|
||||||
|
return function inject() {
|
||||||
// get meta info with sensible defaults
|
// get meta info with sensible defaults
|
||||||
const info = getMetaInfo(options)(this.$root)
|
const metaInfo = getMetaInfo(options, this.$root)
|
||||||
|
|
||||||
// generate server injectors
|
// generate server injectors
|
||||||
for (let key in info) {
|
for (const key in metaInfo) {
|
||||||
if (info.hasOwnProperty(key) && key !== 'titleTemplate' && key !== 'titleChunk') {
|
if (!['titleTemplate', 'titleChunk'].includes(key) && metaInfo.hasOwnProperty(key)) {
|
||||||
info[key] = generateServerInjector(options)(key, info[key])
|
metaInfo[key] = generateServerInjector(options, key, metaInfo[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return metaInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
import inject from '../server/inject'
|
import inject from '../server/inject'
|
||||||
import refresh from '../client/refresh'
|
import refresh from '../client/refresh'
|
||||||
|
|
||||||
export default function _$meta (options = {}) {
|
export default function _$meta(options = {}) {
|
||||||
/**
|
/**
|
||||||
* Returns an injector for server-side rendering.
|
* Returns an injector for server-side rendering.
|
||||||
* @this {Object} - the Vue instance (a root component)
|
* @this {Object} - the Vue instance (a root component)
|
||||||
* @return {Object} - injector
|
* @return {Object} - injector
|
||||||
*/
|
*/
|
||||||
return function $meta () {
|
return function $meta() {
|
||||||
return {
|
return {
|
||||||
inject: inject(options).bind(this),
|
inject: inject(options).bind(this),
|
||||||
refresh: refresh(options).bind(this)
|
refresh: refresh(options).bind(this)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import deepmerge from 'deepmerge'
|
import deepmerge from 'deepmerge'
|
||||||
import uniqBy from './uniqBy'
|
|
||||||
import uniqueId from 'lodash.uniqueid'
|
import uniqueId from 'lodash.uniqueid'
|
||||||
|
import uniqBy from './uniqBy'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the `opts.option` $option value of the given `opts.component`.
|
* Returns the `opts.option` $option value of the given `opts.component`.
|
||||||
@@ -10,21 +10,22 @@ import uniqueId from 'lodash.uniqueid'
|
|||||||
*
|
*
|
||||||
* @param {Object} opts - options
|
* @param {Object} opts - options
|
||||||
* @param {Object} opts.component - Vue component to fetch option data from
|
* @param {Object} opts.component - Vue component to fetch option data from
|
||||||
* @param {String} opts.option - what option to look for
|
|
||||||
* @param {Boolean} opts.deep - look for data in child components as well?
|
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||||
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
||||||
|
* @param {String} opts.keyName - the name of the option to look for
|
||||||
* @param {Object} [result={}] - result so far
|
* @param {Object} [result={}] - result so far
|
||||||
* @return {Object} result - final aggregated result
|
* @return {Object} result - final aggregated result
|
||||||
*/
|
*/
|
||||||
export default function getComponentOption (opts, result = {}) {
|
export default function getComponentOption({ component, deep, arrayMerge, keyName, metaTemplateKeyName, tagIDKeyName, contentKeyName } = {}, result = {}) {
|
||||||
const { component, option, deep, arrayMerge, metaTemplateKeyName, tagIDKeyName, contentKeyName } = opts
|
|
||||||
const { $options } = component
|
const { $options } = component
|
||||||
|
|
||||||
if (component._inactive) return result
|
if (component._inactive) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// only collect option data if it exists
|
// only collect option data if it exists
|
||||||
if (typeof $options[option] !== 'undefined' && $options[option] !== null) {
|
if (typeof $options[keyName] !== 'undefined' && $options[keyName] !== null) {
|
||||||
let data = $options[option]
|
let data = $options[keyName]
|
||||||
|
|
||||||
// if option is a function, replace it with it's result
|
// if option is a function, replace it with it's result
|
||||||
if (typeof data === 'function') {
|
if (typeof data === 'function') {
|
||||||
@@ -44,14 +45,15 @@ export default function getComponentOption (opts, result = {}) {
|
|||||||
component.$children.forEach((childComponent) => {
|
component.$children.forEach((childComponent) => {
|
||||||
result = getComponentOption({
|
result = getComponentOption({
|
||||||
component: childComponent,
|
component: childComponent,
|
||||||
option,
|
keyName,
|
||||||
deep,
|
deep,
|
||||||
arrayMerge
|
arrayMerge
|
||||||
}, result)
|
}, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metaTemplateKeyName && result.hasOwnProperty('meta')) {
|
if (metaTemplateKeyName && result.hasOwnProperty('meta')) {
|
||||||
result.meta = Object.keys(result.meta).map(metaKey => {
|
result.meta = Object.keys(result.meta).map((metaKey) => {
|
||||||
const metaObject = result.meta[metaKey]
|
const metaObject = result.meta[metaKey]
|
||||||
if (!metaObject.hasOwnProperty(metaTemplateKeyName) || !metaObject.hasOwnProperty(contentKeyName) || typeof metaObject[metaTemplateKeyName] === 'undefined') {
|
if (!metaObject.hasOwnProperty(metaTemplateKeyName) || !metaObject.hasOwnProperty(contentKeyName) || typeof metaObject[metaTemplateKeyName] === 'undefined') {
|
||||||
return result.meta[metaKey]
|
return result.meta[metaKey]
|
||||||
|
|||||||
+128
-121
@@ -3,7 +3,7 @@ import isPlainObject from 'lodash.isplainobject'
|
|||||||
import isArray from './isArray'
|
import isArray from './isArray'
|
||||||
import getComponentOption from './getComponentOption'
|
import getComponentOption from './getComponentOption'
|
||||||
|
|
||||||
const escapeHTML = (str) => typeof window === 'undefined'
|
const escapeHTML = str => typeof window === 'undefined'
|
||||||
// server-side escape sequence
|
// server-side escape sequence
|
||||||
? String(str)
|
? String(str)
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -19,138 +19,145 @@ const escapeHTML = (str) => typeof window === 'undefined'
|
|||||||
.replace(/"/g, '\u0022')
|
.replace(/"/g, '\u0022')
|
||||||
.replace(/'/g, '\u0027')
|
.replace(/'/g, '\u0027')
|
||||||
|
|
||||||
export default function _getMetaInfo (options = {}) {
|
const applyTemplate = (component, template, chunk) =>
|
||||||
const { keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = options
|
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
||||||
/**
|
|
||||||
* Returns the correct meta info for the given component
|
|
||||||
* (child components will overwrite parent meta info)
|
|
||||||
*
|
|
||||||
* @param {Object} component - the Vue instance to get meta info from
|
|
||||||
* @return {Object} - returned meta info
|
|
||||||
*/
|
|
||||||
return function getMetaInfo (component) {
|
|
||||||
// set some sane defaults
|
|
||||||
const defaultInfo = {
|
|
||||||
title: '',
|
|
||||||
titleChunk: '',
|
|
||||||
titleTemplate: '%s',
|
|
||||||
htmlAttrs: {},
|
|
||||||
bodyAttrs: {},
|
|
||||||
headAttrs: {},
|
|
||||||
meta: [],
|
|
||||||
base: [],
|
|
||||||
link: [],
|
|
||||||
style: [],
|
|
||||||
script: [],
|
|
||||||
noscript: [],
|
|
||||||
__dangerouslyDisableSanitizers: [],
|
|
||||||
__dangerouslyDisableSanitizersByTagID: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect & aggregate all metaInfo $options
|
/**
|
||||||
let info = getComponentOption({
|
* Returns the correct meta info for the given component
|
||||||
component,
|
* (child components will overwrite parent meta info)
|
||||||
option: keyName,
|
*
|
||||||
deep: true,
|
* @param {Object} component - the Vue instance to get meta info from
|
||||||
metaTemplateKeyName,
|
* @return {Object} - returned meta info
|
||||||
tagIDKeyName,
|
*/
|
||||||
contentKeyName,
|
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, component) {
|
||||||
arrayMerge (target, source) {
|
// set some sane defaults
|
||||||
// we concat the arrays without merging objects contained in,
|
const defaultInfo = {
|
||||||
// but we check for a `vmid` property on each object in the array
|
title: '',
|
||||||
// using an O(1) lookup associative array exploit
|
titleChunk: '',
|
||||||
// note the use of "for in" - we are looping through arrays here, not
|
titleTemplate: '%s',
|
||||||
// plain objects
|
htmlAttrs: {},
|
||||||
const destination = []
|
bodyAttrs: {},
|
||||||
for (let targetIndex in target) {
|
headAttrs: {},
|
||||||
const targetItem = target[targetIndex]
|
meta: [],
|
||||||
let shared = false
|
base: [],
|
||||||
for (let sourceIndex in source) {
|
link: [],
|
||||||
const sourceItem = source[sourceIndex]
|
style: [],
|
||||||
if (targetItem[tagIDKeyName] && targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
|
script: [],
|
||||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
noscript: [],
|
||||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
__dangerouslyDisableSanitizers: [],
|
||||||
if (targetTemplate && !sourceTemplate) {
|
__dangerouslyDisableSanitizersByTagID: {}
|
||||||
sourceItem[contentKeyName] = applyTemplate(component)(targetTemplate)(sourceItem[contentKeyName])
|
}
|
||||||
}
|
|
||||||
// If template defined in child but content in parent
|
// collect & aggregate all metaInfo $options
|
||||||
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
let info = getComponentOption({
|
||||||
sourceItem[contentKeyName] = applyTemplate(component)(sourceTemplate)(targetItem[contentKeyName])
|
deep: true,
|
||||||
delete sourceItem[metaTemplateKeyName]
|
component,
|
||||||
}
|
keyName,
|
||||||
shared = true
|
metaTemplateKeyName,
|
||||||
break
|
tagIDKeyName,
|
||||||
|
contentKeyName,
|
||||||
|
arrayMerge(target, source) {
|
||||||
|
// we concat the arrays without merging objects contained in,
|
||||||
|
// but we check for a `vmid` property on each object in the array
|
||||||
|
// using an O(1) lookup associative array exploit
|
||||||
|
// note the use of "for in" - we are looping through arrays here, not
|
||||||
|
// plain objects
|
||||||
|
const destination = []
|
||||||
|
|
||||||
|
for (const targetIndex in target) {
|
||||||
|
const targetItem = target[targetIndex]
|
||||||
|
let shared = false
|
||||||
|
|
||||||
|
for (const sourceIndex in source) {
|
||||||
|
const sourceItem = source[sourceIndex]
|
||||||
|
|
||||||
|
if (targetItem[tagIDKeyName] && targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
|
||||||
|
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||||
|
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||||
|
|
||||||
|
if (targetTemplate && !sourceTemplate) {
|
||||||
|
sourceItem[contentKeyName] = applyTemplate(component, targetTemplate, sourceItem[contentKeyName])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!shared) {
|
// If template defined in child but content in parent
|
||||||
destination.push(targetItem)
|
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
||||||
|
sourceItem[contentKeyName] = applyTemplate(component, sourceTemplate, targetItem[contentKeyName])
|
||||||
|
delete sourceItem[metaTemplateKeyName]
|
||||||
|
}
|
||||||
|
|
||||||
|
shared = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return destination.concat(source)
|
if (!shared) {
|
||||||
}
|
destination.push(targetItem)
|
||||||
})
|
|
||||||
|
|
||||||
// Remove all "template" tags from meta
|
|
||||||
|
|
||||||
// backup the title chunk in case user wants access to it
|
|
||||||
if (info.title) {
|
|
||||||
info.titleChunk = info.title
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace title with populated template
|
|
||||||
if (info.titleTemplate) {
|
|
||||||
info.title = applyTemplate(component)(info.titleTemplate)(info.titleChunk || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert base tag to an array so it can be handled the same way
|
|
||||||
// as the other tags
|
|
||||||
if (info.base) {
|
|
||||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const ref = info.__dangerouslyDisableSanitizers
|
|
||||||
const refByTagID = info.__dangerouslyDisableSanitizersByTagID
|
|
||||||
|
|
||||||
// sanitizes potentially dangerous characters
|
|
||||||
const escape = (info) => Object.keys(info).reduce((escaped, key) => {
|
|
||||||
let isDisabled = ref && ref.indexOf(key) > -1
|
|
||||||
const tagID = info[tagIDKeyName]
|
|
||||||
if (!isDisabled && tagID) {
|
|
||||||
isDisabled = refByTagID && refByTagID[tagID] && refByTagID[tagID].indexOf(key) > -1
|
|
||||||
}
|
|
||||||
const val = info[key]
|
|
||||||
escaped[key] = val
|
|
||||||
if (key === '__dangerouslyDisableSanitizers' || key === '__dangerouslyDisableSanitizersByTagID') {
|
|
||||||
return escaped
|
|
||||||
}
|
|
||||||
if (!isDisabled) {
|
|
||||||
if (typeof val === 'string') {
|
|
||||||
escaped[key] = escapeHTML(val)
|
|
||||||
} else if (isPlainObject(val)) {
|
|
||||||
escaped[key] = escape(val)
|
|
||||||
} else if (isArray(val)) {
|
|
||||||
escaped[key] = val.map(escape)
|
|
||||||
} else {
|
|
||||||
escaped[key] = val
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination.concat(source)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove all "template" tags from meta
|
||||||
|
|
||||||
|
// backup the title chunk in case user wants access to it
|
||||||
|
if (info.title) {
|
||||||
|
info.titleChunk = info.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace title with populated template
|
||||||
|
if (info.titleTemplate) {
|
||||||
|
info.title = applyTemplate(component, info.titleTemplate, info.titleChunk || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert base tag to an array so it can be handled the same way
|
||||||
|
// as the other tags
|
||||||
|
if (info.base) {
|
||||||
|
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const ref = info.__dangerouslyDisableSanitizers
|
||||||
|
const refByTagID = info.__dangerouslyDisableSanitizersByTagID
|
||||||
|
|
||||||
|
// sanitizes potentially dangerous characters
|
||||||
|
const escape = info => Object.keys(info).reduce((escaped, key) => {
|
||||||
|
let isDisabled = ref && ref.indexOf(key) > -1
|
||||||
|
const tagID = info[tagIDKeyName]
|
||||||
|
|
||||||
|
if (!isDisabled && tagID) {
|
||||||
|
isDisabled = refByTagID && refByTagID[tagID] && refByTagID[tagID].indexOf(key) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = info[key]
|
||||||
|
escaped[key] = val
|
||||||
|
|
||||||
|
if (key === '__dangerouslyDisableSanitizers' || key === '__dangerouslyDisableSanitizersByTagID') {
|
||||||
|
return escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDisabled) {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
escaped[key] = escapeHTML(val)
|
||||||
|
} else if (isPlainObject(val)) {
|
||||||
|
escaped[key] = escape(val)
|
||||||
|
} else if (isArray(val)) {
|
||||||
|
escaped[key] = val.map(escape)
|
||||||
} else {
|
} else {
|
||||||
escaped[key] = val
|
escaped[key] = val
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
escaped[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
return escaped
|
return escaped
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
// merge with defaults
|
// merge with defaults
|
||||||
info = deepmerge(defaultInfo, info)
|
info = deepmerge(defaultInfo, info)
|
||||||
|
|
||||||
// begin sanitization
|
// begin sanitization
|
||||||
info = escape(info)
|
info = escape(info)
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyTemplate = component => template => chunk =>
|
|
||||||
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @param {any} arr - the object to check
|
* @param {any} arr - the object to check
|
||||||
* @return {Boolean} - true if `arr` is an array
|
* @return {Boolean} - true if `arr` is an array
|
||||||
*/
|
*/
|
||||||
export default function isArray (arr) {
|
export default function isArray(arr) {
|
||||||
return Array.isArray
|
return Array.isArray
|
||||||
? Array.isArray(arr)
|
? Array.isArray(arr)
|
||||||
: Object.prototype.toString.call(arr) === '[object Array]'
|
: Object.prototype.toString.call(arr) === '[object Array]'
|
||||||
|
|||||||
+89
-34
@@ -1,6 +1,5 @@
|
|||||||
import assign from 'object-assign'
|
|
||||||
import $meta from './$meta'
|
|
||||||
import batchUpdate from '../client/batchUpdate'
|
import batchUpdate from '../client/batchUpdate'
|
||||||
|
import $meta from './$meta'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VUE_META_KEY_NAME,
|
VUE_META_KEY_NAME,
|
||||||
@@ -19,7 +18,7 @@ if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') {
|
|||||||
* Plugin install function.
|
* Plugin install function.
|
||||||
* @param {Function} Vue - the Vue constructor.
|
* @param {Function} Vue - the Vue constructor.
|
||||||
*/
|
*/
|
||||||
export default function VueMeta (Vue, options = {}) {
|
export default function VueMeta(Vue, options = {}) {
|
||||||
// set some default options
|
// set some default options
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
keyName: VUE_META_KEY_NAME,
|
keyName: VUE_META_KEY_NAME,
|
||||||
@@ -29,8 +28,15 @@ export default function VueMeta (Vue, options = {}) {
|
|||||||
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||||
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
// combine options
|
// combine options
|
||||||
options = assign(defaultOptions, options)
|
options = typeof options === 'object' ? options : {}
|
||||||
|
|
||||||
|
for (const key in defaultOptions) {
|
||||||
|
if (!options[key]) {
|
||||||
|
options[key] = defaultOptions[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// bind the $meta method to this component instance
|
// bind the $meta method to this component instance
|
||||||
Vue.prototype.$meta = $meta(options)
|
Vue.prototype.$meta = $meta(options)
|
||||||
@@ -38,66 +44,115 @@ export default function VueMeta (Vue, options = {}) {
|
|||||||
// store an id to keep track of DOM updates
|
// store an id to keep track of DOM updates
|
||||||
let batchID = null
|
let batchID = null
|
||||||
|
|
||||||
|
const triggerUpdate = (vm) => {
|
||||||
|
// batch potential DOM updates to prevent extraneous re-rendering
|
||||||
|
batchID = batchUpdate(batchID, () => vm.$meta().refresh())
|
||||||
|
}
|
||||||
|
|
||||||
// watch for client side component updates
|
// watch for client side component updates
|
||||||
Vue.mixin({
|
Vue.mixin({
|
||||||
beforeCreate () {
|
beforeCreate() {
|
||||||
// Add a marker to know if it uses metaInfo
|
// Add a marker to know if it uses metaInfo
|
||||||
// _vnode is used to know that it's attached to a real component
|
// _vnode is used to know that it's attached to a real component
|
||||||
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
||||||
if (typeof this.$options[options.keyName] !== 'undefined') {
|
if (typeof this.$options[options.keyName] !== 'undefined' && this.$options[options.keyName] !== null) {
|
||||||
this._hasMetaInfo = true
|
this._hasMetaInfo = true
|
||||||
}
|
|
||||||
// coerce function-style metaInfo to a computed prop so we can observe
|
// coerce function-style metaInfo to a computed prop so we can observe
|
||||||
// it on creation
|
// it on creation
|
||||||
if (typeof this.$options[options.keyName] === 'function') {
|
if (typeof this.$options[options.keyName] === 'function') {
|
||||||
if (typeof this.$options.computed === 'undefined') {
|
if (typeof this.$options.computed === 'undefined') {
|
||||||
this.$options.computed = {}
|
this.$options.computed = {}
|
||||||
|
}
|
||||||
|
this.$options.computed.$metaInfo = this.$options[options.keyName]
|
||||||
|
|
||||||
|
if (!this.$isServer) {
|
||||||
|
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||||
|
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||||
|
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||||
|
this.$options.created = this.$options.created || []
|
||||||
|
this.$options.created.push(() => {
|
||||||
|
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
['activated', 'deactivated', 'beforeMount'].forEach((lifecycleHook) => {
|
||||||
|
this.$options[lifecycleHook] = this.$options[lifecycleHook] || []
|
||||||
|
this.$options[lifecycleHook].push(() => triggerUpdate(this))
|
||||||
|
})
|
||||||
|
|
||||||
|
// do not trigger refresh on the server side
|
||||||
|
if (!this.$isServer) {
|
||||||
|
// re-render meta data when returning from a child component to parent
|
||||||
|
this.$options.destroyed = this.$options.destroyed || []
|
||||||
|
this.$options.destroyed.push(() => {
|
||||||
|
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (this.$el && this.$el.offsetParent !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(interval)
|
||||||
|
|
||||||
|
if (!this.$parent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerUpdate(this)
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.$options.computed.$metaInfo = this.$options[options.keyName]
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
created () {
|
/* Not yet removed
|
||||||
|
created() {
|
||||||
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||||
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||||
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||||
if (!this.$isServer && this.$metaInfo) {
|
if (!this.$isServer && this.$metaInfo) {
|
||||||
this.$watch('$metaInfo', () => {
|
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||||
// batch potential DOM updates to prevent extraneous re-rendering
|
|
||||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
activated () {
|
activated() {
|
||||||
if (this._hasMetaInfo) {
|
if (this._hasMetaInfo) {
|
||||||
// batch potential DOM updates to prevent extraneous re-rendering
|
triggerUpdate(this)
|
||||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deactivated () {
|
deactivated() {
|
||||||
if (this._hasMetaInfo) {
|
if (this._hasMetaInfo) {
|
||||||
// batch potential DOM updates to prevent extraneous re-rendering
|
triggerUpdate(this)
|
||||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount () {
|
beforeMount() {
|
||||||
// batch potential DOM updates to prevent extraneous re-rendering
|
|
||||||
if (this._hasMetaInfo) {
|
if (this._hasMetaInfo) {
|
||||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
triggerUpdate(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed() {
|
||||||
// do not trigger refresh on the server side
|
// do not trigger refresh on the server side
|
||||||
if (this.$isServer) return
|
if (this.$isServer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// re-render meta data when returning from a child component to parent
|
// re-render meta data when returning from a child component to parent
|
||||||
if (this._hasMetaInfo) {
|
if (this._hasMetaInfo) {
|
||||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (this.$el && this.$el.offsetParent !== null) return
|
if (this.$el && this.$el.offsetParent !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
if (!this.$parent) return
|
|
||||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
if (!this.$parent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerUpdate(this)
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}/**/
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function uniqBy (inputArray, predicate) {
|
export default function uniqBy(inputArray, predicate) {
|
||||||
return inputArray
|
return inputArray
|
||||||
.filter((x, i, arr) => i === arr.length - 1
|
.filter((x, i, arr) => i === arr.length - 1
|
||||||
? true
|
? true
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import _getMetaInfo from '../src/shared/getMetaInfo'
|
||||||
|
import { mount, defaultOptions, loadVueMetaPlugin } from './utils'
|
||||||
|
|
||||||
|
import GoodbyeWorld from './fixtures/goodbye-world.vue'
|
||||||
|
import HelloWorld from './fixtures/hello-world.vue'
|
||||||
|
import KeepAlive from './fixtures/keep-alive.vue'
|
||||||
|
|
||||||
|
const getMetaInfo = component => _getMetaInfo(defaultOptions, component)
|
||||||
|
|
||||||
|
describe('client', () => {
|
||||||
|
let Vue
|
||||||
|
|
||||||
|
beforeAll(() => (Vue = loadVueMetaPlugin()))
|
||||||
|
|
||||||
|
test('meta-info refreshed on component\'s data change', () => {
|
||||||
|
const wrapper = mount(HelloWorld, { localVue: Vue })
|
||||||
|
|
||||||
|
let metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Hello World')
|
||||||
|
wrapper.setData({ title: 'Goodbye World' })
|
||||||
|
|
||||||
|
metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Goodbye World')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('child meta-info removed when child is toggled', () => {
|
||||||
|
const wrapper = mount(GoodbyeWorld, { localVue: Vue })
|
||||||
|
|
||||||
|
let metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Hello World')
|
||||||
|
|
||||||
|
wrapper.setData({ childVisible: false })
|
||||||
|
|
||||||
|
metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Goodbye World')
|
||||||
|
|
||||||
|
wrapper.setData({ childVisible: true })
|
||||||
|
|
||||||
|
metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Hello World')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('child meta-info removed when keep-alive child is toggled', () => {
|
||||||
|
const wrapper = mount(KeepAlive, { localVue: Vue })
|
||||||
|
|
||||||
|
let metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Hello World')
|
||||||
|
|
||||||
|
wrapper.setData({ childVisible: false })
|
||||||
|
|
||||||
|
metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Alive World')
|
||||||
|
|
||||||
|
wrapper.setData({ childVisible: true })
|
||||||
|
|
||||||
|
metaInfo = getMetaInfo(wrapper.vm)
|
||||||
|
expect(metaInfo.title).toEqual('Hello World')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meta-info is removed when destroyed', () => {
|
||||||
|
const parentComponent = new Vue({ render: h => h('div') })
|
||||||
|
const wrapper = mount(HelloWorld, { localVue: Vue, parentComponent })
|
||||||
|
|
||||||
|
let metaInfo = getMetaInfo(wrapper.vm.$parent)
|
||||||
|
expect(metaInfo.title).toEqual('Hello World')
|
||||||
|
wrapper.destroy()
|
||||||
|
|
||||||
|
jest.runAllTimers()
|
||||||
|
metaInfo = getMetaInfo(wrapper.vm.$parent)
|
||||||
|
expect(metaInfo.title).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meta-info can be rendered with inject', () => {
|
||||||
|
const wrapper = mount(HelloWorld, { localVue: Vue })
|
||||||
|
|
||||||
|
const metaInfo = wrapper.vm.$meta().inject()
|
||||||
|
expect(metaInfo.title.text()).toEqual('<title data-vue-meta="true">Hello World</title>')
|
||||||
|
})
|
||||||
|
})
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<html {{ head.headAttrs.text() }}>
|
||||||
|
<head></head>
|
||||||
|
bla
|
||||||
|
</html>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: 'Hello World',
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'en'
|
||||||
|
},
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
head() {
|
||||||
|
return meta.inject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<hello-world v-if="childVisible"></hello-world>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HelloWorld from './hello-world.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HelloWorld
|
||||||
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
childVisible: true,
|
||||||
|
title: 'Goodbye World'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div>Test</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: 'Hello World',
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'en'
|
||||||
|
},
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<keep-alive>
|
||||||
|
<hello-world v-if="childVisible"></hello-world>
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HelloWorld from './hello-world.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HelloWorld
|
||||||
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
childVisible: true,
|
||||||
|
title: 'Alive World'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import _generateServerInjector from '../src/server/generateServerInjector'
|
||||||
|
import { defaultOptions } from './utils'
|
||||||
|
import metaInfoData from './utils/meta-info-data'
|
||||||
|
|
||||||
|
const generateServerInjector = (type, data) => _generateServerInjector(defaultOptions, type, data)
|
||||||
|
|
||||||
|
describe('generators', () => {
|
||||||
|
Object.keys(metaInfoData).forEach((type) => {
|
||||||
|
const typeTests = metaInfoData[type]
|
||||||
|
|
||||||
|
const testCases = {
|
||||||
|
add: (tags) => {
|
||||||
|
let html = tags.text()
|
||||||
|
|
||||||
|
// ssr only returns the attributes, convert to full tag
|
||||||
|
if (['htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||||
|
html = `<${type.substr(0, 4)} ${html}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
typeTests.add.expect.forEach((expected) => {
|
||||||
|
expect(html).toContain(expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(`${type} type tests`, () => {
|
||||||
|
Object.keys(typeTests).forEach((action) => {
|
||||||
|
const testInfo = typeTests[action]
|
||||||
|
|
||||||
|
// return when no test case available
|
||||||
|
if (!testCases[action] && !testInfo.test) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTestFn = () => {
|
||||||
|
const tags = generateServerInjector(type, testInfo.data)
|
||||||
|
testCases[action](tags)
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
let testFn
|
||||||
|
if (testInfo.test) {
|
||||||
|
testFn = testInfo.test('server', defaultTestFn)
|
||||||
|
|
||||||
|
if (testFn === true) {
|
||||||
|
testFn = defaultTestFn
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testFn = defaultTestFn
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testFn && typeof testFn === 'function') {
|
||||||
|
test.only(`${action} a tag`, () => {
|
||||||
|
expect.hasAssertions()
|
||||||
|
testFn()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import getComponentOption from '../src/shared/getComponentOption'
|
|
||||||
|
|
||||||
describe('getComponentOption', () => {
|
|
||||||
const container = document.createElement('div')
|
|
||||||
let component
|
|
||||||
|
|
||||||
afterEach(() => component.$destroy())
|
|
||||||
|
|
||||||
it('returns an empty object when no matching options are found', () => {
|
|
||||||
component = new Vue()
|
|
||||||
const mergedOption = getComponentOption({ component, option: 'noop' })
|
|
||||||
expect(mergedOption).to.eql({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fetches the given option from the given component', () => {
|
|
||||||
component = new Vue({ someOption: 'foo' })
|
|
||||||
const mergedOption = getComponentOption({ component, option: 'someOption' })
|
|
||||||
expect(mergedOption).to.eql('foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls a function option, injecting the component as context', () => {
|
|
||||||
component = new Vue({
|
|
||||||
name: 'foobar',
|
|
||||||
someFunc () {
|
|
||||||
return this.$options.name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const mergedOption = getComponentOption({ component, option: 'someFunc' })
|
|
||||||
expect(mergedOption).to.eql('foobar')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fetches deeply nested component options and merges them', () => {
|
|
||||||
Vue.component('merge-child', { template: '<div></div>', foo: { bar: 'baz' } })
|
|
||||||
|
|
||||||
component = new Vue({
|
|
||||||
foo: { fizz: 'buzz' },
|
|
||||||
render: (h) => h('div', null, [h('merge-child')]),
|
|
||||||
el: container
|
|
||||||
})
|
|
||||||
|
|
||||||
const mergedOption = getComponentOption({ component, option: 'foo', deep: true })
|
|
||||||
expect(mergedOption).to.eql({ bar: 'baz', fizz: 'buzz' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows for a custom array merge strategy', () => {
|
|
||||||
Vue.component('array-child', {
|
|
||||||
template: '<div></div>',
|
|
||||||
foo: [
|
|
||||||
{ name: 'flower', content: 'rose' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
component = new Vue({
|
|
||||||
render: (h) => h('div', null, [h('array-child')]),
|
|
||||||
foo: [
|
|
||||||
{ name: 'flower', content: 'tulip' }
|
|
||||||
],
|
|
||||||
el: container
|
|
||||||
})
|
|
||||||
|
|
||||||
const mergedOption = getComponentOption({
|
|
||||||
component,
|
|
||||||
option: 'foo',
|
|
||||||
deep: true,
|
|
||||||
arrayMerge (target, source) {
|
|
||||||
return target.concat(source)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(mergedOption).to.eql([
|
|
||||||
{ name: 'flower', content: 'tulip' },
|
|
||||||
{ name: 'flower', content: 'rose' }
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import getComponentOption from '../src/shared/getComponentOption'
|
||||||
|
import { getVue } from './utils'
|
||||||
|
|
||||||
|
describe('getComponentOption', () => {
|
||||||
|
let Vue
|
||||||
|
|
||||||
|
beforeAll(() => (Vue = getVue()))
|
||||||
|
|
||||||
|
it('returns an empty object when no matching options are found', () => {
|
||||||
|
const component = new Vue()
|
||||||
|
const mergedOption = getComponentOption({ component, keyName: 'noop' })
|
||||||
|
expect(mergedOption).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches the given option from the given component', () => {
|
||||||
|
const component = new Vue({ someOption: 'foo' })
|
||||||
|
const mergedOption = getComponentOption({ component, keyName: 'someOption' })
|
||||||
|
expect(mergedOption).toEqual('foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls a function option, injecting the component as context', () => {
|
||||||
|
const component = new Vue({
|
||||||
|
name: 'Foobar',
|
||||||
|
someFunc() {
|
||||||
|
return this.$options.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mergedOption = getComponentOption({ component, keyName: 'someFunc' })
|
||||||
|
// TODO: Should this be foobar or Foobar
|
||||||
|
expect(mergedOption).toEqual('Foobar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches deeply nested component options and merges them', () => {
|
||||||
|
Vue.component('merge-child', { render: h => h('div'), foo: { bar: 'baz' } })
|
||||||
|
|
||||||
|
const component = new Vue({
|
||||||
|
foo: { fizz: 'buzz' },
|
||||||
|
el: document.createElement('div'),
|
||||||
|
render: h => h('div', null, [h('merge-child')])
|
||||||
|
})
|
||||||
|
|
||||||
|
const mergedOption = getComponentOption({ component, keyName: 'foo', deep: true })
|
||||||
|
expect(mergedOption).toEqual({ bar: 'baz', fizz: 'buzz' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows for a custom array merge strategy', () => {
|
||||||
|
Vue.component('array-child', {
|
||||||
|
render: h => h('div'),
|
||||||
|
foo: [
|
||||||
|
{ name: 'flower', content: 'rose' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const component = new Vue({
|
||||||
|
foo: [
|
||||||
|
{ name: 'flower', content: 'tulip' }
|
||||||
|
],
|
||||||
|
el: document.createElement('div'),
|
||||||
|
render: h => h('div', null, [h('array-child')])
|
||||||
|
})
|
||||||
|
|
||||||
|
const mergedOption = getComponentOption({
|
||||||
|
component,
|
||||||
|
keyName: 'foo',
|
||||||
|
deep: true,
|
||||||
|
arrayMerge(target, source) {
|
||||||
|
return target.concat(source)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mergedOption).toEqual([
|
||||||
|
{ name: 'flower', content: 'tulip' },
|
||||||
|
{ name: 'flower', content: 'rose' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,35 +1,17 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import _getMetaInfo from '../src/shared/getMetaInfo'
|
import _getMetaInfo from '../src/shared/getMetaInfo'
|
||||||
import {
|
import { defaultOptions, loadVueMetaPlugin } from './utils'
|
||||||
VUE_META_ATTRIBUTE,
|
|
||||||
VUE_META_CONTENT_KEY,
|
|
||||||
VUE_META_KEY_NAME,
|
|
||||||
VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
|
||||||
VUE_META_TAG_LIST_ID_KEY_NAME,
|
|
||||||
VUE_META_TEMPLATE_KEY_NAME
|
|
||||||
} from '../src/shared/constants'
|
|
||||||
|
|
||||||
// set some default options
|
const getMetaInfo = component => _getMetaInfo(defaultOptions, component)
|
||||||
const defaultOptions = {
|
|
||||||
keyName: VUE_META_KEY_NAME,
|
|
||||||
attribute: VUE_META_ATTRIBUTE,
|
|
||||||
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
|
||||||
metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME,
|
|
||||||
contentKeyName: VUE_META_CONTENT_KEY,
|
|
||||||
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMetaInfo = _getMetaInfo(defaultOptions)
|
|
||||||
|
|
||||||
describe('getMetaInfo', () => {
|
describe('getMetaInfo', () => {
|
||||||
// const container = document.createElement('div')
|
let Vue
|
||||||
let component
|
|
||||||
|
|
||||||
afterEach(() => component.$destroy())
|
beforeAll(() => (Vue = loadVueMetaPlugin()))
|
||||||
|
|
||||||
it('returns appropriate defaults when no meta info is found', () => {
|
test('returns appropriate defaults when no meta info is found', () => {
|
||||||
component = new Vue()
|
const component = new Vue()
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: '',
|
title: '',
|
||||||
titleChunk: '',
|
titleChunk: '',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -47,8 +29,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns metaInfo when used in component', () => {
|
test('returns metaInfo when used in component', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -56,7 +38,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -75,8 +58,37 @@ describe('getMetaInfo', () => {
|
|||||||
__dangerouslyDisableSanitizersByTagID: {}
|
__dangerouslyDisableSanitizersByTagID: {}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('removes duplicate metaInfo in same component', () => {
|
|
||||||
component = new Vue({
|
test('converts base tag to array', () => {
|
||||||
|
const component = new Vue({
|
||||||
|
metaInfo: {
|
||||||
|
title: 'Hello',
|
||||||
|
base: { href: 'href' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
|
title: 'Hello',
|
||||||
|
titleChunk: 'Hello',
|
||||||
|
titleTemplate: '%s',
|
||||||
|
htmlAttrs: {},
|
||||||
|
headAttrs: {},
|
||||||
|
bodyAttrs: {},
|
||||||
|
meta: [],
|
||||||
|
base: [
|
||||||
|
{ href: 'href' }
|
||||||
|
],
|
||||||
|
link: [],
|
||||||
|
style: [],
|
||||||
|
script: [],
|
||||||
|
noscript: [],
|
||||||
|
__dangerouslyDisableSanitizers: [],
|
||||||
|
__dangerouslyDisableSanitizersByTagID: {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('removes duplicate metaInfo in same component', () => {
|
||||||
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -93,7 +105,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -117,8 +130,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses string titleTemplates', () => {
|
test('properly uses string titleTemplates', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleTemplate: '%s World',
|
titleTemplate: '%s World',
|
||||||
@@ -127,7 +140,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello World',
|
title: 'Hello World',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s World',
|
titleTemplate: '%s World',
|
||||||
@@ -147,10 +161,10 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses function titleTemplates', () => {
|
test('properly uses function titleTemplates', () => {
|
||||||
const titleTemplate = chunk => `${chunk} Function World`
|
const titleTemplate = chunk => `${chunk} Function World`
|
||||||
|
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleTemplate,
|
titleTemplate,
|
||||||
@@ -159,7 +173,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello Function World',
|
title: 'Hello Function World',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate,
|
titleTemplate,
|
||||||
@@ -179,12 +194,12 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has the proper `this` binding when using function titleTemplates', () => {
|
test('has the proper `this` binding when using function titleTemplates', () => {
|
||||||
const titleTemplate = function (chunk) {
|
const titleTemplate = function (chunk) {
|
||||||
return `${chunk} ${this.helloWorldText}`
|
return `${chunk} ${this.helloWorldText}`
|
||||||
}
|
}
|
||||||
|
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleTemplate,
|
titleTemplate,
|
||||||
@@ -192,13 +207,14 @@ describe('getMetaInfo', () => {
|
|||||||
{ charset: 'utf-8' }
|
{ charset: 'utf-8' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
helloWorldText: 'Function World'
|
helloWorldText: 'Function World'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello Function World',
|
title: 'Hello Function World',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate,
|
titleTemplate,
|
||||||
@@ -218,8 +234,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses string meta templates', () => {
|
test('properly uses string meta templates', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -232,7 +248,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -256,8 +273,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses function meta templates', () => {
|
test('properly uses function meta templates', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -270,7 +287,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -294,8 +312,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses content only if template is not defined', () => {
|
test('properly uses content only if template is not defined', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -307,7 +325,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -331,8 +350,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses content only if template is null', () => {
|
test('properly uses content only if template is null', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -345,7 +364,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -369,8 +389,8 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses content only if template is false', () => {
|
test('properly uses content only if template is false', () => {
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -383,7 +403,8 @@ describe('getMetaInfo', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(getMetaInfo(component)).to.eql({
|
|
||||||
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -407,9 +428,9 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses meta templates with one-level-deep nested children content', () => {
|
test('properly uses meta templates with one-level-deep nested children content', () => {
|
||||||
Vue.component('merge-child', {
|
Vue.component('merge-child', {
|
||||||
template: '<div></div>',
|
render: h => h('div'),
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -422,7 +443,7 @@ describe('getMetaInfo', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
@@ -433,11 +454,11 @@ describe('getMetaInfo', () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
render: (h) => h('div', null, [h('merge-child')]),
|
el: document.createElement('div'),
|
||||||
el: document.createElement('div')
|
render: h => h('div', null, [h('merge-child')])
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).to.eql({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -463,9 +484,9 @@ describe('getMetaInfo', () => {
|
|||||||
|
|
||||||
// TODO: Still failing :( Child template won't be applied if child has no content as well
|
// TODO: Still failing :( Child template won't be applied if child has no content as well
|
||||||
|
|
||||||
it('properly uses meta templates with one-level-deep nested children template', () => {
|
test('properly uses meta templates with one-level-deep nested children template', () => {
|
||||||
Vue.component('merge-child', {
|
Vue.component('merge-child', {
|
||||||
template: '<div></div>',
|
render: h => h('div'),
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -478,7 +499,7 @@ describe('getMetaInfo', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
@@ -489,11 +510,11 @@ describe('getMetaInfo', () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
render: (h) => h('div', null, [h('merge-child')]),
|
el: document.createElement('div'),
|
||||||
el: document.createElement('div')
|
render: h => h('div', null, [h('merge-child')])
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).to.eql({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -517,9 +538,9 @@ describe('getMetaInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('properly uses meta templates with one-level-deep nested children template and content', () => {
|
test('properly uses meta templates with one-level-deep nested children template and content', () => {
|
||||||
Vue.component('merge-child', {
|
Vue.component('merge-child', {
|
||||||
template: '<div></div>',
|
render: h => h('div'),
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
meta: [
|
meta: [
|
||||||
@@ -533,7 +554,7 @@ describe('getMetaInfo', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
component = new Vue({
|
const component = new Vue({
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
@@ -544,11 +565,11 @@ describe('getMetaInfo', () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
render: (h) => h('div', null, [h('merge-child')]),
|
el: document.createElement('div'),
|
||||||
el: document.createElement('div')
|
render: h => h('div', null, [h('merge-child')])
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getMetaInfo(component)).to.eql({
|
expect(getMetaInfo(component)).toEqual({
|
||||||
title: 'Hello',
|
title: 'Hello',
|
||||||
titleChunk: 'Hello',
|
titleChunk: 'Hello',
|
||||||
titleTemplate: '%s',
|
titleTemplate: '%s',
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
const testsContext = require.context('.', true, /\.spec$/)
|
|
||||||
const srcContext = require.context('../src', true, /\.js$/)
|
|
||||||
testsContext.keys().forEach(testsContext)
|
|
||||||
srcContext.keys().forEach(srcContext)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import VueMeta from '../src/shared/plugin'
|
|
||||||
import {
|
|
||||||
VUE_META_KEY_NAME,
|
|
||||||
VUE_META_ATTRIBUTE,
|
|
||||||
VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
|
||||||
VUE_META_TAG_LIST_ID_KEY_NAME
|
|
||||||
} from '../src/shared/constants'
|
|
||||||
|
|
||||||
describe('plugin', () => {
|
|
||||||
Vue.use(VueMeta, {
|
|
||||||
keyName: VUE_META_KEY_NAME,
|
|
||||||
attribute: VUE_META_ATTRIBUTE,
|
|
||||||
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
|
||||||
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds $meta() to Vue prototype', () => {
|
|
||||||
const instance = new Vue()
|
|
||||||
expect(instance.$meta).to.be.a('function')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('components have _hasMetaInfo set to true', () => {
|
|
||||||
const Component = Vue.component('test-component', {
|
|
||||||
template: '<div>Test</div>',
|
|
||||||
[VUE_META_KEY_NAME]: {
|
|
||||||
title: 'helloworld'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const vm = new Vue(Component).$mount()
|
|
||||||
expect(vm._hasMetaInfo).to.equal(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { mount, defaultOptions, VueMetaPlugin, loadVueMetaPlugin } from './utils'
|
||||||
|
|
||||||
|
jest.mock('../package.json', () => ({
|
||||||
|
version: 'test-version'
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('plugin', () => {
|
||||||
|
let Vue
|
||||||
|
|
||||||
|
beforeAll(() => (Vue = loadVueMetaPlugin()))
|
||||||
|
|
||||||
|
test('is loaded', () => {
|
||||||
|
const instance = new Vue()
|
||||||
|
expect(instance.$meta).toEqual(expect.any(Function))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('component has _hasMetaInfo set to true', () => {
|
||||||
|
const Component = Vue.component('test-component', {
|
||||||
|
template: '<div>Test</div>',
|
||||||
|
[defaultOptions.keyName]: {
|
||||||
|
title: 'Hello World'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { vm } = mount(Component, { localVue: Vue })
|
||||||
|
expect(vm._hasMetaInfo).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('plugin sets package version', () => {
|
||||||
|
expect(VueMetaPlugin.version).toBe('test-version')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import _updateClientMetaInfo from '../src/client/updateClientMetaInfo'
|
||||||
|
import { defaultOptions } from './utils'
|
||||||
|
import metaInfoData from './utils/meta-info-data'
|
||||||
|
|
||||||
|
const updateClientMetaInfo = (type, data) => _updateClientMetaInfo(defaultOptions, { [type]: data })
|
||||||
|
|
||||||
|
describe('updaters', () => {
|
||||||
|
let html
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
html = document.getElementsByTagName('html')[0]
|
||||||
|
|
||||||
|
// remove default meta charset
|
||||||
|
Array.from(html.getElementsByTagName('meta')).forEach(el => el.parentNode.removeChild(el))
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(metaInfoData).forEach((type) => {
|
||||||
|
const typeTests = metaInfoData[type]
|
||||||
|
|
||||||
|
const testCases = {
|
||||||
|
add: (tags) => {
|
||||||
|
typeTests.add.expect.forEach((expected, index) => {
|
||||||
|
if (!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||||
|
expect(tags.addedTags[type][index].outerHTML).toBe(expected)
|
||||||
|
}
|
||||||
|
expect(html.outerHTML).toContain(expected)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
change: (tags) => {
|
||||||
|
typeTests.add.expect.forEach((expected, index) => {
|
||||||
|
if (!typeTests.change.expect.includes(expected)) {
|
||||||
|
expect(html.outerHTML).not.toContain(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
typeTests.change.expect.forEach((expected, index) => {
|
||||||
|
if (!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) {
|
||||||
|
expect(tags.addedTags[type][index].outerHTML).toBe(expected)
|
||||||
|
}
|
||||||
|
expect(html.outerHTML).toContain(expected)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
remove: (tags) => {
|
||||||
|
// TODO: i'd expect tags.removedTags to be populated
|
||||||
|
|
||||||
|
typeTests.add.expect.forEach((expected, index) => {
|
||||||
|
expect(html.outerHTML).not.toContain(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
typeTests.change.expect.forEach((expected, index) => {
|
||||||
|
expect(html.outerHTML).not.toContain(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(html.outerHTML).not.toContain(`<${type}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(`${type} type tests`, () => {
|
||||||
|
Object.keys(typeTests).forEach((action) => {
|
||||||
|
const testInfo = typeTests[action]
|
||||||
|
|
||||||
|
// return when no test case available
|
||||||
|
if (!testCases[action] && !testInfo.test) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTestFn = () => {
|
||||||
|
const tags = updateClientMetaInfo(type, testInfo.data)
|
||||||
|
|
||||||
|
if (testCases[action]) {
|
||||||
|
testCases[action](tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
let testFn
|
||||||
|
if (testInfo.test) {
|
||||||
|
testFn = testInfo.test('client', defaultTestFn)
|
||||||
|
|
||||||
|
if (testFn === true) {
|
||||||
|
testFn = defaultTestFn
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testFn = defaultTestFn
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testFn && typeof testFn === 'function') {
|
||||||
|
test.only(`${action} a tag`, () => {
|
||||||
|
expect.hasAssertions()
|
||||||
|
testFn()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import { renderToString } from '@vue/server-test-utils'
|
||||||
|
import VueMetaPlugin from '../../src'
|
||||||
|
|
||||||
|
import {
|
||||||
|
VUE_META_ATTRIBUTE,
|
||||||
|
VUE_META_CONTENT_KEY,
|
||||||
|
VUE_META_KEY_NAME,
|
||||||
|
VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||||
|
VUE_META_TAG_LIST_ID_KEY_NAME,
|
||||||
|
VUE_META_TEMPLATE_KEY_NAME
|
||||||
|
} from '../../src/shared/constants'
|
||||||
|
|
||||||
|
export {
|
||||||
|
mount,
|
||||||
|
renderToString,
|
||||||
|
VueMetaPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions = {
|
||||||
|
keyName: VUE_META_KEY_NAME,
|
||||||
|
attribute: VUE_META_ATTRIBUTE,
|
||||||
|
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||||
|
metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME,
|
||||||
|
contentKeyName: VUE_META_CONTENT_KEY,
|
||||||
|
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVue() {
|
||||||
|
return createLocalVue()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadVueMetaPlugin(options, localVue = getVue()) {
|
||||||
|
localVue.use(VueMetaPlugin, Object.assign({}, defaultOptions, options))
|
||||||
|
|
||||||
|
return localVue
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { defaultOptions } from './'
|
||||||
|
|
||||||
|
const metaInfoData = {
|
||||||
|
title: {
|
||||||
|
add: {
|
||||||
|
data: 'Hello World',
|
||||||
|
expect: ['<title data-vue-meta="true">Hello World</title>'],
|
||||||
|
test(side, defaultTest) {
|
||||||
|
if (side === 'client') {
|
||||||
|
// client side vue-meta uses document.title and doesnt change the html
|
||||||
|
return () => {
|
||||||
|
this.expect[0] = '<title>Hello World</title>'
|
||||||
|
const spy = jest.spyOn(document, 'title', 'set')
|
||||||
|
|
||||||
|
defaultTest()
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(spy).toHaveBeenCalledWith('Hello World')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return defaultTest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
add: {
|
||||||
|
data: [{ href: 'href' }],
|
||||||
|
expect: ['<base data-vue-meta="true" href="href">']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: [{ href: 'href2' }],
|
||||||
|
expect: ['<base data-vue-meta="true" href="href2">']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: [],
|
||||||
|
expect: ['']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
add: {
|
||||||
|
data: [{ charset: 'utf-8' }, { property: 'a', content: 'a' }],
|
||||||
|
expect: [
|
||||||
|
'<meta data-vue-meta="true" charset="utf-8">',
|
||||||
|
'<meta data-vue-meta="true" property="a" content="a">'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: [
|
||||||
|
{ charset: 'utf-16' },
|
||||||
|
{ property: 'a', content: 'b' }
|
||||||
|
],
|
||||||
|
expect: [
|
||||||
|
'<meta data-vue-meta="true" charset="utf-16">',
|
||||||
|
'<meta data-vue-meta="true" property="a" content="b">'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// make sure elements that already exists are not unnecessarily updated
|
||||||
|
duplicate: {
|
||||||
|
data: [
|
||||||
|
{ charset: 'utf-16' },
|
||||||
|
{ property: 'a', content: 'c' }
|
||||||
|
],
|
||||||
|
expect: [
|
||||||
|
'<meta data-vue-meta="true" charset="utf-16">',
|
||||||
|
'<meta data-vue-meta="true" property="a" content="c">'
|
||||||
|
],
|
||||||
|
test(side, defaultTest) {
|
||||||
|
if (side === 'client') {
|
||||||
|
return () => {
|
||||||
|
const tags = defaultTest()
|
||||||
|
|
||||||
|
expect(tags.addedTags.meta.length).toBe(1)
|
||||||
|
// TODO: not sure if we really expect this
|
||||||
|
expect(tags.removedTags.meta.length).toBe(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: [],
|
||||||
|
expect: ['']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
add: {
|
||||||
|
data: [{ rel: 'stylesheet', href: 'href' }],
|
||||||
|
expect: ['<link data-vue-meta="true" rel="stylesheet" href="href">']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: [{ rel: 'stylesheet', href: 'href', media: 'screen' }],
|
||||||
|
expect: ['<link data-vue-meta="true" rel="stylesheet" href="href" media="screen">']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: [],
|
||||||
|
expect: ['']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
add: {
|
||||||
|
data: [{ type: 'text/css', cssText: '.foo { color: red; }' }],
|
||||||
|
expect: ['<style data-vue-meta="true" type="text/css">.foo { color: red; }</style>']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: [{ type: 'text/css', cssText: '.foo { color: blue; }' }],
|
||||||
|
expect: ['<style data-vue-meta="true" type="text/css">.foo { color: blue; }</style>']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: [],
|
||||||
|
expect: ['']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
script: {
|
||||||
|
add: {
|
||||||
|
data: [
|
||||||
|
{ src: 'src', async: true, defer: true, [defaultOptions.tagIDKeyName]: 'content' },
|
||||||
|
{ src: 'src', async: true, defer: true, body: true }
|
||||||
|
],
|
||||||
|
expect: [
|
||||||
|
'<script data-vue-meta="true" src="src" async="true" defer="true" data-vmid="content"></script>',
|
||||||
|
'<script data-vue-meta="true" src="src" async="true" defer="true" data-body="true"></script>'
|
||||||
|
],
|
||||||
|
test(side, defaultTest) {
|
||||||
|
return () => {
|
||||||
|
if (side === 'client') {
|
||||||
|
const tags = defaultTest()
|
||||||
|
|
||||||
|
expect(tags.addedTags.script[0].parentNode.tagName).toBe('HEAD')
|
||||||
|
expect(tags.addedTags.script[1].parentNode.tagName).toBe('BODY')
|
||||||
|
} else {
|
||||||
|
// ssr doesnt generate data-body tags
|
||||||
|
const bodyScript = this.expect[1]
|
||||||
|
this.expect = [this.expect[0]]
|
||||||
|
|
||||||
|
const tags = defaultTest()
|
||||||
|
|
||||||
|
expect(tags.text()).not.toContain(bodyScript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: [{ src: 'src', async: true, defer: true, [defaultOptions.tagIDKeyName]: 'content2' }],
|
||||||
|
expect: ['<script data-vue-meta="true" src="src" async="true" defer="true" data-vmid="content2"></script>']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: [],
|
||||||
|
expect: ['']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
noscript: {
|
||||||
|
add: {
|
||||||
|
data: [{ innerHTML: '<p>noscript</p>' }],
|
||||||
|
expect: ['<noscript data-vue-meta="true"><p>noscript</p></noscript>']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: [{ innerHTML: '<p>noscript, no really</p>' }],
|
||||||
|
expect: ['<noscript data-vue-meta="true"><p>noscript, no really</p></noscript>']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: [],
|
||||||
|
expect: ['']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
htmlAttrs: {
|
||||||
|
add: {
|
||||||
|
data: { foo: 'bar' },
|
||||||
|
expect: ['<html foo="bar" data-vue-meta="foo">']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: { foo: 'baz' },
|
||||||
|
expect: ['<html foo="baz" data-vue-meta="foo">']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: {},
|
||||||
|
expect: ['<html>']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headAttrs: {
|
||||||
|
add: {
|
||||||
|
data: { foo: 'bar' },
|
||||||
|
expect: ['<head foo="bar" data-vue-meta="foo">']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: { foo: 'baz' },
|
||||||
|
expect: ['<head foo="baz" data-vue-meta="foo">']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: {},
|
||||||
|
expect: ['<head>']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
add: {
|
||||||
|
data: { foo: 'bar' },
|
||||||
|
expect: ['<body foo="bar" data-vue-meta="foo">']
|
||||||
|
},
|
||||||
|
change: {
|
||||||
|
data: { foo: 'baz' },
|
||||||
|
expect: ['<body foo="baz" data-vue-meta="foo">']
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
data: {},
|
||||||
|
expect: ['<body>']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
add: {
|
||||||
|
data: [{}],
|
||||||
|
expect: [''],
|
||||||
|
test: side => side === 'server'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default metaInfoData
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import jsdom from 'jsdom-global'
|
||||||
|
|
||||||
|
jsdom()
|
||||||
|
|
||||||
|
jest.useFakeTimers()
|
||||||
Reference in New Issue
Block a user