2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-24 02:40:33 +03:00

feat: add possibility to add additional meta info

refactor: server injectors

feat: add head, bodyPrepend, bodyAppend injectors

refactor: create browserbuild through rollup replace (not separate entry)
This commit is contained in:
pimlie
2019-09-13 00:08:21 +02:00
committed by Pim
parent 0e49a9c43e
commit 0ab76ee16b
27 changed files with 389 additions and 387 deletions
+5
View File
@@ -138,5 +138,10 @@ export default function createApp () {
</div>` </div>`
}) })
const { set } = app.$meta().addApp('custom')
set({
meta: [{ charset: 'utf-8' }]
})
return { app, router } return { app, router }
} }
+3 -10
View File
@@ -1,23 +1,16 @@
<!doctype html> <!doctype html>
<html {{ htmlAttrs.text(true) }}> <html {{ htmlAttrs.text(true) }}>
<head {{ headAttrs.text() }}> <head {{ headAttrs.text() }}>
{{ title.text() }} {{ head(true) }}
{{ meta.text() }}
<link rel="stylesheet" href="/global.css"> <link rel="stylesheet" href="/global.css">
{{ link.text() }}
{{ style.text() }}
{{ script.text() }}
{{ noscript.text() }}
</head> </head>
<body {{ bodyAttrs.text() }}> <body {{ bodyAttrs.text() }}>
{{ script.text({ pbody: true }) }} {{ bodyPrepend(true) }}
{{ noscript.text({ pbody: true }) }}
<a href="/">&larr; Examples index</a> <a href="/">&larr; Examples index</a>
{{ app }} {{ app }}
<script src="/__build__/ssr.js"></script> <script src="/__build__/ssr.js"></script>
{{ script.text({ body: true }) }} {{ bodyAppend(true) }}
{{ noscript.text({ body: true }) }}
</body> </body>
</html> </html>
+10 -7
View File
@@ -64,13 +64,6 @@ const router = new Router({
const App = { const App = {
router, router,
metaInfo () {
return {
meta: [
{ charset: 'utf=8' }
]
}
},
template: ` template: `
<div id="app"> <div id="app">
<h1>vue-router</h1> <h1>vue-router</h1>
@@ -86,7 +79,17 @@ const App = {
const app = new Vue(App) const app = new Vue(App)
const { set, remove } = app.$meta().addApp('custom')
set({
meta: [
{ charset: 'utf=8' }
]
})
setTimeout(() => remove(), 3000)
app.$mount('#app') app.$mount('#app')
/* /*
const waitFor = time => new Promise(r => setTimeout(r, time || 1000)) const waitFor = time => new Promise(r => setTimeout(r, time || 1000))
const o = { const o = {
+7 -7
View File
@@ -15,7 +15,8 @@ const banner = `/**
* (c) ${new Date().getFullYear()} * (c) ${new Date().getFullYear()}
* - Declan de Wet * - Declan de Wet
* - Sébastien Chopin (@Atinux) * - Sébastien Chopin (@Atinux)
* - All the amazing contributors * - Pim (@pimlie)
* - All the amazing contributors
* @license MIT * @license MIT
*/ */
` `
@@ -39,13 +40,16 @@ function rollupConfig({
...config ...config
}) { }) {
const isBrowserBuild = !config.output || !config.output.format || config.output.format === 'umd' || config.output.file.includes('.browser.')
const replaceConfig = { const replaceConfig = {
exclude: 'node_modules/**', exclude: 'node_modules/**',
delimiters: ['', ''], delimiters: ['', ''],
values: { values: {
// replaceConfig needs to have some values // replaceConfig needs to have some values
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true', 'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
'process.env.VERSION': `"${version}"` 'process.env.VERSION': `"${version}"`,
'process.server' : isBrowserBuild ? 'false' : 'true'
} }
} }
@@ -57,7 +61,7 @@ function rollupConfig({
}*/ }*/
return defaultsDeep({}, config, { return defaultsDeep({}, config, {
input: 'src/browser.js', input: 'src/index.js',
output: { output: {
name: 'VueMeta', name: 'VueMeta',
format: 'umd', format: 'umd',
@@ -92,7 +96,6 @@ export default [
}, },
// common js build // common js build
{ {
input: 'src/index.js',
output: { output: {
file: pkg.main, file: pkg.main,
format: 'cjs' format: 'cjs'
@@ -101,7 +104,6 @@ export default [
}, },
// esm build // esm build
{ {
input: 'src/index.js',
output: { output: {
file: pkg.web.replace('.js', '.esm.js'), file: pkg.web.replace('.js', '.esm.js'),
format: 'es' format: 'es'
@@ -110,7 +112,6 @@ export default [
}, },
// browser esm build // browser esm build
{ {
input: 'src/browser.js',
output: { output: {
file: pkg.web.replace('.js', '.esm.browser.js'), file: pkg.web.replace('.js', '.esm.browser.js'),
format: 'es' format: 'es'
@@ -119,7 +120,6 @@ export default [
}, },
// minimized browser esm build // minimized browser esm build
{ {
input: 'src/browser.js',
output: { output: {
file: pkg.web.replace('.js', '.esm.browser.min.js'), file: pkg.web.replace('.js', '.esm.browser.min.js'),
format: 'es' format: 'es'
-37
View File
@@ -1,37 +0,0 @@
import { version } from '../package.json'
import createMixin from './shared/mixin'
import { setOptions } from './shared/options'
import { isUndefined } from './utils/is-type'
import $meta from './client/$meta'
import { hasMetaInfo } from './shared/meta-helpers'
/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
function install (Vue, options = {}) {
if (Vue.__vuemeta_installed) {
return
}
Vue.__vuemeta_installed = true
options = setOptions(options)
Vue.prototype.$meta = function () {
return $meta.call(this, options)
}
Vue.mixin(createMixin(Vue, options))
}
// automatic install
if (!isUndefined(window) && !isUndefined(window.Vue)) {
/* istanbul ignore next */
install(window.Vue)
}
export default {
version,
install,
hasMetaInfo
}
-29
View File
@@ -1,29 +0,0 @@
import { showWarningNotSupported } from '../shared/log'
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from './refresh'
export default function $meta (options = {}) {
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
* @return {Object} - injector
*/
if (!this.$root._vueMeta) {
return {
getOptions: showWarningNotSupported,
refresh: showWarningNotSupported,
inject: showWarningNotSupported,
pause: showWarningNotSupported,
resume: showWarningNotSupported
}
}
return {
getOptions: () => getOptions(options),
refresh: () => refresh.call(this, options),
inject: () => {},
pause: () => pause.call(this),
resume: () => resume.call(this)
}
}
+32 -15
View File
@@ -1,26 +1,34 @@
import { clientSequences } from '../shared/escaping' import { clientSequences } from '../shared/escaping'
import { showWarningNotSupported } from '../shared/log'
import { getComponentMetaInfo } from '../shared/getComponentOption' import { getComponentMetaInfo } from '../shared/getComponentOption'
import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app'
import getMetaInfo from '../shared/getMetaInfo' import getMetaInfo from '../shared/getMetaInfo'
import { isFunction } from '../utils/is-type' import { isFunction } from '../utils/is-type'
import updateClientMetaInfo from './updateClientMetaInfo' import updateClientMetaInfo from './updateClientMetaInfo'
export default function refresh (options = {}) { /**
/** * When called, will update the current meta info with new meta info.
* When called, will update the current meta info with new meta info. * Useful when updating meta info as the result of an asynchronous
* Useful when updating meta info as the result of an asynchronous * action that resolves after the initial render takes place.
* action that resolves after the initial render takes place. *
* * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion
* Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion * to implement this method.
* to implement this method. *
* * @return {Object} - new meta info
* @return {Object} - new meta info */
*/ export default function refresh (vm, options = {}) {
// make sure vue-meta was initiated
if (!vm.$root._vueMeta) {
showWarningNotSupported()
return {}
}
// collect & aggregate all metaInfo $options // collect & aggregate all metaInfo $options
const rawInfo = getComponentMetaInfo(options, this.$root) const rawInfo = getComponentMetaInfo(options, vm.$root)
const metaInfo = getMetaInfo(options, rawInfo, clientSequences, this.$root) const metaInfo = getMetaInfo(options, rawInfo, clientSequences, vm.$root)
const appId = this.$root._vueMeta.appId const { appId } = vm.$root._vueMeta
const tags = updateClientMetaInfo(appId, options, metaInfo) const tags = updateClientMetaInfo(appId, options, metaInfo)
// emit "event" with new info // emit "event" with new info
@@ -28,5 +36,14 @@ export default function refresh (options = {}) {
metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags) metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags)
} }
return { vm: this, metaInfo, tags } const appsMetaInfo = getAppsMetaInfo()
if (appsMetaInfo) {
for (const additionalAppId in appsMetaInfo) {
updateClientMetaInfo(additionalAppId, options, appsMetaInfo[additionalAppId])
delete appsMetaInfo[additionalAppId]
}
clearAppsMetaInfo(true)
}
return { vm, metaInfo, tags }
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import { version } from '../package.json' import { version } from '../package.json'
import createMixin from './shared/mixin' import createMixin from './shared/mixin'
import { setOptions } from './shared/options' import { setOptions } from './shared/options'
import $meta from './server/$meta' import $meta from './shared/$meta'
import generate from './server/generate' import generate from './server/generate'
import { hasMetaInfo } from './shared/meta-helpers' import { hasMetaInfo } from './shared/meta-helpers'
@@ -27,6 +27,6 @@ function install (Vue, options = {}) {
export default { export default {
version, version,
install, install,
hasMetaInfo, generate: process.server ? generate : () => {},
generate hasMetaInfo
} }
-19
View File
@@ -1,19 +0,0 @@
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from '../client/refresh'
import inject from './inject'
export default function $meta (options = {}) {
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
* @return {Object} - injector
*/
return {
getOptions: () => getOptions(options),
refresh: () => refresh.call(this, options),
inject: () => inject.call(this, options),
pause: () => pause.call(this),
resume: () => resume.call(this)
}
}
+3 -1
View File
@@ -5,5 +5,7 @@ import generateServerInjector from './generateServerInjector'
export default function generate (rawInfo, options = {}) { export default function generate (rawInfo, options = {}) {
const metaInfo = getMetaInfo(setOptions(options), rawInfo, serverSequences) const metaInfo = getMetaInfo(setOptions(options), rawInfo, serverSequences)
return generateServerInjector(options, metaInfo)
const serverInjector = generateServerInjector(options, metaInfo)
return serverInjector.injectors
} }
+58 -11
View File
@@ -9,24 +9,71 @@ import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
* @return {Object} - the new injector * @return {Object} - the new injector
*/ */
export default function generateServerInjector (options, newInfo) { export default function generateServerInjector (options, metaInfo) {
const serverInjector = {
data: metaInfo,
extraData: undefined,
addInfo (appId, metaInfo) {
this.extraData = this.extraData || {}
this.extraData[appId] = metaInfo
},
callInjectors (opts) {
const m = this.injectors
// only call title for the head
return (opts.body || opts.pbody ? '' : m.title.text(opts)) +
m.meta.text(opts) +
m.link.text(opts) +
m.style.text(opts) +
m.script.text(opts) +
m.noscript.text(opts)
},
injectors: {
head: ln => serverInjector.callInjectors({ ln }),
bodyPrepend: ln => serverInjector.callInjectors({ ln, pbody: true }),
bodyAppend: ln => serverInjector.callInjectors({ ln, body: true })
}
}
for (const type in defaultInfo) { for (const type in defaultInfo) {
if (metaInfoOptionKeys.includes(type)) { if (metaInfoOptionKeys.includes(type)) {
continue continue
} }
if (type === 'title') { serverInjector.injectors[type] = {
newInfo[type] = titleGenerator(options, type, newInfo[type]) text (arg) {
continue if (type === 'title') {
} return titleGenerator(options, type, serverInjector.data[type], arg)
}
if (metaInfoAttributeKeys.includes(type)) { if (metaInfoAttributeKeys.includes(type)) {
newInfo[type] = attributeGenerator(options, type, newInfo[type]) let str = attributeGenerator(options, type, serverInjector.data[type], arg)
continue
}
newInfo[type] = tagGenerator(options, type, newInfo[type]) if (serverInjector.extraData) {
for (const appId in serverInjector.extraData) {
const data = serverInjector.extraData[appId][type]
const extraStr = attributeGenerator(options, type, data, arg)
str = `${str}${extraStr}`
}
}
return str
}
let str = tagGenerator(options, type, serverInjector.data[type], arg)
if (serverInjector.extraData) {
for (const appId in serverInjector.extraData) {
const data = serverInjector.extraData[appId][type]
const extraStr = tagGenerator(options, type, data, { appId, ...arg })
str = `${str}${extraStr}`
}
}
return str
}
}
} }
return newInfo return serverInjector
} }
+20 -24
View File
@@ -8,33 +8,29 @@ import { isUndefined, isArray } from '../../utils/is-type'
* @param {Object} data - the attributes to generate * @param {Object} data - the attributes to generate
* @return {Object} - the attribute generator * @return {Object} - the attribute generator
*/ */
export default function attributeGenerator ({ attribute, ssrAttribute } = {}, type, data) { export default function attributeGenerator ({ attribute, ssrAttribute } = {}, type, data, addSrrAttribute) {
return { let attributeStr = ''
text (addSrrAttribute) { const watchedAttrs = []
let attributeStr = ''
const watchedAttrs = []
for (const attr in data) { for (const attr in data) {
if (data.hasOwnProperty(attr)) { if (data.hasOwnProperty(attr)) {
watchedAttrs.push(attr) watchedAttrs.push(attr)
attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr)
? attr ? attr
: `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"` : `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"`
attributeStr += ' ' attributeStr += ' '
}
}
if (attributeStr) {
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
}
if (type === 'htmlAttrs' && addSrrAttribute) {
return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}`
}
return attributeStr
} }
} }
if (attributeStr) {
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
}
if (type === 'htmlAttrs' && addSrrAttribute) {
return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}`
}
return attributeStr
} }
+64 -67
View File
@@ -14,79 +14,76 @@ import {
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base * @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - the tag generator * @return {Object} - the tag generator
*/ */
export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}, type, tags) { export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}, type, tags, { appId, body = false, pbody = false, ln = false } = {}) {
const dataAttributes = [tagIDKeyName, ...commonDataAttributes] const dataAttributes = [tagIDKeyName, ...commonDataAttributes]
return { if (!tags || !tags.length) {
text ({ body = false, pbody = false } = {}) { return ''
if (!tags || !tags.length) { }
return ''
// build a string containing all tags of this type
return tags.reduce((tagsStr, tag) => {
if (tag.skip) {
return tagsStr
}
const tagKeys = Object.keys(tag)
if (tagKeys.length === 0) {
return tagsStr // Bail on empty tag object
}
if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) {
return tagsStr
}
let attrs = tag.once ? '' : ` ${attribute}="${appId || ssrAppId}"`
// build a string containing all attributes of this tag
for (const attr in tag) {
// these attributes are treated as children on the tag
if (tagAttributeAsInnerContent.includes(attr) || tagProperties.includes(attr)) {
continue
} }
// build a string containing all tags of this type if (attr === 'callback') {
return tags.reduce((tagsStr, tag) => { attrs += ` onload="this.__vm_l=1"`
if (tag.skip) { continue
return tagsStr }
}
const tagKeys = Object.keys(tag) // these form the attribute list for this tag
let prefix = ''
if (dataAttributes.includes(attr)) {
prefix = 'data-'
}
if (tagKeys.length === 0) { const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr)
return tagsStr // Bail on empty tag object if (isBooleanAttr && !tag[attr]) {
} continue
}
if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) { attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
return tagsStr
}
let attrs = tag.once ? '' : ` ${attribute}="${ssrAppId}"`
// build a string containing all attributes of this tag
for (const attr in tag) {
// these attributes are treated as children on the tag
if (tagAttributeAsInnerContent.includes(attr) || tagProperties.includes(attr)) {
continue
}
if (attr === 'callback') {
attrs += ` onload="this.__vm_l=1"`
continue
}
// these form the attribute list for this tag
let prefix = ''
if (dataAttributes.includes(attr)) {
prefix = 'data-'
}
const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr)
if (isBooleanAttr && !tag[attr]) {
continue
}
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
}
let json = ''
if (tag.json) {
json = JSON.stringify(tag.json)
}
// grab child content from one of these attributes, if possible
const content = tag.innerHTML || tag.cssText || json
// generate tag exactly without any other redundant attribute
// these tags have no end tag
const hasEndTag = !tagsWithoutEndTag.includes(type)
// these tag types will have content inserted
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
// the final string for this specific tag
return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` +
(hasContent ? `${content}</${type}>` : '')
}, '')
} }
}
let json = ''
if (tag.json) {
json = JSON.stringify(tag.json)
}
// grab child content from one of these attributes, if possible
const content = tag.innerHTML || tag.cssText || json
// generate tag exactly without any other redundant attribute
// these tags have no end tag
const hasEndTag = !tagsWithoutEndTag.includes(type)
// these tag types will have content inserted
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
// the final string for this specific tag
return `${tagsStr}<${type}${attrs}${!hasContent && hasEndTag ? '/' : ''}>` +
(hasContent ? `${content}</${type}>` : '') +
(ln ? '\n' : '')
}, '')
} }
+5 -8
View File
@@ -5,13 +5,10 @@
* @param {String} data - the title text * @param {String} data - the title text
* @return {Object} - the title generator * @return {Object} - the title generator
*/ */
export default function titleGenerator ({ attribute } = {}, type, data) { export default function titleGenerator ({ attribute } = {}, type, data, { ln } = {}) {
return { if (!data) {
text () { return ''
if (!data) {
return ''
}
return `<${type}>${data}</${type}>`
}
} }
return `<${type}>${data}</${type}>${ln ? '\n' : ''}`
} }
+30 -13
View File
@@ -1,24 +1,41 @@
import { serverSequences } from '../shared/escaping' import { serverSequences } from '../shared/escaping'
import { showWarningNotSupported } from '../shared/log'
import { getComponentMetaInfo } from '../shared/getComponentOption' import { getComponentMetaInfo } from '../shared/getComponentOption'
import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app'
import getMetaInfo from '../shared/getMetaInfo' import getMetaInfo from '../shared/getMetaInfo'
import generateServerInjector from './generateServerInjector' import generateServerInjector from './generateServerInjector'
export default function _inject (options = {}) { /**
/** * Converts the state of the meta info object such that each item
* Converts the state of the meta info object such that each item * can be compiled to a tag string on the server
* can be compiled to a tag string on the server *
* * @vm {Object} - Vue instance - ideally the root component
* @this {Object} - Vue instance - ideally the root component * @return {Object} - server meta info with `toString` methods
* @return {Object} - server meta info with `toString` methods */
*/ export default function inject (vm, options = {}) {
// make sure vue-meta was initiated
if (!vm.$root._vueMeta) {
showWarningNotSupported()
return {}
}
// collect & aggregate all metaInfo $options // collect & aggregate all metaInfo $options
const rawInfo = getComponentMetaInfo(options, this.$root) const rawInfo = getComponentMetaInfo(options, vm.$root)
const metaInfo = getMetaInfo(options, rawInfo, serverSequences, this.$root) const metaInfo = getMetaInfo(options, rawInfo, serverSequences, vm.$root)
// generate server injectors // generate server injector
generateServerInjector(options, metaInfo) const serverInjector = generateServerInjector(options, metaInfo)
return metaInfo // add meta info from additional apps
const appsMetaInfo = getAppsMetaInfo()
if (appsMetaInfo) {
for (const additionalAppId in appsMetaInfo) {
serverInjector.addInfo(additionalAppId, appsMetaInfo[additionalAppId])
delete appsMetaInfo[additionalAppId]
}
clearAppsMetaInfo(true)
}
return serverInjector.injectors
} }
+22
View File
@@ -0,0 +1,22 @@
import refresh from '../client/refresh'
import inject from '../server/inject'
import { showWarningNotSupported } from '../shared/log'
import { addApp } from './additional-app'
import { pause, resume } from './pausing'
import { getOptions } from './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 {
getOptions: () => getOptions(options),
refresh: () => refresh(this, options),
inject: () => process.server ? inject(this, options) : showWarningNotSupported(),
pause: () => pause(this),
resume: () => resume(this),
addApp: appId => addApp(this, appId, options)
}
}
+44
View File
@@ -0,0 +1,44 @@
import updateClientMetaInfo from '../client/updateClientMetaInfo'
import { removeElementsByAppId } from '../utils/elements'
let appsMetaInfo
export function addApp (vm, appId, options) {
return {
set: metaInfo => setMetaInfo(vm.$root, appId, options, metaInfo),
remove: () => removeMetaInfo(vm.$root, appId, options)
}
}
export function setMetaInfo (vm, appId, options, metaInfo) {
// if a vm exists _and_ its mounted then immediately update
if (vm && vm.$el) {
return updateClientMetaInfo(appId, options, metaInfo)
}
// store for later, the info
// will be set on the first refresh
appsMetaInfo = appsMetaInfo || {}
appsMetaInfo[appId] = metaInfo
}
export function removeMetaInfo (vm, appId, options) {
if (vm && vm.$el) {
return removeElementsByAppId(options, appId)
}
if (appsMetaInfo[appId]) {
delete appsMetaInfo[appId]
clearAppsMetaInfo()
}
}
export function getAppsMetaInfo () {
return appsMetaInfo
}
export function clearAppsMetaInfo (force) {
if (force || !Object.keys(appsMetaInfo).length) {
appsMetaInfo = undefined
}
}
+5 -5
View File
@@ -1,13 +1,13 @@
export function pause (refresh = true) { export function pause (vm, refresh = true) {
this.$root._vueMeta.paused = true vm.$root._vueMeta.paused = true
return () => resume(refresh) return () => resume(refresh)
} }
export function resume (refresh = true) { export function resume (vm, refresh = true) {
this.$root._vueMeta.paused = false vm.$root._vueMeta.paused = false
if (refresh) { if (refresh) {
return this.$root.$meta().refresh() return vm.$root.$meta().refresh()
} }
} }
+4
View File
@@ -29,3 +29,7 @@ export function queryElements (parentNode, { appId, attribute, type, tagIDKeyNam
return toArray(parentNode.querySelectorAll(queries.join(', '))) return toArray(parentNode.querySelectorAll(queries.join(', ')))
} }
export function removeElementsByAppId ({ attribute }, appId) {
toArray(document.querySelectorAll(`[${attribute}="${appId}"]`)).map(el => el.remove())
}
+2 -2
View File
@@ -146,7 +146,7 @@ describe('client', () => {
}) })
test('afterNavigation function is called with refreshOnce: true', async () => { test('afterNavigation function is called with refreshOnce: true', async () => {
const Vue = loadVueMetaPlugin(false, { refreshOnceOnNavigation: true }) const Vue = loadVueMetaPlugin({ refreshOnceOnNavigation: true })
const afterNavigation = jest.fn() const afterNavigation = jest.fn()
const component = Vue.component('nav-component', { const component = Vue.component('nav-component', {
render: h => h('div'), render: h => h('div'),
@@ -181,7 +181,7 @@ describe('client', () => {
}) })
test('afterNavigation function is called with refreshOnce: false', async () => { test('afterNavigation function is called with refreshOnce: false', async () => {
const Vue = loadVueMetaPlugin(false, { refreshOnceOnNavigation: false }) const Vue = loadVueMetaPlugin({ refreshOnceOnNavigation: false })
const afterNavigation = jest.fn() const afterNavigation = jest.fn()
const component = Vue.component('nav-component', { const component = Vue.component('nav-component', {
render: h => h('div'), render: h => h('div'),
+2 -2
View File
@@ -3,7 +3,7 @@ import { defaultOptions } from '../../src/shared/constants'
import metaInfoData from '../utils/meta-info-data' import metaInfoData from '../utils/meta-info-data'
import { titleGenerator } from '../../src/server/generators' import { titleGenerator } from '../../src/server/generators'
const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo) const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo).injectors
describe('generators', () => { describe('generators', () => {
for (const type in metaInfoData) { for (const type in metaInfoData) {
@@ -78,7 +78,7 @@ describe('extra tests', () => {
const title = null const title = null
const generatedTitle = titleGenerator({}, 'title', title) const generatedTitle = titleGenerator({}, 'title', title)
expect(generatedTitle.text()).toEqual('') expect(generatedTitle).toEqual('')
}) })
test('auto add ssrAttribute', () => { test('auto add ssrAttribute', () => {
+2 -2
View File
@@ -40,7 +40,7 @@ describe('getComponentOption', () => {
}) })
test('fetches deeply nested component options and merges them', () => { test('fetches deeply nested component options and merges them', () => {
const localVue = loadVueMetaPlugin(true, { keyName: 'foo' }) const localVue = loadVueMetaPlugin({ keyName: 'foo' })
localVue.component('merge-child', { render: h => h('div'), foo: { bar: 'baz' } }) localVue.component('merge-child', { render: h => h('div'), foo: { bar: 'baz' } })
const component = localVue.component('parent', { const component = localVue.component('parent', {
@@ -92,7 +92,7 @@ describe('getComponentOption', () => {
}) */ }) */
test('only traverses branches with metaInfo components', () => { test('only traverses branches with metaInfo components', () => {
const localVue = loadVueMetaPlugin(false, { keyName: 'foo' }) const localVue = loadVueMetaPlugin({ keyName: 'foo' })
localVue.component('meta-child', { localVue.component('meta-child', {
foo: { bar: 'baz' }, foo: { bar: 'baz' },
-108
View File
@@ -1,108 +0,0 @@
import { mount, VueMetaServerPlugin, loadVueMetaPlugin } from '../utils'
import { defaultOptions } from '../../src/shared/constants'
jest.mock('../../package.json', () => ({
version: 'test-version'
}))
describe('plugin', () => {
let Vue
beforeEach(() => jest.clearAllMocks())
beforeAll(() => (Vue = loadVueMetaPlugin()))
test('is loaded', () => {
const instance = new Vue({ metaInfo: {} })
expect(instance.$meta).toEqual(expect.any(Function))
expect(instance.$meta().inject).toEqual(expect.any(Function))
expect(instance.$meta().refresh).toEqual(expect.any(Function))
expect(instance.$meta().getOptions).toEqual(expect.any(Function))
expect(instance.$meta().inject()).toBeDefined()
expect(instance.$meta().refresh()).toBeDefined()
const options = instance.$meta().getOptions()
expect(options).toBeDefined()
expect(options.keyName).toBe(defaultOptions.keyName)
})
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(VueMetaServerPlugin.version).toBe('test-version')
})
test('plugin isnt be installed twice', () => {
expect(Vue.__vuemeta_installed).toBe(true)
Vue.prototype.$meta = undefined
Vue.use({ ...VueMetaServerPlugin })
expect(Vue.prototype.$meta).toBeUndefined()
// reset Vue
Vue = loadVueMetaPlugin(true)
})
test('prints deprecation warning once when using _hasMetaInfo', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
const Component = Vue.component('test-component', {
template: '<div>Test</div>',
[defaultOptions.keyName]: {
title: 'Hello World'
}
})
Vue.config.devtools = true
const { vm } = mount(Component, { localVue: Vue })
expect(vm._hasMetaInfo).toBe(true)
expect(warn).toHaveBeenCalledTimes(1)
expect(vm._hasMetaInfo).toBe(true)
expect(warn).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
test('can use hasMetaInfo export', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
const Component = Vue.component('test-component', {
template: '<div>Test</div>',
[defaultOptions.keyName]: {
title: 'Hello World'
}
})
const { vm } = mount(Component, { localVue: Vue })
expect(VueMetaServerPlugin.hasMetaInfo(vm)).toBe(true)
expect(warn).not.toHaveBeenCalled()
warn.mockRestore()
})
test('can use generate export', () => {
const rawInfo = {
meta: [{ charset: 'utf-8' }]
}
const metaInfo = VueMetaServerPlugin.generate(rawInfo)
expect(metaInfo.meta.text()).toBe('<meta data-vue-meta="ssr" charset="utf-8">')
// no error on not provided metaInfo types
expect(metaInfo.script.text()).toBe('')
})
})
@@ -1,5 +1,5 @@
import { triggerUpdate, batchUpdate } from '../../src/client/update' import { triggerUpdate, batchUpdate } from '../../src/client/update'
import { mount, vmTick, VueMetaBrowserPlugin, loadVueMetaPlugin } from '../utils' import { mount, vmTick, VueMetaPlugin, loadVueMetaPlugin } from '../utils'
import { defaultOptions } from '../../src/shared/constants' import { defaultOptions } from '../../src/shared/constants'
jest.mock('../../src/client/update') jest.mock('../../src/client/update')
@@ -15,6 +15,7 @@ describe('plugin', () => {
test('not loaded when no metaInfo defined', () => { test('not loaded when no metaInfo defined', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}) const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
process.server = false
const instance = new Vue() const instance = new Vue()
expect(instance.$meta).toEqual(expect.any(Function)) expect(instance.$meta).toEqual(expect.any(Function))
@@ -23,14 +24,16 @@ describe('plugin', () => {
expect(instance.$meta().refresh).toEqual(expect.any(Function)) expect(instance.$meta().refresh).toEqual(expect.any(Function))
expect(instance.$meta().getOptions).toEqual(expect.any(Function)) expect(instance.$meta().getOptions).toEqual(expect.any(Function))
expect(instance.$meta().inject()).not.toBeDefined() expect(instance.$meta().inject()).toBeUndefined()
expect(warn).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledTimes(1)
expect(instance.$meta().refresh()).not.toBeDefined() expect(instance.$meta().refresh()).toEqual({})
expect(warn).toHaveBeenCalledTimes(2) expect(warn).toHaveBeenCalledTimes(2)
instance.$meta().getOptions() instance.$meta().getOptions()
expect(warn).toHaveBeenCalledTimes(3) expect(warn).toHaveBeenCalledTimes(2)
warn.mockRestore() warn.mockRestore()
delete process.server
}) })
test('is loaded', () => { test('is loaded', () => {
@@ -62,14 +65,14 @@ describe('plugin', () => {
}) })
test('plugin sets package version', () => { test('plugin sets package version', () => {
expect(VueMetaBrowserPlugin.version).toBe('test-version') expect(VueMetaPlugin.version).toBe('test-version')
}) })
test('plugin isnt be installed twice', () => { test('plugin isnt be installed twice', () => {
expect(Vue.__vuemeta_installed).toBe(true) expect(Vue.__vuemeta_installed).toBe(true)
Vue.prototype.$meta = undefined Vue.prototype.$meta = undefined
Vue.use({ ...VueMetaBrowserPlugin }) Vue.use({ ...VueMetaPlugin })
expect(Vue.prototype.$meta).toBeUndefined() expect(Vue.prototype.$meta).toBeUndefined()
@@ -77,6 +80,57 @@ describe('plugin', () => {
Vue = loadVueMetaPlugin(true) Vue = loadVueMetaPlugin(true)
}) })
test('prints deprecation warning once when using _hasMetaInfo', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
const Component = Vue.component('test-component', {
template: '<div>Test</div>',
[defaultOptions.keyName]: {
title: 'Hello World'
}
})
Vue.config.devtools = true
const { vm } = mount(Component, { localVue: Vue })
expect(vm._hasMetaInfo).toBe(true)
expect(warn).toHaveBeenCalledTimes(1)
expect(vm._hasMetaInfo).toBe(true)
expect(warn).toHaveBeenCalledTimes(1)
warn.mockRestore()
})
test('can use hasMetaInfo export', () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
const Component = Vue.component('test-component', {
template: '<div>Test</div>',
[defaultOptions.keyName]: {
title: 'Hello World'
}
})
const { vm } = mount(Component, { localVue: Vue })
expect(VueMetaPlugin.hasMetaInfo(vm)).toBe(true)
expect(warn).not.toHaveBeenCalled()
warn.mockRestore()
})
test('can use generate export', () => {
const rawInfo = {
meta: [{ charset: 'utf-8' }]
}
const metaInfo = VueMetaPlugin.generate(rawInfo)
expect(metaInfo.meta.text()).toBe('<meta data-vue-meta="ssr" charset="utf-8">')
// no error on not provided metaInfo types
expect(metaInfo.script.text()).toBe('')
})
test('updates can be paused and resumed', async () => { test('updates can be paused and resumed', async () => {
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update') const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate) const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
+2 -1
View File
@@ -22,7 +22,8 @@ export function getVueMetaPath (browser) {
return path.resolve(__dirname, `../..${browser ? '/dist/vue-meta.js' : ''}`) return path.resolve(__dirname, `../..${browser ? '/dist/vue-meta.js' : ''}`)
} }
return path.resolve(__dirname, `../../src${browser ? '/browser' : ''}`) process.server = !browser
return path.resolve(__dirname, `../../src`)
} }
export function webpackRun (config) { export function webpackRun (config) {
+4 -10
View File
@@ -2,28 +2,22 @@ import { JSDOM } from 'jsdom'
import { mount, shallowMount, createWrapper, createLocalVue } from '@vue/test-utils' import { mount, shallowMount, createWrapper, createLocalVue } from '@vue/test-utils'
import { renderToString } from '@vue/server-test-utils' import { renderToString } from '@vue/server-test-utils'
import { defaultOptions } from '../../src/shared/constants' import { defaultOptions } from '../../src/shared/constants'
import VueMetaBrowserPlugin from '../../src/browser' import VueMetaPlugin from '../../src'
import VueMetaServerPlugin from '../../src'
export { export {
mount, mount,
shallowMount, shallowMount,
createWrapper, createWrapper,
renderToString, renderToString,
VueMetaBrowserPlugin, VueMetaPlugin
VueMetaServerPlugin
} }
export function getVue () { export function getVue () {
return createLocalVue() return createLocalVue()
} }
export function loadVueMetaPlugin (browser, options, localVue = getVue()) { export function loadVueMetaPlugin (options, localVue = getVue()) {
if (browser) { localVue.use(VueMetaPlugin, Object.assign({}, defaultOptions, options))
localVue.use(VueMetaBrowserPlugin, Object.assign({}, defaultOptions, options))
} else {
localVue.use(VueMetaServerPlugin, Object.assign({}, defaultOptions, options))
}
return localVue return localVue
} }
+2
View File
@@ -1,2 +1,4 @@
process.server = true
jest.useFakeTimers() jest.useFakeTimers()
jest.setTimeout(30000) jest.setTimeout(30000)