diff --git a/src/client/updateClientMetaInfo.js b/src/client/updateClientMetaInfo.js index 19abe89..9f15607 100644 --- a/src/client/updateClientMetaInfo.js +++ b/src/client/updateClientMetaInfo.js @@ -1,5 +1,6 @@ import { metaInfoOptionKeys, metaInfoAttributeKeys } from '../shared/constants' import { isArray } from '../utils/is-type' +import { includes } from '../utils/array' import { updateAttribute, updateTag, updateTitle } from './updaters' function getTag(tags, tag) { @@ -36,7 +37,7 @@ export default function updateClientMetaInfo(options = {}, newInfo) { for (const type in newInfo) { // ignore these - if (metaInfoOptionKeys.includes(type)) { + if (includes(metaInfoOptionKeys, type)) { continue } @@ -46,7 +47,7 @@ export default function updateClientMetaInfo(options = {}, newInfo) { continue } - if (metaInfoAttributeKeys.includes(type)) { + if (includes(metaInfoAttributeKeys, type)) { const tagName = type.substr(0, 4) updateAttribute(options, newInfo[type], getTag(tags, tagName)) continue diff --git a/src/client/updaters/attribute.js b/src/client/updaters/attribute.js index 446060f..cb68f70 100644 --- a/src/client/updaters/attribute.js +++ b/src/client/updaters/attribute.js @@ -1,4 +1,5 @@ import { booleanHtmlAttributes } from '../../shared/constants' +import { toArray, includes } from '../../utils/array' import { isArray } from '../../utils/is-type' /** @@ -10,18 +11,18 @@ import { isArray } from '../../utils/is-type' export default function updateAttribute({ attribute } = {}, attrs, tag) { const vueMetaAttrString = tag.getAttribute(attribute) const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : [] - const toRemove = Array.from(vueMetaAttrs) + const toRemove = toArray(vueMetaAttrs) const keepIndexes = [] for (const attr in attrs) { if (attrs.hasOwnProperty(attr)) { - const value = booleanHtmlAttributes.includes(attr) + const value = includes(booleanHtmlAttributes, attr) ? '' : isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr] tag.setAttribute(attr, value || '') - if (!vueMetaAttrs.includes(attr)) { + if (!includes(vueMetaAttrs, attr)) { vueMetaAttrs.push(attr) } @@ -31,7 +32,7 @@ export default function updateAttribute({ attribute } = {}, attrs, tag) { } const removedAttributesCount = toRemove - .filter((el, index) => !keepIndexes.includes(index)) + .filter((el, index) => !includes(keepIndexes, index)) .reduce((acc, attr) => { tag.removeAttribute(attr) return acc + 1 diff --git a/src/client/updaters/tag.js b/src/client/updaters/tag.js index 7c6bd84..09f645d 100644 --- a/src/client/updaters/tag.js +++ b/src/client/updaters/tag.js @@ -1,4 +1,5 @@ import { isUndefined } from '../../utils/is-type' +import { toArray, includes } from '../../utils/array' /** * Updates meta tags inside and on the client. Borrowed from `react-helmet`: @@ -9,8 +10,9 @@ import { isUndefined } from '../../utils/is-type' * @return {Object} - a representation of what tags changed */ export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) { - const oldHeadTags = Array.from(headTag.querySelectorAll(`${type}[${attribute}]`)) - const oldBodyTags = Array.from(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`)) + const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`)) + const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`)) + const dataAttributes = [tagIDKeyName, 'body'] const newTags = [] if (tags.length > 1) { @@ -20,7 +22,7 @@ export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, const found = [] tags = tags.filter((x) => { const k = JSON.stringify(x) - const res = !found.includes(k) + const res = !includes(found, k) found.push(k) return res }) @@ -44,13 +46,12 @@ export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, } else { newElement.appendChild(document.createTextNode(tag.cssText)) } - } else if ([tagIDKeyName, 'body'].includes(attr)) { - const _attr = `data-${attr}` + } else { + const _attr = includes(dataAttributes, attr) + ? `data-${attr}` + : attr const value = isUndefined(tag[attr]) ? '' : tag[attr] newElement.setAttribute(_attr, value) - } else { - const value = isUndefined(tag[attr]) ? '' : tag[attr] - newElement.setAttribute(attr, value) } } } diff --git a/src/shared/escaping.js b/src/shared/escaping.js index ef0e4b0..fe28a28 100644 --- a/src/shared/escaping.js +++ b/src/shared/escaping.js @@ -1,4 +1,5 @@ import { isString, isArray, isObject } from '../utils/is-type' +import { includes } from '../utils/array' import { metaInfoOptionKeys, disableOptionKeys } from './constants' export const serverSequences = [ @@ -27,13 +28,13 @@ export function escape(info, options, escapeOptions) { const value = info[key] // no need to escape configuration options - if (metaInfoOptionKeys.includes(key)) { + if (includes(metaInfoOptionKeys, key)) { escaped[key] = value continue } let [ disableKey ] = disableOptionKeys - if (escapeOptions[disableKey] && escapeOptions[disableKey].includes(key)) { + if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) { // this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers escaped[key] = value continue @@ -44,7 +45,7 @@ export function escape(info, options, escapeOptions) { disableKey = disableOptionKeys[1] // keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped - if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && escapeOptions[disableKey][tagId].includes(key)) { + if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) { escaped[key] = value continue } diff --git a/src/shared/getMetaInfo.js b/src/shared/getMetaInfo.js index 9bb78c3..a94d79a 100644 --- a/src/shared/getMetaInfo.js +++ b/src/shared/getMetaInfo.js @@ -13,7 +13,7 @@ import getComponentOption from './getComponentOption' */ export default function getMetaInfo(options = {}, component, escapeSequences = []) { // collect & aggregate all metaInfo $options - let info = getComponentOption({ ...options, component }, defaultInfo) + let info = getComponentOption(options, component, defaultInfo) // Remove all "template" tags from meta diff --git a/src/shared/merge.js b/src/shared/merge.js index e33c0a2..b7b5349 100644 --- a/src/shared/merge.js +++ b/src/shared/merge.js @@ -1,4 +1,5 @@ import deepmerge from 'deepmerge' +import { findIndex } from '../utils/array' import { applyTemplate } from './template' import { metaInfoAttributeKeys } from './constants' @@ -15,7 +16,7 @@ export function arrayMerge({ component, tagIDKeyName, metaTemplateKeyName, conte return } - const sourceIndex = source.findIndex(item => item[tagIDKeyName] === targetItem[tagIDKeyName]) + const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName]) const sourceItem = source[sourceIndex] // source doesnt contain any duplicate vmid's, we can keep targetItem diff --git a/src/utils/array.js b/src/utils/array.js new file mode 100644 index 0000000..7ac529c --- /dev/null +++ b/src/utils/array.js @@ -0,0 +1,45 @@ +/* + * To reduce build size, this file provides simple polyfills without + * overly excessive type checking and without modifying + * the global Array.prototype + * The polyfills are automatically removed in the commonjs build + * Also, only files in client/ & shared/ should use these functions + * files in server/ still use normal js function + */ + +// this const is replaced by rollup to true for umd builds +// which means the polyfills are removed for other build formats +const polyfill = process.env.NODE_ENV === 'test' + +export function findIndex(array, predicate) { + 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)) { + return idx + } + } + return -1 + } + return array.findIndex(predicate, arguments[2]) +} + +export function toArray(arg) { + if (polyfill && !Array.from) { + return Array.prototype.slice.call(arg) + } + return Array.from(arg) +} + +export function includes(array, value) { + if (polyfill && !Array.prototype.includes) { + for (const idx in array) { + if (array[idx] === value) { + return true + } + } + + return false + } + return array.includes(value) +} diff --git a/test/unit/shared.test.js b/test/unit/shared.test.js index 60c25b9..118a11a 100644 --- a/test/unit/shared.test.js +++ b/test/unit/shared.test.js @@ -1,32 +1,7 @@ -/** - * @jest-environment node - */ -import setOptions from '../../src/shared/options' +import { setOptions } from '../../src/shared/options' import { defaultOptions } from '../../src/shared/constants' -import { ensureIsArray } from '../../src/utils/ensure' -import { hasGlobalWindowFn } from '../../src/utils/window' describe('shared', () => { - test('ensureIsArray ensures var is array', () => { - let a = { p: 1 } - expect(ensureIsArray(a)).toEqual([]) - - a = 1 - expect(ensureIsArray(a)).toEqual([]) - - a = [1] - expect(ensureIsArray(a)).toBe(a) - }) - - test('ensureIsArray ensures obj prop is array', () => { - const a = { p: 1 } - expect(ensureIsArray(a, 'p')).toEqual({ p: [] }) - }) - - test('no error when window is not defined', () => { - expect(hasGlobalWindowFn()).toBe(false) - }) - test('can use setOptions', () => { const keyName = 'MY KEY' let options = { keyName } diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js new file mode 100644 index 0000000..8f897e3 --- /dev/null +++ b/test/unit/utils.test.js @@ -0,0 +1,63 @@ +/** + * @jest-environment node + */ +import { findIndex, includes, toArray } from '../../src/utils/array' +import { ensureIsArray } from '../../src/utils/ensure' +import { hasGlobalWindowFn } from '../../src/utils/window' + +describe('shared', () => { + afterEach(() => jest.restoreAllMocks()) + + test('ensureIsArray ensures var is array', () => { + let a = { p: 1 } + expect(ensureIsArray(a)).toEqual([]) + + a = 1 + expect(ensureIsArray(a)).toEqual([]) + + a = [1] + expect(ensureIsArray(a)).toBe(a) + }) + + test('ensureIsArray ensures obj prop is array', () => { + const a = { p: 1 } + expect(ensureIsArray(a, 'p')).toEqual({ p: [] }) + }) + + test('no error when window is not defined', () => { + expect(hasGlobalWindowFn()).toBe(false) + }) + + /* eslint-disable no-extend-native */ + test('findIndex polyfill', () => { + const _findIndex = Array.prototype.findIndex + Array.prototype.findIndex = false + + const arr = [1, 2, 3] + expect(findIndex(arr, v => v === 2)).toBe(1) + expect(findIndex(arr, v => v === 4)).toBe(-1) + + Array.prototype.findIndex = _findIndex + }) + + test('includes polyfill', () => { + const _includes = Array.prototype.includes + Array.prototype.includes = false + + const arr = [1, 2, 3] + expect(includes(arr, 2)).toBe(true) + expect(includes(arr, 4)).toBe(false) + + Array.prototype.includes = _includes + }) + + test('from/toArray polyfill', () => { + const _from = Array.from + Array.from = false + + expect(toArray('foo')).toEqual(['f', 'o', 'o']) + + Array.from = _from + }) + /* eslint-enable no-extend-native */ +})