Skip to content

Commit

Permalink
fix: implement simply array polyfills (fixes #328)
Browse files Browse the repository at this point in the history
  • Loading branch information
pimlie authored and manniL committed Mar 12, 2019
1 parent 02c7beb commit d38f81e
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 45 deletions.
5 changes: 3 additions & 2 deletions src/client/updateClientMetaInfo.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/client/updaters/attribute.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { booleanHtmlAttributes } from '../../shared/constants'
import { toArray, includes } from '../../utils/array'
import { isArray } from '../../utils/is-type'

/**
Expand All @@ -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)
}

Expand All @@ -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
Expand Down
17 changes: 9 additions & 8 deletions src/client/updaters/tag.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isUndefined } from '../../utils/is-type'
import { toArray, includes } from '../../utils/array'

/**
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
Expand All @@ -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) {
Expand All @@ -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
})
Expand All @@ -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}`
const value = isUndefined(tag[attr]) ? '' : tag[attr]
newElement.setAttribute(_attr, value)
} else {
const _attr = includes(dataAttributes, attr)
? `data-${attr}`
: attr
const value = isUndefined(tag[attr]) ? '' : tag[attr]
newElement.setAttribute(attr, value)
newElement.setAttribute(_attr, value)
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/shared/escaping.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/getMetaInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/shared/merge.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import deepmerge from 'deepmerge'
import { findIndex } from '../utils/array'
import { applyTemplate } from './template'
import { metaInfoAttributeKeys } from './constants'

Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions src/utils/array.js
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 1 addition & 26 deletions test/unit/shared.test.js
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down
63 changes: 63 additions & 0 deletions test/unit/utils.test.js
Original file line number Diff line number Diff line change
@@ -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 */
})

0 comments on commit d38f81e

Please sign in to comment.