mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-08 22:02:25 +03:00
feat: major refactor, cleanup and jest tests
This commit is contained in:
@@ -1,8 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["istanbul"]
|
||||
}
|
||||
}
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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))
|
||||
|
||||
const host = process.env.HOST || 'localhost'
|
||||
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 renderer = require('vue-server-renderer').createRenderer()
|
||||
const VueMeta = require('../')
|
||||
const VueMeta = require(process.env.NODE_ENV === 'development' ? '../' : 'vue-meta')
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Vue = require('vue')
|
||||
const renderer = require('vue-server-renderer').createRenderer()
|
||||
const VueMeta = require('../')
|
||||
const VueMeta = require(process.env.NODE_ENV === 'development' ? '../' : 'vue-meta')
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import assign from 'object-assign'
|
||||
import Vue from 'vue'
|
||||
import VueMeta from 'vue-meta'
|
||||
import Router from 'vue-router'
|
||||
@@ -41,6 +40,7 @@ const router = new Router({
|
||||
})
|
||||
|
||||
const App = {
|
||||
router,
|
||||
template: `
|
||||
<div id="app">
|
||||
<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')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assign from 'object-assign'
|
||||
import Vue from 'vue'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
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 store from './store'
|
||||
import router from './router'
|
||||
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 path from 'path'
|
||||
import webpack from 'webpack'
|
||||
import WebpackBar from 'webpackbar'
|
||||
import VueLoaderPlugin from 'vue-loader/lib/plugin'
|
||||
|
||||
export default {
|
||||
devtool: 'inline-source-map',
|
||||
@@ -28,7 +30,7 @@ export default {
|
||||
resolve: {
|
||||
alias: {
|
||||
'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.
|
||||
@@ -37,7 +39,8 @@ export default {
|
||||
__dirname: true
|
||||
},
|
||||
plugins: [
|
||||
// new webpack.optimize.CommonsChunkPlugin('shared.js'),
|
||||
new WebpackBar(),
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'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",
|
||||
"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",
|
||||
"author": "Declan de Wet <declandewet@me.com>",
|
||||
"bugs": "https://github.com/declandewet/vue-meta/issues",
|
||||
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"build": "rimraf lib && rollup -c",
|
||||
"codecov": "codecov",
|
||||
"deploy": "npm version",
|
||||
"dev": "babel-node examples/server.js",
|
||||
"lint": "standard --verbose | snazzy",
|
||||
"minify": "uglifyjs lib/vue-meta.js -cm --comments -o lib/vue-meta.min.js",
|
||||
"postbuild": "npm run minify",
|
||||
"dev": "cd examples && npm run dev && cd ..",
|
||||
"lint": "eslint src test",
|
||||
"postdeploy": "git push origin master --follow-tags && npm run release",
|
||||
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
|
||||
"prebuild": "rimraf lib",
|
||||
"predeploy": "git checkout master && git pull -r",
|
||||
"prerelease": "npm run build",
|
||||
"pretest": "npm run lint",
|
||||
"preversion": "npm run toc",
|
||||
"release": "npm publish",
|
||||
"test": "cross-env NODE_ENV=test karma start karma.conf.js",
|
||||
"test": "jest",
|
||||
"toc": "doctoc README.md --title '# Table of Contents'",
|
||||
"update-cdn": "babel-node scripts/update-cdn.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^3.0.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.uniqueid": "^4.0.1",
|
||||
"object-assign": "^4.1.1"
|
||||
"lodash.uniqueid": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/node": "^7.0.0",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@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-plugin-istanbul": "^5.1.0",
|
||||
"chai": "^4.2.0",
|
||||
"codecov": "^3.1.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^2.0.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"es6-promise": "^4.2.5",
|
||||
"express": "^4.16.4",
|
||||
"express-urlrewrite": "^1.2.0",
|
||||
"file-loader": "^3.0.0",
|
||||
"karma": "^3.1.1",
|
||||
"karma-chai": "^0.1.0",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^3.0.5",
|
||||
"mocha": "^5.2.0",
|
||||
"phantomjs-prebuilt": "^2.1.16",
|
||||
"eslint": "^5.13.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-jest": "^22.2.2",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.1.0",
|
||||
"jest": "^24.1.0",
|
||||
"jsdom": "^13.2.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"rollup": "^1.0.0",
|
||||
"rollup-plugin-buble": "^0.19.4",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-json": "^3.1.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"snazzy": "^8.0.0",
|
||||
"standard": "^12.0.1",
|
||||
"uglify-js": "^3.4.9",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"update-section": "^0.3.3",
|
||||
"vue": "^2.5.17",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-server-renderer": "^2.5.17",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"vuex": "^3.0.1",
|
||||
"webpack": "^4.26.1",
|
||||
"webpack-dev-server": "^3.1.10"
|
||||
"vue": "^2.6.3",
|
||||
"vue-jest": "^3.0.2",
|
||||
"vue-server-renderer": "^2.6.3",
|
||||
"vue-template-compiler": "^2.6.3"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"types/index.d.ts",
|
||||
"types/vue.d.ts"
|
||||
],
|
||||
"homepage": "https://github.com/declandewet/vue-meta",
|
||||
"homepage": "https://github.com/nuxt/vue-meta",
|
||||
"keywords": [
|
||||
"attribute",
|
||||
"google",
|
||||
@@ -97,28 +83,12 @@
|
||||
"vue"
|
||||
],
|
||||
"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",
|
||||
"nyc": {
|
||||
"exclude": [
|
||||
"test/**/*.js"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"url": "git@github.com:declandewet/vue-meta.git",
|
||||
"url": "git@github.com/nuxt/vue-meta.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 json from 'rollup-plugin-json'
|
||||
import buble from 'rollup-plugin-buble'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
const pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: './src/index.js',
|
||||
output: {
|
||||
file: pkg.main,
|
||||
format: 'umd',
|
||||
name: 'VueMeta',
|
||||
banner: `/**
|
||||
const banner = `/**
|
||||
* vue-meta v${pkg.version}
|
||||
* (c) ${new Date().getFullYear()} Declan de Wet & Sébastien Chopin (@Atinux)
|
||||
* @license MIT
|
||||
*/
|
||||
`.replace(/ {4}/gm, '').trim()
|
||||
|
||||
const baseConfig = {
|
||||
input: './src/index.js',
|
||||
output: {
|
||||
file: pkg.web,
|
||||
format: 'umd',
|
||||
name: 'VueMeta',
|
||||
sourcemap: false,
|
||||
banner
|
||||
},
|
||||
plugins: [
|
||||
json(),
|
||||
nodeResolve({ jsnext: true }),
|
||||
commonjs(),
|
||||
buble()
|
||||
nodeResolve(),
|
||||
commonJs(),
|
||||
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
|
||||
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
|
||||
@@ -12,7 +12,7 @@ const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFram
|
||||
* @param {Function} callback - the update to perform
|
||||
* @return {Number} id - a new ID
|
||||
*/
|
||||
export default function batchUpdate (id, callback) {
|
||||
export default function batchUpdate(id, callback) {
|
||||
stopUpdate(id)
|
||||
return startUpdate(() => {
|
||||
id = null
|
||||
|
||||
+12
-5
@@ -1,7 +1,7 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
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.
|
||||
* 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 function refresh () {
|
||||
const info = getMetaInfo(options)(this.$root)
|
||||
updateClientMetaInfo(options).call(this, info)
|
||||
return info
|
||||
return function refresh() {
|
||||
const metaInfo = getMetaInfo(options, this.$root)
|
||||
|
||||
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 updateTagAttributes from './updaters/updateTagAttributes'
|
||||
import updateTags from './updaters/updateTags'
|
||||
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @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 = {}
|
||||
// only cache tags for current update
|
||||
const tags = {}
|
||||
|
||||
Object.keys(newInfo).forEach((key) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
const htmlTag = getTag(tags, 'html')
|
||||
|
||||
// emit "event" with new info
|
||||
if (typeof newInfo.changed === 'function') {
|
||||
newInfo.changed.call(this, newInfo, addedTags, removedTags)
|
||||
// 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((type) => {
|
||||
// ignore these
|
||||
if ([
|
||||
'titleChunk',
|
||||
'titleTemplate',
|
||||
'changed',
|
||||
'__dangerouslyDisableSanitizers',
|
||||
'__dangerouslyDisableSanitizersByTagID'
|
||||
].includes(type)) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// remove the server render attribute so we can update on changes
|
||||
htmlTag.removeAttribute(ssrAttribute)
|
||||
}
|
||||
|
||||
if (type === 'title') {
|
||||
// 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 install from './shared/plugin'
|
||||
|
||||
install.version = version
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import titleGenerator from './generators/titleGenerator'
|
||||
import attrsGenerator from './generators/attrsGenerator'
|
||||
import tagGenerator from './generators/tagGenerator'
|
||||
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
|
||||
|
||||
export default function _generateServerInjector (options = {}) {
|
||||
/**
|
||||
* 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|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
return function generateServerInjector (type, data) {
|
||||
switch (type) {
|
||||
case 'title':
|
||||
return titleGenerator(options)(type, data)
|
||||
case 'htmlAttrs':
|
||||
case 'bodyAttrs':
|
||||
case 'headAttrs':
|
||||
return attrsGenerator(options)(type, data)
|
||||
default:
|
||||
return tagGenerator(options)(type, data)
|
||||
}
|
||||
/**
|
||||
* 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|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
|
||||
export default function generateServerInjector(options, type, data) {
|
||||
if (type === 'title') {
|
||||
return titleGenerator(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 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
|
||||
* 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
|
||||
* @return {Object} - server meta info with `toString` methods
|
||||
*/
|
||||
return function inject () {
|
||||
|
||||
return function inject() {
|
||||
// get meta info with sensible defaults
|
||||
const info = getMetaInfo(options)(this.$root)
|
||||
const metaInfo = getMetaInfo(options, this.$root)
|
||||
|
||||
// generate server injectors
|
||||
for (let key in info) {
|
||||
if (info.hasOwnProperty(key) && key !== 'titleTemplate' && key !== 'titleChunk') {
|
||||
info[key] = generateServerInjector(options)(key, info[key])
|
||||
for (const key in metaInfo) {
|
||||
if (!['titleTemplate', 'titleChunk'].includes(key) && metaInfo.hasOwnProperty(key)) {
|
||||
metaInfo[key] = generateServerInjector(options, key, metaInfo[key])
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
return metaInfo
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import inject from '../server/inject'
|
||||
import refresh from '../client/refresh'
|
||||
|
||||
export default function _$meta (options = {}) {
|
||||
export default function _$meta(options = {}) {
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
* @return {Object} - injector
|
||||
*/
|
||||
return function $meta () {
|
||||
return function $meta() {
|
||||
return {
|
||||
inject: inject(options).bind(this),
|
||||
refresh: refresh(options).bind(this)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import uniqBy from './uniqBy'
|
||||
import uniqueId from 'lodash.uniqueid'
|
||||
import uniqBy from './uniqBy'
|
||||
|
||||
/**
|
||||
* 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.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 {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
|
||||
* @return {Object} result - final aggregated result
|
||||
*/
|
||||
export default function getComponentOption (opts, result = {}) {
|
||||
const { component, option, deep, arrayMerge, metaTemplateKeyName, tagIDKeyName, contentKeyName } = opts
|
||||
export default function getComponentOption({ component, deep, arrayMerge, keyName, metaTemplateKeyName, tagIDKeyName, contentKeyName } = {}, result = {}) {
|
||||
const { $options } = component
|
||||
|
||||
if (component._inactive) return result
|
||||
if (component._inactive) {
|
||||
return result
|
||||
}
|
||||
|
||||
// only collect option data if it exists
|
||||
if (typeof $options[option] !== 'undefined' && $options[option] !== null) {
|
||||
let data = $options[option]
|
||||
if (typeof $options[keyName] !== 'undefined' && $options[keyName] !== null) {
|
||||
let data = $options[keyName]
|
||||
|
||||
// if option is a function, replace it with it's result
|
||||
if (typeof data === 'function') {
|
||||
@@ -44,14 +45,15 @@ export default function getComponentOption (opts, result = {}) {
|
||||
component.$children.forEach((childComponent) => {
|
||||
result = getComponentOption({
|
||||
component: childComponent,
|
||||
option,
|
||||
keyName,
|
||||
deep,
|
||||
arrayMerge
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
if (!metaObject.hasOwnProperty(metaTemplateKeyName) || !metaObject.hasOwnProperty(contentKeyName) || typeof metaObject[metaTemplateKeyName] === 'undefined') {
|
||||
return result.meta[metaKey]
|
||||
|
||||
+128
-121
@@ -3,7 +3,7 @@ import isPlainObject from 'lodash.isplainobject'
|
||||
import isArray from './isArray'
|
||||
import getComponentOption from './getComponentOption'
|
||||
|
||||
const escapeHTML = (str) => typeof window === 'undefined'
|
||||
const escapeHTML = str => typeof window === 'undefined'
|
||||
// server-side escape sequence
|
||||
? String(str)
|
||||
.replace(/&/g, '&')
|
||||
@@ -19,138 +19,145 @@ const escapeHTML = (str) => typeof window === 'undefined'
|
||||
.replace(/"/g, '\u0022')
|
||||
.replace(/'/g, '\u0027')
|
||||
|
||||
export default function _getMetaInfo (options = {}) {
|
||||
const { keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = options
|
||||
/**
|
||||
* 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: {}
|
||||
}
|
||||
const applyTemplate = (component, template, chunk) =>
|
||||
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
let info = getComponentOption({
|
||||
component,
|
||||
option: keyName,
|
||||
deep: true,
|
||||
metaTemplateKeyName,
|
||||
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 (let targetIndex in target) {
|
||||
const targetItem = target[targetIndex]
|
||||
let shared = false
|
||||
for (let 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 template defined in child but content in parent
|
||||
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component)(sourceTemplate)(targetItem[contentKeyName])
|
||||
delete sourceItem[metaTemplateKeyName]
|
||||
}
|
||||
shared = true
|
||||
break
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, 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({
|
||||
deep: true,
|
||||
component,
|
||||
keyName,
|
||||
metaTemplateKeyName,
|
||||
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) {
|
||||
destination.push(targetItem)
|
||||
// If template defined in child but content in parent
|
||||
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component, sourceTemplate, targetItem[contentKeyName])
|
||||
delete sourceItem[metaTemplateKeyName]
|
||||
}
|
||||
|
||||
shared = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
escaped[key] = val
|
||||
if (!shared) {
|
||||
destination.push(targetItem)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
escaped[key] = val
|
||||
}
|
||||
} else {
|
||||
escaped[key] = val
|
||||
}
|
||||
|
||||
return escaped
|
||||
}, {})
|
||||
return escaped
|
||||
}, {})
|
||||
|
||||
// merge with defaults
|
||||
info = deepmerge(defaultInfo, info)
|
||||
// merge with defaults
|
||||
info = deepmerge(defaultInfo, info)
|
||||
|
||||
// begin sanitization
|
||||
info = escape(info)
|
||||
// begin sanitization
|
||||
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
|
||||
* @return {Boolean} - true if `arr` is an array
|
||||
*/
|
||||
export default function isArray (arr) {
|
||||
export default function isArray(arr) {
|
||||
return Array.isArray
|
||||
? Array.isArray(arr)
|
||||
: 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 $meta from './$meta'
|
||||
|
||||
import {
|
||||
VUE_META_KEY_NAME,
|
||||
@@ -19,7 +18,7 @@ if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') {
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
export default function VueMeta (Vue, options = {}) {
|
||||
export default function VueMeta(Vue, options = {}) {
|
||||
// set some default options
|
||||
const defaultOptions = {
|
||||
keyName: VUE_META_KEY_NAME,
|
||||
@@ -29,8 +28,15 @@ export default function VueMeta (Vue, options = {}) {
|
||||
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
||||
}
|
||||
|
||||
// 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
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
@@ -38,66 +44,115 @@ export default function VueMeta (Vue, options = {}) {
|
||||
// store an id to keep track of DOM updates
|
||||
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
|
||||
Vue.mixin({
|
||||
beforeCreate () {
|
||||
beforeCreate() {
|
||||
// Add a marker to know if it uses metaInfo
|
||||
// _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)
|
||||
if (typeof this.$options[options.keyName] !== 'undefined') {
|
||||
if (typeof this.$options[options.keyName] !== 'undefined' && this.$options[options.keyName] !== null) {
|
||||
this._hasMetaInfo = true
|
||||
}
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (typeof this.$options[options.keyName] === 'function') {
|
||||
if (typeof this.$options.computed === 'undefined') {
|
||||
this.$options.computed = {}
|
||||
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (typeof this.$options[options.keyName] === 'function') {
|
||||
if (typeof this.$options.computed === 'undefined') {
|
||||
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
|
||||
// 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)
|
||||
if (!this.$isServer && this.$metaInfo) {
|
||||
this.$watch('$metaInfo', () => {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
})
|
||||
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||
}
|
||||
|
||||
},
|
||||
activated () {
|
||||
activated() {
|
||||
if (this._hasMetaInfo) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
deactivated () {
|
||||
deactivated() {
|
||||
if (this._hasMetaInfo) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
beforeMount() {
|
||||
if (this._hasMetaInfo) {
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
destroyed() {
|
||||
// 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
|
||||
if (this._hasMetaInfo) {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) return
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
if (!this.$parent) return
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
|
||||
if (!this.$parent) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerUpdate(this)
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
}/**/
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function uniqBy (inputArray, predicate) {
|
||||
export default function uniqBy(inputArray, predicate) {
|
||||
return inputArray
|
||||
.filter((x, i, arr) => i === arr.length - 1
|
||||
? 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 {
|
||||
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'
|
||||
import { defaultOptions, loadVueMetaPlugin } from './utils'
|
||||
|
||||
// set some default options
|
||||
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)
|
||||
const getMetaInfo = component => _getMetaInfo(defaultOptions, component)
|
||||
|
||||
describe('getMetaInfo', () => {
|
||||
// const container = document.createElement('div')
|
||||
let component
|
||||
let Vue
|
||||
|
||||
afterEach(() => component.$destroy())
|
||||
beforeAll(() => (Vue = loadVueMetaPlugin()))
|
||||
|
||||
it('returns appropriate defaults when no meta info is found', () => {
|
||||
component = new Vue()
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
test('returns appropriate defaults when no meta info is found', () => {
|
||||
const component = new Vue()
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: '',
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
@@ -47,8 +29,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('returns metaInfo when used in component', () => {
|
||||
component = new Vue({
|
||||
test('returns metaInfo when used in component', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -56,7 +38,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -75,8 +58,37 @@ describe('getMetaInfo', () => {
|
||||
__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: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -93,7 +105,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -117,8 +130,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('properly uses string titleTemplates', () => {
|
||||
component = new Vue({
|
||||
test('properly uses string titleTemplates', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
titleTemplate: '%s World',
|
||||
@@ -127,7 +140,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello World',
|
||||
titleChunk: 'Hello',
|
||||
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`
|
||||
|
||||
component = new Vue({
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
titleTemplate,
|
||||
@@ -159,7 +173,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello Function World',
|
||||
titleChunk: 'Hello',
|
||||
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) {
|
||||
return `${chunk} ${this.helloWorldText}`
|
||||
}
|
||||
|
||||
component = new Vue({
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
titleTemplate,
|
||||
@@ -192,13 +207,14 @@ describe('getMetaInfo', () => {
|
||||
{ charset: 'utf-8' }
|
||||
]
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
helloWorldText: 'Function World'
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello Function World',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate,
|
||||
@@ -218,8 +234,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('properly uses string meta templates', () => {
|
||||
component = new Vue({
|
||||
test('properly uses string meta templates', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -232,7 +248,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -256,8 +273,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('properly uses function meta templates', () => {
|
||||
component = new Vue({
|
||||
test('properly uses function meta templates', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -270,7 +287,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -294,8 +312,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('properly uses content only if template is not defined', () => {
|
||||
component = new Vue({
|
||||
test('properly uses content only if template is not defined', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -307,7 +325,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -331,8 +350,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('properly uses content only if template is null', () => {
|
||||
component = new Vue({
|
||||
test('properly uses content only if template is null', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -345,7 +364,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -369,8 +389,8 @@ describe('getMetaInfo', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('properly uses content only if template is false', () => {
|
||||
component = new Vue({
|
||||
test('properly uses content only if template is false', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -383,7 +403,8 @@ describe('getMetaInfo', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(getMetaInfo(component)).to.eql({
|
||||
|
||||
expect(getMetaInfo(component)).toEqual({
|
||||
title: 'Hello',
|
||||
titleChunk: 'Hello',
|
||||
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', {
|
||||
template: '<div></div>',
|
||||
render: h => h('div'),
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -422,7 +443,7 @@ describe('getMetaInfo', () => {
|
||||
}
|
||||
})
|
||||
|
||||
component = new Vue({
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
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',
|
||||
titleChunk: 'Hello',
|
||||
titleTemplate: '%s',
|
||||
@@ -463,9 +484,9 @@ describe('getMetaInfo', () => {
|
||||
|
||||
// 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', {
|
||||
template: '<div></div>',
|
||||
render: h => h('div'),
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -478,7 +499,7 @@ describe('getMetaInfo', () => {
|
||||
}
|
||||
})
|
||||
|
||||
component = new Vue({
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
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',
|
||||
titleChunk: 'Hello',
|
||||
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', {
|
||||
template: '<div></div>',
|
||||
render: h => h('div'),
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
meta: [
|
||||
@@ -533,7 +554,7 @@ describe('getMetaInfo', () => {
|
||||
}
|
||||
})
|
||||
|
||||
component = new Vue({
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
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',
|
||||
titleChunk: 'Hello',
|
||||
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