diff --git a/examples/ssr/App.js b/examples/ssr/App.js index ae55f94..3ac118d 100644 --- a/examples/ssr/App.js +++ b/examples/ssr/App.js @@ -138,5 +138,10 @@ export default function createApp () { ` }) + const { set } = app.$meta().addApp('custom') + set({ + meta: [{ charset: 'utf-8' }] + }) + return { app, router } } diff --git a/examples/ssr/app.template.html b/examples/ssr/app.template.html index 396a873..10a996c 100644 --- a/examples/ssr/app.template.html +++ b/examples/ssr/app.template.html @@ -1,23 +1,16 @@ - {{ title.text() }} - {{ meta.text() }} + {{ head(true) }} - {{ link.text() }} - {{ style.text() }} - {{ script.text() }} - {{ noscript.text() }} - {{ script.text({ pbody: true }) }} - {{ noscript.text({ pbody: true }) }} + {{ bodyPrepend(true) }} ← Examples index {{ app }} - {{ script.text({ body: true }) }} - {{ noscript.text({ body: true }) }} + {{ bodyAppend(true) }} diff --git a/examples/vue-router/app.js b/examples/vue-router/app.js index 1e2a5f2..2b5c0f5 100644 --- a/examples/vue-router/app.js +++ b/examples/vue-router/app.js @@ -64,13 +64,6 @@ const router = new Router({ const App = { router, - metaInfo () { - return { - meta: [ - { charset: 'utf=8' } - ] - } - }, template: `

vue-router

@@ -86,7 +79,17 @@ const App = { const app = new Vue(App) +const { set, remove } = app.$meta().addApp('custom') + +set({ + meta: [ + { charset: 'utf=8' } + ] +}) +setTimeout(() => remove(), 3000) + app.$mount('#app') + /* const waitFor = time => new Promise(r => setTimeout(r, time || 1000)) const o = { diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js index d735e5b..3c00656 100644 --- a/scripts/rollup.config.js +++ b/scripts/rollup.config.js @@ -15,7 +15,8 @@ const banner = `/** * (c) ${new Date().getFullYear()} * - Declan de Wet * - Sébastien Chopin (@Atinux) - * - All the amazing contributors + * - Pim (@pimlie) + * - All the amazing contributors * @license MIT */ ` @@ -39,13 +40,16 @@ function rollupConfig({ ...config }) { + const isBrowserBuild = !config.output || !config.output.format || config.output.format === 'umd' || config.output.file.includes('.browser.') + const replaceConfig = { exclude: 'node_modules/**', delimiters: ['', ''], values: { // replaceConfig needs to have some values '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, { - input: 'src/browser.js', + input: 'src/index.js', output: { name: 'VueMeta', format: 'umd', @@ -92,7 +96,6 @@ export default [ }, // common js build { - input: 'src/index.js', output: { file: pkg.main, format: 'cjs' @@ -101,7 +104,6 @@ export default [ }, // esm build { - input: 'src/index.js', output: { file: pkg.web.replace('.js', '.esm.js'), format: 'es' @@ -110,7 +112,6 @@ export default [ }, // browser esm build { - input: 'src/browser.js', output: { file: pkg.web.replace('.js', '.esm.browser.js'), format: 'es' @@ -119,7 +120,6 @@ export default [ }, // minimized browser esm build { - input: 'src/browser.js', output: { file: pkg.web.replace('.js', '.esm.browser.min.js'), format: 'es' diff --git a/src/browser.js b/src/browser.js deleted file mode 100644 index 87f2c1f..0000000 --- a/src/browser.js +++ /dev/null @@ -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 -} diff --git a/src/client/$meta.js b/src/client/$meta.js deleted file mode 100644 index f5d38c5..0000000 --- a/src/client/$meta.js +++ /dev/null @@ -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) - } -} diff --git a/src/client/refresh.js b/src/client/refresh.js index 8f1ce0b..f4885ae 100644 --- a/src/client/refresh.js +++ b/src/client/refresh.js @@ -1,26 +1,34 @@ import { clientSequences } from '../shared/escaping' +import { showWarningNotSupported } from '../shared/log' import { getComponentMetaInfo } from '../shared/getComponentOption' +import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app' import getMetaInfo from '../shared/getMetaInfo' import { isFunction } from '../utils/is-type' import updateClientMetaInfo from './updateClientMetaInfo' -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 - * action that resolves after the initial render takes place. - * - * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion - * to implement this method. - * - * @return {Object} - 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 + * action that resolves after the initial render takes place. + * + * Credit to [Sébastien Chopin](https://github.com/Atinux) for the suggestion + * to implement this method. + * + * @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 - 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) // emit "event" with new info @@ -28,5 +36,14 @@ export default function refresh (options = {}) { 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 } } diff --git a/src/index.js b/src/index.js index 7cc601f..0ce148c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import { version } from '../package.json' import createMixin from './shared/mixin' import { setOptions } from './shared/options' -import $meta from './server/$meta' +import $meta from './shared/$meta' import generate from './server/generate' import { hasMetaInfo } from './shared/meta-helpers' @@ -27,6 +27,6 @@ function install (Vue, options = {}) { export default { version, install, - hasMetaInfo, - generate + generate: process.server ? generate : () => {}, + hasMetaInfo } diff --git a/src/server/$meta.js b/src/server/$meta.js deleted file mode 100644 index 76e040a..0000000 --- a/src/server/$meta.js +++ /dev/null @@ -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) - } -} diff --git a/src/server/generate.js b/src/server/generate.js index f3210f1..90b4027 100644 --- a/src/server/generate.js +++ b/src/server/generate.js @@ -5,5 +5,7 @@ import generateServerInjector from './generateServerInjector' export default function generate (rawInfo, options = {}) { const metaInfo = getMetaInfo(setOptions(options), rawInfo, serverSequences) - return generateServerInjector(options, metaInfo) + + const serverInjector = generateServerInjector(options, metaInfo) + return serverInjector.injectors } diff --git a/src/server/generateServerInjector.js b/src/server/generateServerInjector.js index 25d3fcb..2adbca8 100644 --- a/src/server/generateServerInjector.js +++ b/src/server/generateServerInjector.js @@ -9,24 +9,71 @@ import { titleGenerator, attributeGenerator, tagGenerator } from './generators' * @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) { if (metaInfoOptionKeys.includes(type)) { continue } - if (type === 'title') { - newInfo[type] = titleGenerator(options, type, newInfo[type]) - continue - } + serverInjector.injectors[type] = { + text (arg) { + if (type === 'title') { + return titleGenerator(options, type, serverInjector.data[type], arg) + } - if (metaInfoAttributeKeys.includes(type)) { - newInfo[type] = attributeGenerator(options, type, newInfo[type]) - continue - } + if (metaInfoAttributeKeys.includes(type)) { + let str = attributeGenerator(options, type, serverInjector.data[type], arg) - 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 } diff --git a/src/server/generators/attribute.js b/src/server/generators/attribute.js index ebfc6aa..a79049a 100644 --- a/src/server/generators/attribute.js +++ b/src/server/generators/attribute.js @@ -8,33 +8,29 @@ import { isUndefined, isArray } from '../../utils/is-type' * @param {Object} data - the attributes to generate * @return {Object} - the attribute generator */ -export default function attributeGenerator ({ attribute, ssrAttribute } = {}, type, data) { - return { - text (addSrrAttribute) { - let attributeStr = '' - const watchedAttrs = [] +export default function attributeGenerator ({ attribute, ssrAttribute } = {}, type, data, addSrrAttribute) { + let attributeStr = '' + const watchedAttrs = [] - for (const attr in data) { - if (data.hasOwnProperty(attr)) { - watchedAttrs.push(attr) + for (const attr in data) { + if (data.hasOwnProperty(attr)) { + watchedAttrs.push(attr) - attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) - ? attr - : `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"` + attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr) + ? attr + : `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"` - attributeStr += ' ' - } - } - - if (attributeStr) { - attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"` - } - - if (type === 'htmlAttrs' && addSrrAttribute) { - return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}` - } - - return attributeStr + attributeStr += ' ' } } + + if (attributeStr) { + attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"` + } + + if (type === 'htmlAttrs' && addSrrAttribute) { + return `${ssrAttribute}${attributeStr ? ' ' : ''}${attributeStr}` + } + + return attributeStr } diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index a5c6245..e4ed12d 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -14,79 +14,76 @@ import { * @param {(Array|Object)} tags - an array of tag objects or a single object in case of base * @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] - return { - text ({ body = false, pbody = false } = {}) { - if (!tags || !tags.length) { - 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 - return tags.reduce((tagsStr, tag) => { - if (tag.skip) { - return tagsStr - } + if (attr === 'callback') { + attrs += ` onload="this.__vm_l=1"` + continue + } - 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) { - return tagsStr // Bail on empty tag object - } + const isBooleanAttr = !prefix && booleanHtmlAttributes.includes(attr) + if (isBooleanAttr && !tag[attr]) { + continue + } - if (Boolean(tag.body) !== body || Boolean(tag.pbody) !== pbody) { - 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}` : '') - }, '') + 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}` : '') + + (ln ? '\n' : '') + }, '') } diff --git a/src/server/generators/title.js b/src/server/generators/title.js index f1370e1..645b721 100644 --- a/src/server/generators/title.js +++ b/src/server/generators/title.js @@ -5,13 +5,10 @@ * @param {String} data - the title text * @return {Object} - the title generator */ -export default function titleGenerator ({ attribute } = {}, type, data) { - return { - text () { - if (!data) { - return '' - } - return `<${type}>${data}` - } +export default function titleGenerator ({ attribute } = {}, type, data, { ln } = {}) { + if (!data) { + return '' } + + return `<${type}>${data}${ln ? '\n' : ''}` } diff --git a/src/server/inject.js b/src/server/inject.js index 4ee5ad0..49f00bd 100644 --- a/src/server/inject.js +++ b/src/server/inject.js @@ -1,24 +1,41 @@ import { serverSequences } from '../shared/escaping' +import { showWarningNotSupported } from '../shared/log' import { getComponentMetaInfo } from '../shared/getComponentOption' +import { getAppsMetaInfo, clearAppsMetaInfo } from '../shared/additional-app' import getMetaInfo from '../shared/getMetaInfo' import generateServerInjector from './generateServerInjector' -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 - * - * @this {Object} - Vue instance - ideally the root component - * @return {Object} - server meta info with `toString` methods - */ +/** + * Converts the state of the meta info object such that each item + * can be compiled to a tag string on the server + * + * @vm {Object} - Vue instance - ideally the root component + * @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 - 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 - generateServerInjector(options, metaInfo) + // generate server injector + 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 } diff --git a/src/shared/$meta.js b/src/shared/$meta.js new file mode 100644 index 0000000..6df77b1 --- /dev/null +++ b/src/shared/$meta.js @@ -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) + } +} diff --git a/src/shared/additional-app.js b/src/shared/additional-app.js new file mode 100644 index 0000000..19c368c --- /dev/null +++ b/src/shared/additional-app.js @@ -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 + } +} diff --git a/src/shared/pausing.js b/src/shared/pausing.js index 44253cd..a2b0231 100644 --- a/src/shared/pausing.js +++ b/src/shared/pausing.js @@ -1,13 +1,13 @@ -export function pause (refresh = true) { - this.$root._vueMeta.paused = true +export function pause (vm, refresh = true) { + vm.$root._vueMeta.paused = true return () => resume(refresh) } -export function resume (refresh = true) { - this.$root._vueMeta.paused = false +export function resume (vm, refresh = true) { + vm.$root._vueMeta.paused = false if (refresh) { - return this.$root.$meta().refresh() + return vm.$root.$meta().refresh() } } diff --git a/src/utils/elements.js b/src/utils/elements.js index ce12af8..e92b055 100644 --- a/src/utils/elements.js +++ b/src/utils/elements.js @@ -29,3 +29,7 @@ export function queryElements (parentNode, { appId, attribute, type, tagIDKeyNam return toArray(parentNode.querySelectorAll(queries.join(', '))) } + +export function removeElementsByAppId ({ attribute }, appId) { + toArray(document.querySelectorAll(`[${attribute}="${appId}"]`)).map(el => el.remove()) +} diff --git a/test/unit/components.test.js b/test/unit/components.test.js index f5741b6..bc02344 100644 --- a/test/unit/components.test.js +++ b/test/unit/components.test.js @@ -146,7 +146,7 @@ describe('client', () => { }) 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 component = Vue.component('nav-component', { render: h => h('div'), @@ -181,7 +181,7 @@ describe('client', () => { }) 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 component = Vue.component('nav-component', { render: h => h('div'), diff --git a/test/unit/generators.test.js b/test/unit/generators.test.js index 0a81533..cc27be4 100644 --- a/test/unit/generators.test.js +++ b/test/unit/generators.test.js @@ -3,7 +3,7 @@ import { defaultOptions } from '../../src/shared/constants' import metaInfoData from '../utils/meta-info-data' import { titleGenerator } from '../../src/server/generators' -const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo) +const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo).injectors describe('generators', () => { for (const type in metaInfoData) { @@ -78,7 +78,7 @@ describe('extra tests', () => { const title = null const generatedTitle = titleGenerator({}, 'title', title) - expect(generatedTitle.text()).toEqual('') + expect(generatedTitle).toEqual('') }) test('auto add ssrAttribute', () => { diff --git a/test/unit/getComponentOptions.test.js b/test/unit/getComponentOptions.test.js index a4e73e4..d902d0d 100644 --- a/test/unit/getComponentOptions.test.js +++ b/test/unit/getComponentOptions.test.js @@ -40,7 +40,7 @@ describe('getComponentOption', () => { }) 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' } }) const component = localVue.component('parent', { @@ -92,7 +92,7 @@ describe('getComponentOption', () => { }) */ test('only traverses branches with metaInfo components', () => { - const localVue = loadVueMetaPlugin(false, { keyName: 'foo' }) + const localVue = loadVueMetaPlugin({ keyName: 'foo' }) localVue.component('meta-child', { foo: { bar: 'baz' }, diff --git a/test/unit/plugin-server.test.js b/test/unit/plugin-server.test.js deleted file mode 100644 index df108e3..0000000 --- a/test/unit/plugin-server.test.js +++ /dev/null @@ -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: '
Test
', - [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: '
Test
', - [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: '
Test
', - [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('') - - // no error on not provided metaInfo types - expect(metaInfo.script.text()).toBe('') - }) -}) diff --git a/test/unit/plugin-browser.test.js b/test/unit/plugin.test.js similarity index 76% rename from test/unit/plugin-browser.test.js rename to test/unit/plugin.test.js index 9115455..ba519b5 100644 --- a/test/unit/plugin-browser.test.js +++ b/test/unit/plugin.test.js @@ -1,5 +1,5 @@ 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' jest.mock('../../src/client/update') @@ -15,6 +15,7 @@ describe('plugin', () => { test('not loaded when no metaInfo defined', () => { const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}) + process.server = false const instance = new Vue() expect(instance.$meta).toEqual(expect.any(Function)) @@ -23,14 +24,16 @@ describe('plugin', () => { expect(instance.$meta().refresh).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(instance.$meta().refresh()).not.toBeDefined() + expect(instance.$meta().refresh()).toEqual({}) expect(warn).toHaveBeenCalledTimes(2) instance.$meta().getOptions() - expect(warn).toHaveBeenCalledTimes(3) + expect(warn).toHaveBeenCalledTimes(2) + warn.mockRestore() + delete process.server }) test('is loaded', () => { @@ -62,14 +65,14 @@ describe('plugin', () => { }) test('plugin sets package version', () => { - expect(VueMetaBrowserPlugin.version).toBe('test-version') + expect(VueMetaPlugin.version).toBe('test-version') }) test('plugin isnt be installed twice', () => { expect(Vue.__vuemeta_installed).toBe(true) Vue.prototype.$meta = undefined - Vue.use({ ...VueMetaBrowserPlugin }) + Vue.use({ ...VueMetaPlugin }) expect(Vue.prototype.$meta).toBeUndefined() @@ -77,6 +80,57 @@ describe('plugin', () => { 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: '
Test
', + [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: '
Test
', + [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('') + + // no error on not provided metaInfo types + expect(metaInfo.script.text()).toBe('') + }) + test('updates can be paused and resumed', async () => { const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update') const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate) diff --git a/test/utils/build.js b/test/utils/build.js index 532af1e..8daa539 100644 --- a/test/utils/build.js +++ b/test/utils/build.js @@ -22,7 +22,8 @@ export function getVueMetaPath (browser) { 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) { diff --git a/test/utils/index.js b/test/utils/index.js index 300cb96..a8bd4b2 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -2,28 +2,22 @@ import { JSDOM } from 'jsdom' import { mount, shallowMount, createWrapper, createLocalVue } from '@vue/test-utils' import { renderToString } from '@vue/server-test-utils' import { defaultOptions } from '../../src/shared/constants' -import VueMetaBrowserPlugin from '../../src/browser' -import VueMetaServerPlugin from '../../src' +import VueMetaPlugin from '../../src' export { mount, shallowMount, createWrapper, renderToString, - VueMetaBrowserPlugin, - VueMetaServerPlugin + VueMetaPlugin } export function getVue () { return createLocalVue() } -export function loadVueMetaPlugin (browser, options, localVue = getVue()) { - if (browser) { - localVue.use(VueMetaBrowserPlugin, Object.assign({}, defaultOptions, options)) - } else { - localVue.use(VueMetaServerPlugin, Object.assign({}, defaultOptions, options)) - } +export function loadVueMetaPlugin (options, localVue = getVue()) { + localVue.use(VueMetaPlugin, Object.assign({}, defaultOptions, options)) return localVue } diff --git a/test/utils/setup.js b/test/utils/setup.js index 6b9103d..4ff9b72 100644 --- a/test/utils/setup.js +++ b/test/utils/setup.js @@ -1,2 +1,4 @@ +process.server = true + jest.useFakeTimers() jest.setTimeout(30000)