From ee12bfcc904fc6a607b41edddf059c2930c5883c Mon Sep 17 00:00:00 2001 From: pimlie Date: Sat, 14 Sep 2019 18:08:21 +0200 Subject: [PATCH] chore: dist size improvements chore: configure terser to mangle internal properties chore: do not use default value assignment in function args --- scripts/rollup.config.js | 49 ++++++++++++++++++++++++++---- src/client/load.js | 13 ++++---- src/client/refresh.js | 19 +++++++++--- src/client/update.js | 6 ++-- src/client/updateClientMetaInfo.js | 17 ++++++----- src/client/updaters/attribute.js | 9 ++++-- src/client/updaters/tag.js | 22 +++++++------- src/index.js | 2 +- src/server/generate.js | 5 +-- src/server/generators/attribute.js | 3 +- src/server/generators/tag.js | 5 ++- src/server/generators/title.js | 4 ++- src/server/inject.js | 2 +- src/shared/$meta.js | 21 +++++++------ src/shared/constants.js | 17 ++++++----- src/shared/escaping.js | 3 +- src/shared/getComponentOption.js | 13 +++++--- src/shared/getMetaInfo.js | 7 +++-- src/shared/merge.js | 19 ++++++++---- src/shared/meta-helpers.js | 6 ++-- src/shared/mixin.js | 42 +++++++++++++------------ src/shared/nav-guards.js | 19 ++++++------ src/shared/options.js | 21 +++++++++---- src/shared/pausing.js | 12 ++++---- src/shared/template.js | 5 +-- src/utils/array.js | 6 ++-- src/utils/elements.js | 14 +++++++-- test/unit/components.test.js | 4 +-- test/unit/plugin.test.js | 14 ++++----- test/unit/updaters.test.js | 4 +-- test/utils/build.js | 2 +- test/utils/meta-info-data.js | 10 +++--- 32 files changed, 248 insertions(+), 147 deletions(-) diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js index 0c948ef..89f199b 100644 --- a/scripts/rollup.config.js +++ b/scripts/rollup.config.js @@ -5,6 +5,7 @@ import babel from 'rollup-plugin-babel' import replace from 'rollup-plugin-replace' import { terser } from 'rollup-plugin-terser' import defaultsDeep from 'lodash/defaultsDeep' +import { defaultOptions } from '../src/shared/constants' const pkg = require('../package.json') @@ -35,6 +36,41 @@ const babelConfig = () => ({ ] }) +const internalObjectProperties = [ + // Plugin options + // NOTE, see shared/options for why/how this is possible to do + ...Object.keys(defaultOptions), + 'refreshOnceOnNavigation', + // Runtime state props on $root._vueMeta + 'appId', + 'pausing', + 'navGuards', + 'initialized', + 'initializing', + 'deprecationWarningShown', + // updateClientMetaInfo return props + 'tagsAdded', + 'tagsRemoved', + // escapeOptions + 'doEscape', + // deepmerge + 'isMergeableObject', + 'arrayMerge' +] + +const terserOpts = { + nameCache: {}, + mangle: { + properties: { + //debug: '___DEBUGGGG___', + // minimize all object properties except when they are quotes like obj['prop'] + keep_quoted: "strict", + // and minimize props listed in internalObjectProperties + regex: new RegExp(`^(${internalObjectProperties.join('|')})$`) + } + } +} + function rollupConfig({ plugins = [], ...config @@ -51,17 +87,18 @@ function rollupConfig({ 'process.env.VERSION': `"${version}"`, 'process.server' : isBrowserBuild ? 'false' : 'true', /* remove unused stuff from deepmerge */ - // remove react stuff from is-mergeable-object '|| isReactElement(value)': '|| false', // we always provide an arrayMerge, remove default '|| defaultArrayMerge' : '', - // we dont provide a custom merge + // clone is a deprecated option we dont use 'options.clone' : 'false', // we dont provide a custom merge 'options.customMerge' : 'false', - // dont use this - 'deepmerge.all = ' : 'false;' + // we dont use this helper + 'deepmerge.all = ' : 'false;', + // we dont use symbols on our objects + '.concat(getEnumerableOwnPropertySymbols(target))': '' } } @@ -103,7 +140,7 @@ export default [ file: pkg.web.replace('.js', '.min.js'), }, plugins: [ - terser() + terser(terserOpts) ] }, // common js build @@ -137,7 +174,7 @@ export default [ format: 'es' }, plugins: [ - terser() + terser(terserOpts) ], external: Object.keys(pkg.dependencies) } diff --git a/src/client/load.js b/src/client/load.js index 6980fd2..35f7c0c 100644 --- a/src/client/load.js +++ b/src/client/load.js @@ -1,13 +1,14 @@ import { toArray } from '../utils/array' +import { querySelector, removeAttribute } from '../utils/elements' const callbacks = [] -export function isDOMLoaded (d = document) { - return d.readyState !== 'loading' +export function isDOMLoaded (d) { + return (d || document).readyState !== 'loading' } -export function isDOMComplete (d = document) { - return d.readyState === 'complete' +export function isDOMComplete (d) { + return (d || document).readyState === 'complete' } export function waitDOMLoaded () { @@ -69,7 +70,7 @@ export function applyCallbacks (matchElement) { let elements = [] if (!matchElement) { - elements = toArray(document.querySelectorAll(selector)) + elements = toArray(querySelector(selector)) } if (matchElement && matchElement.matches(selector)) { @@ -95,7 +96,7 @@ export function applyCallbacks (matchElement) { * attribute after ssr and if we dont remove it the node * will fail isEqualNode on the client */ - element.removeAttribute('onload') + removeAttribute(element, 'onload') callback(element) } diff --git a/src/client/refresh.js b/src/client/refresh.js index 3d7793f..bb13424 100644 --- a/src/client/refresh.js +++ b/src/client/refresh.js @@ -17,7 +17,9 @@ import updateClientMetaInfo from './updateClientMetaInfo' * * @return {Object} - new meta info */ -export default function refresh (rootVm, options = {}) { +export default function refresh (rootVm, options) { + options = options || {} + // make sure vue-meta was initiated if (!rootVm[rootConfigKey]) { showWarningNotSupported() @@ -30,11 +32,16 @@ export default function refresh (rootVm, options = {}) { const metaInfo = getMetaInfo(options, rawInfo, clientSequences, rootVm) const { appId } = rootVm[rootConfigKey] - const tags = updateClientMetaInfo(appId, options, metaInfo) + let tags = updateClientMetaInfo(appId, options, metaInfo) // emit "event" with new info if (tags && isFunction(metaInfo.changed)) { - metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags) + metaInfo.changed(metaInfo, tags.tagsAdded, tags.tagsRemoved) + + tags = { + addedTags: tags.tagsAdded, + removedTags: tags.tagsRemoved + } } const appsMetaInfo = getAppsMetaInfo() @@ -46,5 +53,9 @@ export default function refresh (rootVm, options = {}) { clearAppsMetaInfo(true) } - return { vm: rootVm, metaInfo, tags } + return { + vm: rootVm, + metaInfo: metaInfo, // eslint-disable-line object-shorthand + tags + } } diff --git a/src/client/update.js b/src/client/update.js index 5524472..2e557d9 100644 --- a/src/client/update.js +++ b/src/client/update.js @@ -11,7 +11,7 @@ export function triggerUpdate (rootVm, hookName) { rootVm[rootConfigKey].initialized = null } - if (rootVm[rootConfigKey].initialized && !rootVm[rootConfigKey].paused) { + if (rootVm[rootConfigKey].initialized && !rootVm[rootConfigKey].pausing) { // batch potential DOM updates to prevent extraneous re-rendering batchUpdate(() => rootVm.$meta().refresh()) } @@ -24,7 +24,9 @@ export function triggerUpdate (rootVm, hookName) { * @param {Function} callback - the update to perform * @return {Number} id - a new ID */ -export function batchUpdate (callback, timeout = 10) { +export function batchUpdate (callback, timeout) { + timeout = timeout || 10 + clearTimeout(batchId) batchId = setTimeout(() => { diff --git a/src/client/updateClientMetaInfo.js b/src/client/updateClientMetaInfo.js index 917af3e..08e269d 100644 --- a/src/client/updateClientMetaInfo.js +++ b/src/client/updateClientMetaInfo.js @@ -1,7 +1,7 @@ import { metaInfoOptionKeys, metaInfoAttributeKeys, tagsSupportingOnload } from '../shared/constants' import { isArray } from '../utils/is-type' import { includes } from '../utils/array' -import { getTag } from '../utils/elements' +import { getTag, removeAttribute } from '../utils/elements' import { addCallbacks, addListeners } from './load' import { updateAttribute, updateTag, updateTitle } from './updaters' @@ -10,7 +10,8 @@ import { updateAttribute, updateTag, updateTitle } from './updaters' * * @param {Object} newInfo - the meta info to update to */ -export default function updateClientMetaInfo (appId, options = {}, newInfo) { +export default function updateClientMetaInfo (appId, options, newInfo) { + options = options || {} const { ssrAttribute, ssrAppId } = options // only cache tags for current update @@ -21,7 +22,7 @@ export default function updateClientMetaInfo (appId, options = {}, newInfo) { // if this is a server render, then dont update if (appId === ssrAppId && htmlTag.hasAttribute(ssrAttribute)) { // remove the server render attribute so we can update on (next) changes - htmlTag.removeAttribute(ssrAttribute) + removeAttribute(htmlTag, ssrAttribute) // add load callbacks if the let addLoadListeners = false @@ -39,8 +40,8 @@ export default function updateClientMetaInfo (appId, options = {}, newInfo) { } // initialize tracked changes - const addedTags = {} - const removedTags = {} + const tagsAdded = {} + const tagsRemoved = {} for (const type in newInfo) { // ignore these @@ -75,10 +76,10 @@ export default function updateClientMetaInfo (appId, options = {}, newInfo) { ) if (newTags.length) { - addedTags[type] = newTags - removedTags[type] = oldTags + tagsAdded[type] = newTags + tagsRemoved[type] = oldTags } } - return { addedTags, removedTags } + return { tagsAdded, tagsRemoved } } diff --git a/src/client/updaters/attribute.js b/src/client/updaters/attribute.js index c4b14cb..e0e92cb 100644 --- a/src/client/updaters/attribute.js +++ b/src/client/updaters/attribute.js @@ -1,5 +1,6 @@ import { booleanHtmlAttributes } from '../../shared/constants' import { includes } from '../../utils/array' +import { removeAttribute } from '../../utils/elements' // keep a local map of attribute values // instead of adding it to the html @@ -11,11 +12,13 @@ export const attributeMap = {} * @param {Object} attrs - the new document html attributes * @param {HTMLElement} tag - the HTMLElement tag to update with new attrs */ -export default function updateAttribute (appId, { attribute } = {}, type, attrs, tag) { +export default function updateAttribute (appId, options, type, attrs, tag) { + const { attribute } = options || {} + const vueMetaAttrString = tag.getAttribute(attribute) if (vueMetaAttrString) { attributeMap[type] = JSON.parse(decodeURI(vueMetaAttrString)) - tag.removeAttribute(attribute) + removeAttribute(tag, attribute) } const data = attributeMap[type] || {} @@ -62,7 +65,7 @@ export default function updateAttribute (appId, { attribute } = {}, type, attrs, tag.setAttribute(attr, attrValue) } else { - tag.removeAttribute(attr) + removeAttribute(tag, attr) } } diff --git a/src/client/updaters/tag.js b/src/client/updaters/tag.js index db7c8c3..3491618 100644 --- a/src/client/updaters/tag.js +++ b/src/client/updaters/tag.js @@ -10,8 +10,8 @@ import { queryElements, getElementsKey } from '../../utils/elements.js' * @param {(Array|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 (appId, options = {}, type, tags, head, body) { - const { attribute, tagIDKeyName } = options +export default function updateTag (appId, options, type, tags, head, body) { + const { attribute, tagIDKeyName } = options || {} const dataAttributes = commonDataAttributes.slice() dataAttributes.push(tagIDKeyName) @@ -46,20 +46,20 @@ export default function updateTag (appId, options = {}, type, tags, head, body) const newElement = document.createElement(type) newElement.setAttribute(attribute, appId) - for (const attr in tag) { + Object.keys(tag).forEach((attr) => { /* istanbul ignore next */ - if (!tag.hasOwnProperty(attr) || includes(tagProperties, attr)) { - continue + if (includes(tagProperties, attr)) { + return } if (attr === 'innerHTML') { newElement.innerHTML = tag.innerHTML - continue + return } if (attr === 'json') { newElement.innerHTML = JSON.stringify(tag.json) - continue + return } if (attr === 'cssText') { @@ -69,12 +69,12 @@ export default function updateTag (appId, options = {}, type, tags, head, body) } else { newElement.appendChild(document.createTextNode(tag.cssText)) } - continue + return } if (attr === 'callback') { newElement.onload = () => tag[attr](newElement) - continue + return } const _attr = includes(dataAttributes, attr) @@ -83,12 +83,12 @@ export default function updateTag (appId, options = {}, type, tags, head, body) const isBooleanAttribute = includes(booleanHtmlAttributes, attr) if (isBooleanAttribute && !tag[attr]) { - continue + return } const value = isBooleanAttribute ? '' : tag[attr] newElement.setAttribute(_attr, value) - } + }) const oldElements = currentElements[getElementsKey(tag)] diff --git a/src/index.js b/src/index.js index 16528d8..b4699fa 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ import { hasMetaInfo } from './shared/meta-helpers' * Plugin install function. * @param {Function} Vue - the Vue constructor. */ -function install (Vue, options = {}) { +function install (Vue, options) { if (Vue.__vuemeta_installed) { return } diff --git a/src/server/generate.js b/src/server/generate.js index 90b4027..3a6f5e9 100644 --- a/src/server/generate.js +++ b/src/server/generate.js @@ -3,8 +3,9 @@ import { serverSequences } from '../shared/escaping' import { setOptions } from '../shared/options' import generateServerInjector from './generateServerInjector' -export default function generate (rawInfo, options = {}) { - const metaInfo = getMetaInfo(setOptions(options), rawInfo, serverSequences) +export default function generate (rawInfo, options) { + options = setOptions(options) + const metaInfo = getMetaInfo(options, rawInfo, serverSequences) const serverInjector = generateServerInjector(options, metaInfo) return serverInjector.injectors diff --git a/src/server/generators/attribute.js b/src/server/generators/attribute.js index cebc943..873669b 100644 --- a/src/server/generators/attribute.js +++ b/src/server/generators/attribute.js @@ -7,7 +7,8 @@ import { booleanHtmlAttributes } from '../../shared/constants' * @param {Object} data - the attributes to generate * @return {Object} - the attribute generator */ -export default function attributeGenerator ({ attribute, ssrAttribute } = {}, type, data, addSrrAttribute) { +export default function attributeGenerator (options, type, data, addSrrAttribute) { + const { attribute, ssrAttribute } = options || {} let attributeStr = '' for (const attr in data) { diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index e4ed12d..0eab0e9 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -14,7 +14,10 @@ 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, { appId, body = false, pbody = false, ln = false } = {}) { +export default function tagGenerator (options, type, tags, generatorOptions) { + const { ssrAppId, attribute, tagIDKeyName } = options || {} + const { appId, body = false, pbody = false, ln = false } = generatorOptions || {} + const dataAttributes = [tagIDKeyName, ...commonDataAttributes] if (!tags || !tags.length) { diff --git a/src/server/generators/title.js b/src/server/generators/title.js index 645b721..1a8a8a7 100644 --- a/src/server/generators/title.js +++ b/src/server/generators/title.js @@ -5,7 +5,9 @@ * @param {String} data - the title text * @return {Object} - the title generator */ -export default function titleGenerator ({ attribute } = {}, type, data, { ln } = {}) { +export default function titleGenerator (options, type, data, generatorOptions) { + const { ln } = generatorOptions || {} + if (!data) { return '' } diff --git a/src/server/inject.js b/src/server/inject.js index 022d0df..5755ff9 100644 --- a/src/server/inject.js +++ b/src/server/inject.js @@ -13,7 +13,7 @@ import generateServerInjector from './generateServerInjector' * @vm {Object} - Vue instance - ideally the root component * @return {Object} - server meta info with `toString` methods */ -export default function inject (rootVm, options = {}) { +export default function inject (rootVm, options) { // make sure vue-meta was initiated if (!rootVm[rootConfigKey]) { showWarningNotSupported() diff --git a/src/shared/$meta.js b/src/shared/$meta.js index fb816af..b1c331d 100644 --- a/src/shared/$meta.js +++ b/src/shared/$meta.js @@ -6,7 +6,8 @@ import { addNavGuards } from './nav-guards' import { pause, resume } from './pausing' import { getOptions } from './options' -export default function $meta (options = {}) { +export default function $meta (options) { + options = options || {} /** * Returns an injector for server-side rendering. * @this {Object} - the Vue instance (a root component) @@ -15,16 +16,18 @@ export default function $meta (options = {}) { const $root = this.$root return { - getOptions: () => getOptions(options), - setOptions: ({ refreshOnceOnNavigation } = {}) => { - if (refreshOnceOnNavigation) { + 'getOptions': () => getOptions(options), + 'setOptions': (newOptions) => { + const refreshNavKey = 'refreshOnceOnNavigation' + if (newOptions && newOptions[refreshNavKey]) { + options.refreshOnceOnNavigation = newOptions[refreshNavKey] addNavGuards($root) } }, - refresh: () => refresh($root, options), - inject: () => process.server ? inject($root, options) : showWarningNotSupportedInBrowserBundle('inject'), - pause: () => pause($root), - resume: () => resume($root), - addApp: appId => addApp($root, appId, options) + 'refresh': () => refresh($root, options), + 'inject': () => process.server ? inject($root, options) : showWarningNotSupportedInBrowserBundle('inject'), + 'pause': () => pause($root), + 'resume': () => resume($root), + 'addApp': appId => addApp($root, appId, options) } } diff --git a/src/shared/constants.js b/src/shared/constants.js index a4327b1..58962c6 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -59,25 +59,28 @@ export const defaultOptions = { ssrAppId } +// might be a bit ugly, but minimizes the browser bundles a bit +const defaultInfoKeys = Object.keys(defaultInfo) + // The metaInfo property keys which are used to disable escaping export const disableOptionKeys = [ - '__dangerouslyDisableSanitizers', - '__dangerouslyDisableSanitizersByTagID' + defaultInfoKeys[12], + defaultInfoKeys[13] ] // List of metaInfo property keys which are configuration options (and dont generate html) export const metaInfoOptionKeys = [ - 'titleChunk', - 'titleTemplate', + defaultInfoKeys[1], + defaultInfoKeys[2], 'changed', ...disableOptionKeys ] // List of metaInfo property keys which only generates attributes and no tags export const metaInfoAttributeKeys = [ - 'htmlAttrs', - 'headAttrs', - 'bodyAttrs' + defaultInfoKeys[3], + defaultInfoKeys[4], + defaultInfoKeys[5] ] // HTML elements which support the onload event diff --git a/src/shared/escaping.js b/src/shared/escaping.js index 55eb284..1d09736 100644 --- a/src/shared/escaping.js +++ b/src/shared/escaping.js @@ -83,7 +83,8 @@ export function escape (info, options, escapeOptions, escapeKeys) { return escaped } -export function escapeMetaInfo (options, info, escapeSequences = []) { +export function escapeMetaInfo (options, info, escapeSequences) { + escapeSequences = escapeSequences || [] // do not use destructuring for seq, it increases transpiled size // due to var checks while we are guaranteed the structure of the cb const escapeOptions = { diff --git a/src/shared/getComponentOption.js b/src/shared/getComponentOption.js index da150dc..631cd76 100644 --- a/src/shared/getComponentOption.js +++ b/src/shared/getComponentOption.js @@ -3,8 +3,8 @@ import { defaultInfo } from './constants' import { merge } from './merge' import { inMetaInfoBranch } from './meta-helpers' -export function getComponentMetaInfo (options = {}, component) { - return getComponentOption(options, component, defaultInfo) +export function getComponentMetaInfo (options, component) { + return getComponentOption(options || {}, component, defaultInfo) } /** @@ -21,14 +21,17 @@ export function getComponentMetaInfo (options = {}, component) { * @param {Object} [result={}] - result so far * @return {Object} result - final aggregated result */ -export function getComponentOption (options = {}, component, result = {}) { - const { keyName } = options - const { $metaInfo, $options, $children } = component +export function getComponentOption (options, component, result) { + result = result || {} if (component._inactive) { return result } + options = options || {} + const { keyName } = options + const { $metaInfo, $options, $children } = component + // only collect option data if it exists if ($options[keyName]) { // if $metaInfo exists then [keyName] was defined as a function diff --git a/src/shared/getMetaInfo.js b/src/shared/getMetaInfo.js index af446d9..ce6dd74 100644 --- a/src/shared/getMetaInfo.js +++ b/src/shared/getMetaInfo.js @@ -9,7 +9,10 @@ import { applyTemplate } from './template' * @param {Object} component - the Vue instance to get meta info from * @return {Object} - returned meta info */ -export default function getMetaInfo (options = {}, info, escapeSequences = [], component) { +export default function getMetaInfo (options, info, escapeSequences, component) { + options = options || {} + escapeSequences = escapeSequences || [] + const { tagIDKeyName } = options // Remove all "template" tags from meta @@ -32,7 +35,7 @@ export default function getMetaInfo (options = {}, info, escapeSequences = [], c if (info.meta) { // remove meta items with duplicate vmid's info.meta = info.meta.filter((metaItem, index, arr) => { - const hasVmid = metaItem.hasOwnProperty(tagIDKeyName) + const hasVmid = !!metaItem[tagIDKeyName] if (!hasVmid) { return true } diff --git a/src/shared/merge.js b/src/shared/merge.js index a251d0e..c6f8ff2 100644 --- a/src/shared/merge.js +++ b/src/shared/merge.js @@ -33,8 +33,10 @@ export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, cont // when sourceItem explictly defines contentKeyName or innerHTML as undefined, its // an indication that we need to skip the default behaviour or child has preference over parent // which means we keep the targetItem and ignore/remove the sourceItem - if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) || - (sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) { + if ( + (contentKeyName in sourceItem && sourceItem[contentKeyName] === undefined) || + ('innerHTML' in sourceItem && sourceItem.innerHTML === undefined) + ) { destination.push(targetItem) // remove current index from source array so its not concatenated to destination below source.splice(sourceIndex, 1) @@ -75,11 +77,15 @@ export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, cont return destination.concat(source) } -export function merge (target, source, options = {}) { +let warningShown = false + +export function merge (target, source, options) { + options = options || {} + // remove properties explicitly set to false so child components can // optionally _not_ overwrite the parents content // (for array properties this is checked in arrayMerge) - if (source.hasOwnProperty('title') && source.title === undefined) { + if (source.title === undefined) { delete source.title } @@ -89,9 +95,10 @@ export function merge (target, source, options = {}) { } for (const key in source[attrKey]) { - if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) { - if (includes(booleanHtmlAttributes, key)) { + if (key in source[attrKey] && source[attrKey][key] === undefined) { + if (includes(booleanHtmlAttributes, key) && !warningShown) { warn('VueMeta: Please note that since v2 the value undefined is not used to indicate boolean attributes anymore, see migration guide for details') + warningShown = true } delete source[attrKey][key] } diff --git a/src/shared/meta-helpers.js b/src/shared/meta-helpers.js index fa2982e..aaa12d6 100644 --- a/src/shared/meta-helpers.js +++ b/src/shared/meta-helpers.js @@ -2,11 +2,13 @@ import { isUndefined, isObject } from '../utils/is-type' import { rootConfigKey } from './constants' // Vue $root instance has a _vueMeta object property, otherwise its a boolean true -export function hasMetaInfo (vm = this) { +export function hasMetaInfo (vm) { + vm = vm || this return vm && (vm[rootConfigKey] === true || isObject(vm[rootConfigKey])) } // a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has -export function inMetaInfoBranch (vm = this) { +export function inMetaInfoBranch (vm) { + vm = vm || this return vm && !isUndefined(vm[rootConfigKey]) } diff --git a/src/shared/mixin.js b/src/shared/mixin.js index fd2ec17..02c4e5c 100644 --- a/src/shared/mixin.js +++ b/src/shared/mixin.js @@ -15,19 +15,22 @@ export default function createMixin (Vue, options) { // watch for client side component updates return { beforeCreate () { - const $root = this.$root - const $options = this.$options - const $isServer = this.$isServer + // https://github.com/terser/terser/issues/458 + const $this = this + const $root = $this.$root + const $options = $this.$options + const $isServer = $this.$isServer + const $nextTick = $this.$nextTick - Object.defineProperty(this, '_hasMetaInfo', { + Object.defineProperty($this, '_hasMetaInfo', { configurable: true, get () { // Show deprecation warning once when devtools enabled - if (Vue.config.devtools && !$root[rootConfigKey]._shown) { + if (Vue.config.devtools && !$root[rootConfigKey].deprecationWarningShown) { warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead') - $root[rootConfigKey]._shown = true + $root[rootConfigKey].deprecationWarningShown = true } - return hasMetaInfo(this) + return hasMetaInfo($this) } }) @@ -45,10 +48,10 @@ export default function createMixin (Vue, options) { // to speed up updates we keep track of branches which have a component with vue-meta info defined // if _vueMeta = true it has info, if _vueMeta = false a child has info - if (!this[rootConfigKey]) { - this[rootConfigKey] = true + if (!$this[rootConfigKey]) { + $this[rootConfigKey] = true - let parent = this.$parent + let parent = $this.$parent while (parent && parent !== $root) { if (isUndefined(parent[rootConfigKey])) { parent[rootConfigKey] = false @@ -60,9 +63,7 @@ export default function createMixin (Vue, options) { // coerce function-style metaInfo to a computed prop so we can observe // it on creation if (isFunction($options[options.keyName])) { - if (!$options.computed) { - $options.computed = {} - } + $options.computed = $options.computed || {} $options.computed.$metaInfo = $options[options.keyName] if (!$isServer) { @@ -70,7 +71,7 @@ export default function createMixin (Vue, options) { // 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) ensuredPush($options, 'created', () => { - this.$watch('$metaInfo', () => { + $this.$watch('$metaInfo', () => { triggerUpdate($root, 'watcher') }) }) @@ -88,7 +89,7 @@ export default function createMixin (Vue, options) { ensuredPush($options, 'beforeMount', () => { // if this Vue-app was server rendered, set the appId to 'ssr' // only one SSR app per page is supported - if ($root.$el && $root.$el.hasAttribute && $root.$el.hasAttribute('data-server-rendered')) { + if ($root.$el && $root.$el.nodeType === 1 && $root.$el.hasAttribute('data-server-rendered')) { $root[rootConfigKey].appId = options.ssrAppId } }) @@ -101,7 +102,7 @@ export default function createMixin (Vue, options) { $root[rootConfigKey].initializing = true // refresh meta in nextTick so all child components have loaded - this.$nextTick(function () { + $nextTick(function () { const { tags, metaInfo } = $root.$meta().refresh() // After ssr hydration (identifier by tags === false) check @@ -111,7 +112,7 @@ export default function createMixin (Vue, options) { // current hook was called // (during initialization all changes are blocked) if (tags === false && $root[rootConfigKey].initialized === null) { - this.$nextTick(() => triggerUpdate($root, 'initializing')) + $nextTick(() => triggerUpdate($root, 'init')) } $root[rootConfigKey].initialized = true @@ -145,23 +146,24 @@ export default function createMixin (Vue, options) { }, // TODO: move back into beforeCreate when Vue issue is resolved destroyed () { + const $this = this // do not trigger refresh: // - when the component doesnt have a parent // - doesnt have metaInfo defined - if (!this.$parent || !hasMetaInfo(this)) { + if (!$this.$parent || !hasMetaInfo($this)) { return } // Wait that element is hidden before refreshing meta tags (to support animations) const interval = setInterval(() => { - if (this.$el && this.$el.offsetParent !== null) { + if ($this.$el && $this.$el.offsetParent !== null) { /* istanbul ignore next line */ return } clearInterval(interval) - triggerUpdate(this.$root, 'destroyed') + triggerUpdate($this.$root, 'destroyed') }, 50) } } diff --git a/src/shared/nav-guards.js b/src/shared/nav-guards.js index 6161d9d..c63d925 100644 --- a/src/shared/nav-guards.js +++ b/src/shared/nav-guards.js @@ -1,26 +1,27 @@ import { isFunction } from '../utils/is-type' import { rootConfigKey } from './constants' +import { pause, resume } from './pausing' export function addNavGuards (rootVm) { + const router = rootVm.$router + // return when nav guards already added or no router exists - if (rootVm[rootConfigKey].navGuards || !rootVm.$router) { + if (rootVm[rootConfigKey].navGuards || !router) { /* istanbul ignore next */ return } rootVm[rootConfigKey].navGuards = true - const $router = rootVm.$router - const $meta = rootVm.$meta() - - $router.beforeEach((to, from, next) => { - $meta.pause() + router.beforeEach((to, from, next) => { + pause(rootVm) next() }) - $router.afterEach(() => { - const { metaInfo } = $meta.resume() - if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) { + router.afterEach(() => { + const { metaInfo } = resume(rootVm) + + if (metaInfo && isFunction(metaInfo.afterNavigation)) { metaInfo.afterNavigation(metaInfo) } }) diff --git a/src/shared/options.js b/src/shared/options.js index 88a3254..80c6493 100644 --- a/src/shared/options.js +++ b/src/shared/options.js @@ -5,13 +5,22 @@ export function setOptions (options) { // combine options options = isObject(options) ? options : {} - for (const key in defaultOptions) { - if (!options[key]) { - options[key] = defaultOptions[key] - } + // The options are set like this so they can + // be minified by terser while keeping the + // user api intact + // terser --mangle-properties keep_quoted=strict + /* eslint-disable dot-notation */ + return { + keyName: options['keyName'] || defaultOptions.keyName, + attribute: options['attribute'] || defaultOptions.attribute, + ssrAttribute: options['ssrAttribute'] || defaultOptions.ssrAttribute, + tagIDKeyName: options['tagIDKeyName'] || defaultOptions.tagIDKeyName, + contentKeyName: options['contentKeyName'] || defaultOptions.contentKeyName, + metaTemplateKeyName: options['metaTemplateKeyName'] || defaultOptions.metaTemplateKeyName, + ssrAppId: options['ssrAppId'] || defaultOptions.ssrAppId, + refreshOnceOnNavigation: !!options['refreshOnceOnNavigation'] } - - return options + /* eslint-enable dot-notation */ } export function getOptions (options) { diff --git a/src/shared/pausing.js b/src/shared/pausing.js index 5d62d19..5cf2282 100644 --- a/src/shared/pausing.js +++ b/src/shared/pausing.js @@ -1,15 +1,15 @@ import { rootConfigKey } from './constants' -export function pause (rootVm, refresh = true) { - rootVm[rootConfigKey].paused = true +export function pause (rootVm, refresh) { + rootVm[rootConfigKey].pausing = true - return () => resume(refresh) + return () => resume(rootVm, refresh) } -export function resume (rootVm, refresh = true) { - rootVm[rootConfigKey].paused = false +export function resume (rootVm, refresh) { + rootVm[rootConfigKey].pausing = false - if (refresh) { + if (refresh || refresh === undefined) { return rootVm.$meta().refresh() } } diff --git a/src/shared/template.js b/src/shared/template.js index 144db82..b24edad 100644 --- a/src/shared/template.js +++ b/src/shared/template.js @@ -14,10 +14,7 @@ export function applyTemplate ({ component, metaTemplateKeyName, contentKeyName // return early if no template defined if (!template) { // cleanup faulty template properties - if (headObject.hasOwnProperty(metaTemplateKeyName)) { - delete headObject[metaTemplateKeyName] - } - + delete headObject[metaTemplateKeyName] return false } diff --git a/src/utils/array.js b/src/utils/array.js index 3f77000..41b2456 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -11,17 +11,17 @@ // which means the polyfills are removed for other build formats const polyfill = process.env.NODE_ENV === 'test' -export function findIndex (array, predicate) { +export function findIndex (array, predicate, thisArg) { if (polyfill && !Array.prototype.findIndex) { // idx needs to be a Number, for..in returns string for (let idx = 0; idx < array.length; idx++) { - if (predicate.call(arguments[2], array[idx], idx, array)) { + if (predicate.call(thisArg, array[idx], idx, array)) { return idx } } return -1 } - return array.findIndex(predicate, arguments[2]) + return array.findIndex(predicate, thisArg) } export function toArray (arg) { diff --git a/src/utils/elements.js b/src/utils/elements.js index e92b055..d33418f 100644 --- a/src/utils/elements.js +++ b/src/utils/elements.js @@ -1,5 +1,7 @@ import { toArray } from './array' +export const querySelector = (arg, el) => (el || document).querySelectorAll(arg) + export function getTag (tags, tag) { if (!tags[tag]) { tags[tag] = document.getElementsByTagName(tag)[0] @@ -14,7 +16,9 @@ export function getElementsKey ({ body, pbody }) { : (pbody ? 'pbody' : 'head') } -export function queryElements (parentNode, { appId, attribute, type, tagIDKeyName }, attributes = {}) { +export function queryElements (parentNode, { appId, attribute, type, tagIDKeyName }, attributes) { + attributes = attributes || {} + const queries = [ `${type}[${attribute}="${appId}"]`, `${type}[data-${tagIDKeyName}]` @@ -27,9 +31,13 @@ export function queryElements (parentNode, { appId, attribute, type, tagIDKeyNam return query }) - return toArray(parentNode.querySelectorAll(queries.join(', '))) + return toArray(querySelector(queries.join(', '), parentNode)) } export function removeElementsByAppId ({ attribute }, appId) { - toArray(document.querySelectorAll(`[${attribute}="${appId}"]`)).map(el => el.remove()) + toArray(querySelector(`[${attribute}="${appId}"]`)).map(el => el.remove()) +} + +export function removeAttribute (el, attributeName) { + el.removeAttribute(attributeName) } diff --git a/test/unit/components.test.js b/test/unit/components.test.js index d155ba6..8b47e4e 100644 --- a/test/unit/components.test.js +++ b/test/unit/components.test.js @@ -257,7 +257,7 @@ describe('components', () => { expect(guards.after).toBeDefined() guards.before(null, null, () => {}) - expect(wrapper.vm.$root._vueMeta.paused).toBe(true) + expect(wrapper.vm.$root._vueMeta.pausing).toBe(true) guards.after() expect(afterNavigation).toHaveBeenCalled() @@ -292,7 +292,7 @@ describe('components', () => { expect(guards.after).toBeDefined() guards.before(null, null, () => {}) - expect(wrapper.vm.$root._vueMeta.paused).toBe(true) + expect(wrapper.vm.$root._vueMeta.pausing).toBe(true) guards.after() expect(afterNavigation).toHaveBeenCalled() diff --git a/test/unit/plugin.test.js b/test/unit/plugin.test.js index 65a143d..079dc72 100644 --- a/test/unit/plugin.test.js +++ b/test/unit/plugin.test.js @@ -148,13 +148,13 @@ describe('plugin', () => { warn.mockRestore() }) - test('updates can be paused and resumed', async () => { + test('updates can be pausing and resumed', async () => { const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update') const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate) // because triggerUpdate & batchUpdate reside in the same file we cant mock them both, // so just recreate the triggerUpdate fn by copying its implementation const triggerUpdateSpy = triggerUpdate.mockImplementation((vm, hookName) => { - if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.pausing) { // batch potential DOM updates to prevent extraneous re-rendering batchUpdateSpy(() => vm.$meta().refresh()) } @@ -185,7 +185,7 @@ describe('plugin', () => { // no batchUpdate on initialization expect(wrapper.vm.$root._vueMeta.initialized).toBe(false) - expect(wrapper.vm.$root._vueMeta.paused).toBeFalsy() + expect(wrapper.vm.$root._vueMeta.pausing).toBeFalsy() expect(triggerUpdateSpy).toHaveBeenCalledTimes(1) expect(batchUpdateSpy).not.toHaveBeenCalled() jest.clearAllMocks() @@ -196,7 +196,7 @@ describe('plugin', () => { // batchUpdate on normal update expect(wrapper.vm.$root._vueMeta.initialized).toBe(true) - expect(wrapper.vm.$root._vueMeta.paused).toBeFalsy() + expect(wrapper.vm.$root._vueMeta.pausing).toBeFalsy() expect(triggerUpdateSpy).toHaveBeenCalledTimes(1) expect(batchUpdateSpy).toHaveBeenCalledTimes(1) jest.clearAllMocks() @@ -205,9 +205,9 @@ describe('plugin', () => { title = 'third title' wrapper.setProps({ title }) - // no batchUpdate when paused + // no batchUpdate when pausing expect(wrapper.vm.$root._vueMeta.initialized).toBe(true) - expect(wrapper.vm.$root._vueMeta.paused).toBe(true) + expect(wrapper.vm.$root._vueMeta.pausing).toBe(true) expect(triggerUpdateSpy).toHaveBeenCalledTimes(1) expect(batchUpdateSpy).not.toHaveBeenCalled() jest.clearAllMocks() @@ -225,7 +225,7 @@ describe('plugin', () => { // because triggerUpdate & batchUpdate reside in the same file we cant mock them both, // so just recreate the triggerUpdate fn by copying its implementation triggerUpdate.mockImplementation((vm, hookName) => { - if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) { + if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.pausing) { // batch potential DOM updates to prevent extraneous re-rendering batchUpdateSpy(refreshSpy) } diff --git a/test/unit/updaters.test.js b/test/unit/updaters.test.js index dc38bc4..8dc54f0 100644 --- a/test/unit/updaters.test.js +++ b/test/unit/updaters.test.js @@ -23,7 +23,7 @@ describe('updaters', () => { add: (tags) => { typeTests.add.expect.forEach((expected, index) => { if (!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) { - expect(tags.addedTags[type][index].outerHTML).toBe(expected) + expect(tags.tagsAdded[type][index].outerHTML).toBe(expected) } expect(html.outerHTML).toContain(expected) }) @@ -37,7 +37,7 @@ describe('updaters', () => { typeTests.change.expect.forEach((expected, index) => { if (!['title', 'htmlAttrs', 'headAttrs', 'bodyAttrs'].includes(type)) { - expect(tags.addedTags[type][index].outerHTML).toBe(expected) + expect(tags.tagsAdded[type][index].outerHTML).toBe(expected) } expect(html.outerHTML).toContain(expected) }) diff --git a/test/utils/build.js b/test/utils/build.js index 8daa539..043e316 100644 --- a/test/utils/build.js +++ b/test/utils/build.js @@ -19,7 +19,7 @@ export const useDist = stdEnv.test && stdEnv.ci export function getVueMetaPath (browser) { if (useDist) { - return path.resolve(__dirname, `../..${browser ? '/dist/vue-meta.js' : ''}`) + return path.resolve(__dirname, `../..${browser ? '/dist/vue-meta.min.js' : ''}`) } process.server = !browser diff --git a/test/utils/meta-info-data.js b/test/utils/meta-info-data.js index 10bffb6..627b5f6 100644 --- a/test/utils/meta-info-data.js +++ b/test/utils/meta-info-data.js @@ -71,9 +71,9 @@ const metaInfoData = { return () => { const tags = defaultTest() - expect(tags.addedTags.meta.length).toBe(1) + expect(tags.tagsAdded.meta.length).toBe(1) // TODO: not sure if we really expect this - expect(tags.removedTags.meta.length).toBe(1) + expect(tags.tagsRemoved.meta.length).toBe(1) } } } @@ -143,9 +143,9 @@ const metaInfoData = { } const tags = defaultTest() - expect(tags.addedTags.script[0].parentNode.tagName).toBe('HEAD') - expect(tags.addedTags.script[1].parentNode.tagName).toBe('BODY') - expect(tags.addedTags.script[2].parentNode.tagName).toBe('BODY') + expect(tags.tagsAdded.script[0].parentNode.tagName).toBe('HEAD') + expect(tags.tagsAdded.script[1].parentNode.tagName).toBe('BODY') + expect(tags.tagsAdded.script[2].parentNode.tagName).toBe('BODY') } else { // ssr doesnt generate data-body tags const bodyPrepended = this.expect[1]