Skip to content

Commit

Permalink
fix: Add Safari polyfill for Proxy (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmdartus authored and diervo committed Apr 4, 2018
1 parent 13cd8c4 commit e9dc833
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/lwc-engine/src/framework/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "../polyfills/proxy-concat/main";

export { createElement } from "./upgrade";
export { getComponentDef } from "./def";
export { Element } from "./html-element";
Expand Down
29 changes: 29 additions & 0 deletions packages/lwc-engine/src/polyfills/proxy-concat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Proxy-concat polyfill

This polyfill is needed for Safari to properly handle `Array.prototype.concat` on proxified array.
Webkit issue: https://bugs.webkit.org/show_bug.cgi?id=184267

```js
// Repro
const proxy = new Proxy([3, 4], {});
const res = [1, 2].concat(proxy);

console.log(res[0]);
console.log(res[1]);
console.log(res[2]);
console.log(res[3]);

// Expected ouptut
1
2
3
4

// Safari output
1
2
Proxy {0: 3, 1: 4}
undefined
```

> TODO: Remove this polyfill once the issue is fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import applyPolyfill from '../polyfill';

applyPolyfill();

it('should correctly concatenate 2 standard arrays', () => {
const first = [1, 2];
const second = [3, 4];
const result = first.concat(second);

expect(result.length).toBe(4);
expect(result).toEqual([1, 2, 3, 4]);
});

it('should correctly concatenate all parameters', () => {
const result = [1].concat([2], [3, 4], [5]);

expect(result.length).toBe(5);
expect(result).toEqual([1, 2, 3, 4, 5]);
});

it('should correctly concatenate values and arrays', () => {
const result = ([1] as any).concat([ true ], null, { x: 'x' }, [5]);

expect(result.length).toBe(5);
expect(result).toEqual([1, true, null, { x: 'x' }, 5]);
});

it('should respect isConcatSpreadable on arrays', () => {
const first = [1, 2];
const second = [3, 4];
second[Symbol.isConcatSpreadable] = false;
const result = first.concat(second);

expect(result.length).toBe(3);
expect(result).toEqual([1, 2, second]);
});

it('should respect isConcatSpreadable on array-like objects', () => {
const first = [1, 2];
const second: any = {
[Symbol.isConcatSpreadable]: true,
length: 2,
0: 3,
1: 4
};
const result = first.concat(second);

expect(result.length).toBe(4);
expect(result).toEqual([1, 2, 3, 4]);
});

it('should correctly concatenate when the target is a Proxy', () => {
const first = new Proxy([1, 2], {});
const second = [3, 4];
const result = first.concat(second);

expect(result.length).toBe(4);
expect(result).toEqual([1, 2, 3, 4]);
});

it('should correctly concatenate when the parameter is a proxy', () => {
const first = [1, 2];
const second = new Proxy([3, 4], {});
const result = first.concat(second);

expect(result.length).toBe(4);
expect(result).toEqual([1, 2, 3, 4]);
});

it('should correctly concatenate 2 proxified arrays', () => {
const first = new Proxy([1, 2], {});
const second = new Proxy([3, 4], {});
const result = first.concat(second);

expect(result.length).toBe(4);
expect(result).toEqual([1, 2, 3, 4]);
});

it('should call all the proxy traps', () => {
const getTrap = jest.fn((target, key) => Reflect.get(target, key));
const hasTrap = jest.fn((target, key) => Reflect.has(target, key));
const proxyHandler = {
get: getTrap,
has: hasTrap,
};

const first = [1, 2];
const second = [3, 4];
const secondProxified = new Proxy([3, 4], proxyHandler);
const result = first.concat(secondProxified);

expect(getTrap.mock.calls).toEqual([
[second, Symbol.isConcatSpreadable, second],
[second, 'length', second],
[second, '0', second],
[second, '1', second],
]);
expect(hasTrap.mock.calls).toEqual([
[second, '0'],
[second, '1'],
]);
});
11 changes: 11 additions & 0 deletions packages/lwc-engine/src/polyfills/proxy-concat/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function detect(): boolean {
// Don't apply polyfill when ProxyCompat is enabled.
if ('getKey' in Proxy) {
return false;
}

const proxy = new Proxy([3, 4], {});
const res = [1, 2].concat(proxy);

return res.length !== 4;
}
6 changes: 6 additions & 0 deletions packages/lwc-engine/src/polyfills/proxy-concat/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import detect from './detect';
import apply from './polyfill';

if (detect()) {
apply();
}
51 changes: 51 additions & 0 deletions packages/lwc-engine/src/polyfills/proxy-concat/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const { isConcatSpreadable } = Symbol;
const { isArray } = Array;
const { slice: ArraySlice, unshift: ArrayUnshift, shift: ArrayShift } = Array.prototype;

function isObject(O: any): boolean {
return typeof O === 'object' ? O !== null : typeof O === 'function';
}

// https://www.ecma-international.org/ecma-262/6.0/#sec-isconcatspreadable
function isSpreadable(O: any): boolean {
if (!isObject(O)) {
return false;
}

const spreadable = O[isConcatSpreadable];
return spreadable !== undefined ? Boolean(spreadable) : isArray(O);
}

// https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.concat
function ArrayConcatPolyfill(this: any, ...args: any[][]): any[] {
const O = Object(this);
const A: any = [];
let N = 0;

const items = ArraySlice.call(arguments);
ArrayUnshift.call(items, O);

while (items.length) {
const E = ArrayShift.call(items);

if (isSpreadable(E)) {
let k = 0;
const length = E.length;
for (k; k < length; k += 1, N += 1) {
if (k in E) {
const subElement = E[k];
A[N] = subElement;
}
}
} else {
A[N] = E;
N += 1;
}
}

return A;
}

export default function apply() {
Array.prototype.concat = ArrayConcatPolyfill;
}

0 comments on commit e9dc833

Please sign in to comment.