From cb2758eb0ffd6ba867217d375c084df304314386 Mon Sep 17 00:00:00 2001 From: pimlie Date: Sun, 28 Jul 2019 16:13:58 +0200 Subject: [PATCH] feat: support generating tags directly from metaInfo object --- scripts/rollup.config.js | 2 ++ src/client/refresh.js | 9 ++++++-- src/index.js | 4 +++- src/server/generate.js | 15 +++++++++++++ src/server/generateServerInjector.js | 28 ++++++++++++++++-------- src/server/generators/tag.js | 4 ++++ src/server/inject.js | 16 ++++++-------- src/shared/escaping.js | 22 +++++++++++++++++++ src/shared/getComponentOption.js | 7 +++++- src/shared/getMetaInfo.js | 31 +++------------------------ test/unit/components.test.js | 3 ++- test/unit/escaping.test.js | 3 ++- test/unit/generators.test.js | 30 ++++++++++++++++++-------- test/unit/getComponentOptions.test.js | 2 +- test/unit/getMetaInfo.test.js | 3 ++- test/unit/plugin-server.test.js | 12 +++++++++++ test/utils/meta-info-data.js | 7 ------ 17 files changed, 128 insertions(+), 70 deletions(-) create mode 100644 src/server/generate.js diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js index 69f7463..f46d4d6 100644 --- a/scripts/rollup.config.js +++ b/scripts/rollup.config.js @@ -21,6 +21,8 @@ const banner = `/** const babelConfig = () => ({ presets: [ ['@babel/preset-env', { + /*useBuiltIns: 'usage', + corejs: 2,*/ targets: { node: 8, ie: 9, diff --git a/src/client/refresh.js b/src/client/refresh.js index 76dcadd..06384d9 100644 --- a/src/client/refresh.js +++ b/src/client/refresh.js @@ -1,6 +1,7 @@ +import { clientSequences } from '../shared/escaping' +import { getComponentMetaInfo } from '../shared/getComponentOption' import getMetaInfo from '../shared/getMetaInfo' import { isFunction } from '../utils/is-type' -import { clientSequences } from '../shared/escaping' import updateClientMetaInfo from './updateClientMetaInfo' export default function _refresh (options = {}) { @@ -15,10 +16,14 @@ export default function _refresh (options = {}) { * @return {Object} - new meta info */ return function refresh () { - const metaInfo = getMetaInfo(options, this.$root, clientSequences) + // collect & aggregate all metaInfo $options + const rawInfo = getComponentMetaInfo(options, this.$root) + + const metaInfo = getMetaInfo(options, rawInfo, clientSequences, this.$root) const appId = this.$root._vueMeta.appId const tags = updateClientMetaInfo(appId, options, metaInfo) + // emit "event" with new info if (tags && isFunction(metaInfo.changed)) { metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags) diff --git a/src/index.js b/src/index.js index 275435a..9dabb38 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import { version } from '../package.json' import createMixin from './shared/mixin' import { setOptions } from './shared/options' import $meta from './server/$meta' +import generate from './server/generate' import { hasMetaInfo } from './shared/meta-helpers' /** @@ -24,5 +25,6 @@ function install (Vue, options = {}) { export default { version, install, - hasMetaInfo + hasMetaInfo, + generate } diff --git a/src/server/generate.js b/src/server/generate.js new file mode 100644 index 0000000..dc6fe14 --- /dev/null +++ b/src/server/generate.js @@ -0,0 +1,15 @@ +import getMetaInfo from '../shared/getMetaInfo' +import { defaultOptions } from '../shared/constants' +import { serverSequences } from '../shared/escaping' +import { setOptions } from '../shared/options' +import generateServerInjector from './generateServerInjector' + +export default function generate (options, rawInfo) { + if (arguments.length === 1) { + rawInfo = options + options = defaultOptions + } + + const metaInfo = getMetaInfo(setOptions(options), rawInfo, serverSequences) + return generateServerInjector(options, metaInfo) +} diff --git a/src/server/generateServerInjector.js b/src/server/generateServerInjector.js index a269330..25d3fcb 100644 --- a/src/server/generateServerInjector.js +++ b/src/server/generateServerInjector.js @@ -1,4 +1,4 @@ -import { metaInfoAttributeKeys } from '../shared/constants' +import { metaInfoOptionKeys, metaInfoAttributeKeys, defaultInfo } from '../shared/constants' import { titleGenerator, attributeGenerator, tagGenerator } from './generators' /** @@ -9,14 +9,24 @@ import { titleGenerator, attributeGenerator, tagGenerator } from './generators' * @return {Object} - the new injector */ -export default function generateServerInjector (options, type, data) { - if (type === 'title') { - return titleGenerator(options, type, data) +export default function generateServerInjector (options, newInfo) { + for (const type in defaultInfo) { + if (metaInfoOptionKeys.includes(type)) { + continue + } + + if (type === 'title') { + newInfo[type] = titleGenerator(options, type, newInfo[type]) + continue + } + + if (metaInfoAttributeKeys.includes(type)) { + newInfo[type] = attributeGenerator(options, type, newInfo[type]) + continue + } + + newInfo[type] = tagGenerator(options, type, newInfo[type]) } - if (metaInfoAttributeKeys.includes(type)) { - return attributeGenerator(options, type, data) - } - - return tagGenerator(options, type, data) + return newInfo } diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index dfbe826..b6da8a1 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -18,6 +18,10 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {} return { text ({ body = false, pbody = false } = {}) { + if (!tags || !tags.length) { + return '' + } + // build a string containing all tags of this type return tags.reduce((tagsStr, tag) => { if (tag.skip) { diff --git a/src/server/inject.js b/src/server/inject.js index 5a1897f..041d8c1 100644 --- a/src/server/inject.js +++ b/src/server/inject.js @@ -1,6 +1,6 @@ -import getMetaInfo from '../shared/getMetaInfo' -import { metaInfoOptionKeys } from '../shared/constants' import { serverSequences } from '../shared/escaping' +import { getComponentMetaInfo } from '../shared/getComponentOption' +import getMetaInfo from '../shared/getMetaInfo' import generateServerInjector from './generateServerInjector' export default function _inject (options = {}) { @@ -12,15 +12,13 @@ export default function _inject (options = {}) { * @return {Object} - server meta info with `toString` methods */ return function inject () { - // get meta info with sensible defaults - const metaInfo = getMetaInfo(options, this.$root, serverSequences) + // collect & aggregate all metaInfo $options + const rawInfo = getComponentMetaInfo(options, this.$root) + + const metaInfo = getMetaInfo(options, rawInfo, serverSequences, this.$root) // generate server injectors - for (const key in metaInfo) { - if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) { - metaInfo[key] = generateServerInjector(options, key, metaInfo[key]) - } - } + generateServerInjector(options, metaInfo) return metaInfo } diff --git a/src/shared/escaping.js b/src/shared/escaping.js index 1c68513..0d3da3b 100644 --- a/src/shared/escaping.js +++ b/src/shared/escaping.js @@ -1,5 +1,6 @@ import { isString, isArray, isPureObject } from '../utils/is-type' import { includes } from '../utils/array' +import { ensureIsArray } from '../utils/ensure' import { metaInfoOptionKeys, disableOptionKeys } from './constants' export const serverSequences = [ @@ -78,3 +79,24 @@ export function escape (info, options, escapeOptions, escapeKeys) { return escaped } + +export function escapeMetaInfo (options, info, escapeSequences = []) { + const escapeOptions = { + doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value) + } + + disableOptionKeys.forEach((disableKey, index) => { + if (index === 0) { + ensureIsArray(info, disableKey) + } else if (index === 1) { + for (const key in info[disableKey]) { + ensureIsArray(info[disableKey], key) + } + } + + escapeOptions[disableKey] = info[disableKey] + }) + + // begin sanitization + return escape(info, options, escapeOptions) +} diff --git a/src/shared/getComponentOption.js b/src/shared/getComponentOption.js index 27d8b3f..ecd4e07 100644 --- a/src/shared/getComponentOption.js +++ b/src/shared/getComponentOption.js @@ -1,9 +1,14 @@ import { isFunction, isObject } from '../utils/is-type' import { findIndex } from '../utils/array' +import { defaultInfo } from './constants' import { merge } from './merge' import { applyTemplate } from './template' import { inMetaInfoBranch } from './meta-helpers' +export function getComponentMetaInfo (options = {}, component) { + return getComponentOption(options, component, defaultInfo) +} + /** * Returns the `opts.option` $option value of the given `opts.component`. * If methods are encountered, they will be bound to the component context. @@ -18,7 +23,7 @@ import { inMetaInfoBranch } from './meta-helpers' * @param {Object} [result={}] - result so far * @return {Object} result - final aggregated result */ -export default function getComponentOption (options = {}, component, result = {}) { +export function getComponentOption (options = {}, component, result = {}) { const { keyName, metaTemplateKeyName, tagIDKeyName } = options const { $options, $children } = component diff --git a/src/shared/getMetaInfo.js b/src/shared/getMetaInfo.js index 5be688a..e5d27e7 100644 --- a/src/shared/getMetaInfo.js +++ b/src/shared/getMetaInfo.js @@ -1,8 +1,5 @@ -import { ensureIsArray } from '../utils/ensure' +import { escapeMetaInfo } from '../shared/escaping' import { applyTemplate } from './template' -import { defaultInfo, disableOptionKeys } from './constants' -import { escape } from './escaping' -import getComponentOption from './getComponentOption' /** * Returns the correct meta info for the given component @@ -11,10 +8,7 @@ import getComponentOption from './getComponentOption' * @param {Object} component - the Vue instance to get meta info from * @return {Object} - returned meta info */ -export default function getMetaInfo (options = {}, component, escapeSequences = []) { - // collect & aggregate all metaInfo $options - let info = getComponentOption(options, component, defaultInfo) - +export default function getMetaInfo (options = {}, info, escapeSequences = [], component) { // Remove all "template" tags from meta // backup the title chunk in case user wants access to it @@ -33,24 +27,5 @@ export default function getMetaInfo (options = {}, component, escapeSequences = info.base = Object.keys(info.base).length ? [info.base] : [] } - const escapeOptions = { - doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value) - } - - disableOptionKeys.forEach((disableKey, index) => { - if (index === 0) { - ensureIsArray(info, disableKey) - } else if (index === 1) { - for (const key in info[disableKey]) { - ensureIsArray(info[disableKey], key) - } - } - - escapeOptions[disableKey] = info[disableKey] - }) - - // begin sanitization - info = escape(info, options, escapeOptions) - - return info + return escapeMetaInfo(options, info, escapeSequences) } diff --git a/test/unit/components.test.js b/test/unit/components.test.js index 431b647..f5741b6 100644 --- a/test/unit/components.test.js +++ b/test/unit/components.test.js @@ -1,3 +1,4 @@ +import { getComponentMetaInfo } from '../../src/shared/getComponentOption' import _getMetaInfo from '../../src/shared/getMetaInfo' import { mount, createWrapper, loadVueMetaPlugin, vmTick } from '../utils' import { defaultOptions } from '../../src/shared/constants' @@ -7,7 +8,7 @@ import HelloWorld from '../components/hello-world.vue' import KeepAlive from '../components/keep-alive.vue' import Changed from '../components/changed.vue' -const getMetaInfo = component => _getMetaInfo(defaultOptions, component) +const getMetaInfo = component => _getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component)) jest.mock('../../src/utils/window', () => ({ hasGlobalWindow: false diff --git a/test/unit/escaping.test.js b/test/unit/escaping.test.js index aff3fef..e3024c8 100644 --- a/test/unit/escaping.test.js +++ b/test/unit/escaping.test.js @@ -1,9 +1,10 @@ +import { getComponentMetaInfo } from '../../src/shared/getComponentOption' import _getMetaInfo from '../../src/shared/getMetaInfo' import { loadVueMetaPlugin } from '../utils' import { defaultOptions } from '../../src/shared/constants' import { serverSequences } from '../../src/shared/escaping' -const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, component, escapeSequences) +const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component), escapeSequences) describe('escaping', () => { let Vue diff --git a/test/unit/generators.test.js b/test/unit/generators.test.js index eb4b905..0a81533 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 = (type, data) => _generateServerInjector(defaultOptions, type, data) +const generateServerInjector = metaInfo => _generateServerInjector(defaultOptions, metaInfo) describe('generators', () => { for (const type in metaInfoData) { @@ -34,9 +34,9 @@ describe('generators', () => { } const defaultTestFn = () => { - const tags = generateServerInjector(type, testInfo.data) - testCases[action](tags) - return tags + const newInfo = generateServerInjector({ [type]: testInfo.data }) + testCases[action](newInfo[type]) + return newInfo[type] } let testFn @@ -62,6 +62,18 @@ describe('generators', () => { }) describe('extra tests', () => { + test('empty config doesnt generate a tag', () => { + const { meta } = generateServerInjector({ meta: [] }) + + expect(meta.text()).toEqual('') + }) + + test('config with empty object doesnt generate a tag', () => { + const { meta } = generateServerInjector({ meta: [{}] }) + + expect(meta.text()).toEqual('') + }) + test('title generator should return an empty string when title is null', () => { const title = null const generatedTitle = titleGenerator({}, 'title', title) @@ -70,19 +82,19 @@ describe('extra tests', () => { }) test('auto add ssrAttribute', () => { - const htmlAttrs = generateServerInjector('htmlAttrs', {}) + const { htmlAttrs } = generateServerInjector({ htmlAttrs: {} }) expect(htmlAttrs.text(true)).toBe('data-vue-meta-server-rendered') - const headAttrs = generateServerInjector('headAttrs', {}) + const { headAttrs } = generateServerInjector({ headAttrs: {} }) expect(headAttrs.text(true)).toBe('') - const bodyAttrs = generateServerInjector('bodyAttrs', {}) + const { bodyAttrs } = generateServerInjector({ bodyAttrs: {} }) expect(bodyAttrs.text(true)).toBe('') }) test('script prepend body', () => { const tags = [{ src: '/script.js', pbody: true }] - const scriptTags = generateServerInjector('script', tags) + const { script: scriptTags } = generateServerInjector({ script: tags }) expect(scriptTags.text()).toBe('') expect(scriptTags.text({ body: true })).toBe('') @@ -91,7 +103,7 @@ describe('extra tests', () => { test('script append body', () => { const tags = [{ src: '/script.js', body: true }] - const scriptTags = generateServerInjector('script', tags) + const { script: scriptTags } = generateServerInjector({ script: tags }) expect(scriptTags.text()).toBe('') expect(scriptTags.text({ body: true })).toBe('') diff --git a/test/unit/getComponentOptions.test.js b/test/unit/getComponentOptions.test.js index 90ca394..fe1dc38 100644 --- a/test/unit/getComponentOptions.test.js +++ b/test/unit/getComponentOptions.test.js @@ -1,4 +1,4 @@ -import getComponentOption from '../../src/shared/getComponentOption' +import { getComponentOption } from '../../src/shared/getComponentOption' import { inMetaInfoBranch } from '../../src/shared/meta-helpers' import { mount, getVue, loadVueMetaPlugin } from '../utils' diff --git a/test/unit/getMetaInfo.test.js b/test/unit/getMetaInfo.test.js index 475ee86..d80b69a 100644 --- a/test/unit/getMetaInfo.test.js +++ b/test/unit/getMetaInfo.test.js @@ -1,8 +1,9 @@ +import { getComponentMetaInfo } from '../../src/shared/getComponentOption' import _getMetaInfo from '../../src/shared/getMetaInfo' import { loadVueMetaPlugin } from '../utils' import { defaultOptions } from '../../src/shared/constants' -const getMetaInfo = component => _getMetaInfo(defaultOptions, component) +const getMetaInfo = component => _getMetaInfo(defaultOptions, getComponentMetaInfo(defaultOptions, component), [], component) describe('getMetaInfo', () => { let Vue diff --git a/test/unit/plugin-server.test.js b/test/unit/plugin-server.test.js index cf6cd9a..df108e3 100644 --- a/test/unit/plugin-server.test.js +++ b/test/unit/plugin-server.test.js @@ -93,4 +93,16 @@ describe('plugin', () => { 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/utils/meta-info-data.js b/test/utils/meta-info-data.js index a0a65e4..2732e0d 100644 --- a/test/utils/meta-info-data.js +++ b/test/utils/meta-info-data.js @@ -225,13 +225,6 @@ const metaInfoData = { data: {}, expect: [''] } - }, - empty: { - add: { - data: [{}], - expect: [''], - test: side => side === 'server' - } } }