diff --git a/Libraries/Core/InitializeCore.js b/Libraries/Core/InitializeCore.js index 8b9a6a4907..263001547b 100644 --- a/Libraries/Core/InitializeCore.js +++ b/Libraries/Core/InitializeCore.js @@ -28,6 +28,7 @@ const start = Date.now(); require('./setUpGlobals'); +require('./polyfillES6Collections'); require('./setUpSystrace'); require('./setUpErrorHandling'); require('./polyfillPromise'); diff --git a/Libraries/Core/__tests__/MapAndSetPolyfills-test.js b/Libraries/Core/__tests__/MapAndSetPolyfills-test.js new file mode 100644 index 0000000000..20f54a0e68 --- /dev/null +++ b/Libraries/Core/__tests__/MapAndSetPolyfills-test.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @emails oncall+react_native + */ +'use strict'; + +// Save these methods so that we can restore them afterward. +const {freeze, seal, preventExtensions} = Object; + +function setup() { + jest.setMock('../../vendor/core/_shouldPolyfillES6Collection', () => true); +} + +function cleanup() { + Object.assign(Object, {freeze, seal, preventExtensions}); +} + +describe('Map polyfill', () => { + setup(); + + const Map = require('../../vendor/core/Map'); + + it('is not native', () => { + const getCode = Function.prototype.toString.call(Map.prototype.get); + expect(getCode).not.toContain('[native code]'); + expect(getCode).toContain('getIndex'); + }); + + it('should tolerate non-extensible object keys', () => { + const map = new Map(); + const key = Object.create(null); + Object.freeze(key); + map.set(key, key); + expect(map.size).toBe(1); + expect(map.has(key)).toBe(true); + map.delete(key); + expect(map.size).toBe(0); + expect(map.has(key)).toBe(false); + }); + + it('should not get confused by prototypal inheritance', () => { + const map = new Map(); + const proto = Object.create(null); + const base = Object.create(proto); + map.set(proto, proto); + expect(map.size).toBe(1); + expect(map.has(proto)).toBe(true); + expect(map.has(base)).toBe(false); + map.set(base, base); + expect(map.size).toBe(2); + expect(map.get(proto)).toBe(proto); + expect(map.get(base)).toBe(base); + }); + + afterAll(cleanup); +}); + +describe('Set polyfill', () => { + setup(); + + const Set = require('../../vendor/core/Set'); + + it('is not native', () => { + const addCode = Function.prototype.toString.call(Set.prototype.add); + expect(addCode).not.toContain('[native code]'); + }); + + it('should tolerate non-extensible object elements', () => { + const set = new Set(); + const elem = Object.create(null); + Object.freeze(elem); + set.add(elem); + expect(set.size).toBe(1); + expect(set.has(elem)).toBe(true); + set.add(elem); + expect(set.size).toBe(1); + set.delete(elem); + expect(set.size).toBe(0); + expect(set.has(elem)).toBe(false); + }); + + it('should not get confused by prototypal inheritance', () => { + const set = new Set(); + const proto = Object.create(null); + const base = Object.create(proto); + set.add(proto); + expect(set.size).toBe(1); + expect(set.has(proto)).toBe(true); + expect(set.has(base)).toBe(false); + set.add(base); + expect(set.size).toBe(2); + expect(set.has(proto)).toBe(true); + expect(set.has(base)).toBe(true); + }); + + afterAll(cleanup); +}); diff --git a/Libraries/Core/polyfillES6Collections.js b/Libraries/Core/polyfillES6Collections.js new file mode 100644 index 0000000000..b58718540f --- /dev/null +++ b/Libraries/Core/polyfillES6Collections.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ +'use strict'; + +const {polyfillGlobal} = require('../Utilities/PolyfillFunctions'); + +/** + * Polyfill ES6 collections (Map and Set). + * If you don't need these polyfills, don't use InitializeCore; just directly + * require the modules you need from InitializeCore for setup. + */ +const _shouldPolyfillCollection = require('../vendor/core/_shouldPolyfillES6Collection'); +if (_shouldPolyfillCollection('Map')) { + // $FlowFixMe: even in strict-local mode Flow expects Map to be Flow-typed + polyfillGlobal('Map', () => require('../vendor/core/Map')); +} +if (_shouldPolyfillCollection('Set')) { + // $FlowFixMe: even in strict-local mode Flow expects Set to be Flow-typed + polyfillGlobal('Set', () => require('../vendor/core/Set')); +} diff --git a/Libraries/vendor/core/Map.js b/Libraries/vendor/core/Map.js new file mode 100644 index 0000000000..4f23b1f478 --- /dev/null +++ b/Libraries/vendor/core/Map.js @@ -0,0 +1,590 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @preventMunge + * @typechecks + */ + +/* eslint-disable no-extend-native, no-shadow-restricted-names */ + +'use strict'; + +const _shouldPolyfillES6Collection = require('./_shouldPolyfillES6Collection'); +const guid = require('./guid'); +const toIterator = require('./toIterator'); + +module.exports = (function(global, undefined) { + // Since our implementation is spec-compliant for the most part we can safely + // delegate to a built-in version if exists and is implemented correctly. + // Firefox had gotten a few implementation details wrong across different + // versions so we guard against that. + if (!_shouldPolyfillES6Collection('Map')) { + return global.Map; + } + + const hasOwn = Object.prototype.hasOwnProperty; + + /** + * == ES6 Map Collection == + * + * This module is meant to implement a Map collection as described in chapter + * 23.1 of the ES6 specification. + * + * Map objects are collections of key/value pairs where both the keys and + * values may be arbitrary ECMAScript language values. A distinct key value + * may only occur in one key/value pair within the Map's collection. + * + * https://people.mozilla.org/~jorendorff/es6-draft.html#sec-map-objects + * + * There only two -- rather small -- deviations from the spec: + * + * 1. The use of untagged frozen objects as keys. + * We decided not to allow and simply throw an error, because this + * implementation of Map works by tagging objects used as Map keys + * with a secret hash property for fast access to the object's place + * in the internal _mapData array. However, to limit the impact of + * this spec deviation, Libraries/Core/InitializeCore.js also wraps + * Object.freeze, Object.seal, and Object.preventExtensions so that + * they tag objects before making them non-extensible, by inserting + * each object into a Map and then immediately removing it. + * + * 2. The `size` property on a map object is a regular property and not a + * computed property on the prototype as described by the spec. + * The reason being is that we simply want to support ES3 environments + * which doesn't implement computed properties. + * + * == Usage == + * + * var map = new Map(iterable); + * + * map.set(key, value); + * map.get(key); // value + * map.has(key); // true + * map.delete(key); // true + * + * var iterator = map.keys(); + * iterator.next(); // {value: key, done: false} + * + * var iterator = map.values(); + * iterator.next(); // {value: value, done: false} + * + * var iterator = map.entries(); + * iterator.next(); // {value: [key, value], done: false} + * + * map.forEach(function(value, key){ this === thisArg }, thisArg); + * + * map.clear(); // resets map. + */ + + /** + * Constants + */ + + // Kinds of map iterations 23.1.5.3 + const KIND_KEY = 'key'; + const KIND_VALUE = 'value'; + const KIND_KEY_VALUE = 'key+value'; + + // In older browsers we can't create a null-prototype object so we have to + // defend against key collisions with built-in methods. + const KEY_PREFIX = '$map_'; + + // This property will be used as the internal size variable to disallow + // writing and to issue warnings for writings in development. + let SECRET_SIZE_PROP; + if (__DEV__) { + SECRET_SIZE_PROP = '$size' + guid(); + } + + class Map { + /** + * 23.1.1.1 + * Takes an `iterable` which is basically any object that implements a + * Symbol.iterator (@@iterator) method. The iterable is expected to be a + * collection of pairs. Each pair is a key/value pair that will be used + * to instantiate the map. + * + * @param {*} iterable + */ + constructor(iterable) { + if (!isObject(this)) { + throw new TypeError('Wrong map object type.'); + } + + initMap(this); + + if (iterable != null) { + const it = toIterator(iterable); + let next; + while (!(next = it.next()).done) { + if (!isObject(next.value)) { + throw new TypeError('Expected iterable items to be pair objects.'); + } + this.set(next.value[0], next.value[1]); + } + } + } + + /** + * 23.1.3.1 + * Clears the map from all keys and values. + */ + clear() { + initMap(this); + } + + /** + * 23.1.3.7 + * Check if a key exists in the collection. + * + * @param {*} key + * @return {boolean} + */ + has(key) { + const index = getIndex(this, key); + return !!(index != null && this._mapData[index]); + } + + /** + * 23.1.3.9 + * Adds a key/value pair to the collection. + * + * @param {*} key + * @param {*} value + * @return {map} + */ + set(key, value) { + let index = getIndex(this, key); + + if (index != null && this._mapData[index]) { + this._mapData[index][1] = value; + } else { + index = this._mapData.push([key, value]) - 1; + setIndex(this, key, index); + if (__DEV__) { + this[SECRET_SIZE_PROP] += 1; + } else { + this.size += 1; + } + } + + return this; + } + + /** + * 23.1.3.6 + * Gets a value associated with a key in the collection. + * + * @param {*} key + * @return {*} + */ + get(key) { + const index = getIndex(this, key); + if (index == null) { + return undefined; + } else { + return this._mapData[index][1]; + } + } + + /** + * 23.1.3.3 + * Delete a key/value from the collection. + * + * @param {*} key + * @return {boolean} Whether the key was found and deleted. + */ + delete(key) { + const index = getIndex(this, key); + if (index != null && this._mapData[index]) { + setIndex(this, key, undefined); + this._mapData[index] = undefined; + if (__DEV__) { + this[SECRET_SIZE_PROP] -= 1; + } else { + this.size -= 1; + } + return true; + } else { + return false; + } + } + + /** + * 23.1.3.4 + * Returns an iterator over the key/value pairs (in the form of an Array) in + * the collection. + * + * @return {MapIterator} + */ + entries() { + return new MapIterator(this, KIND_KEY_VALUE); + } + + /** + * 23.1.3.8 + * Returns an iterator over the keys in the collection. + * + * @return {MapIterator} + */ + keys() { + return new MapIterator(this, KIND_KEY); + } + + /** + * 23.1.3.11 + * Returns an iterator over the values pairs in the collection. + * + * @return {MapIterator} + */ + values() { + return new MapIterator(this, KIND_VALUE); + } + + /** + * 23.1.3.5 + * Iterates over the key/value pairs in the collection calling `callback` + * with [value, key, map]. An optional `thisArg` can be passed to set the + * context when `callback` is called. + * + * @param {function} callback + * @param {?object} thisArg + */ + forEach(callback, thisArg) { + if (typeof callback !== 'function') { + throw new TypeError('Callback must be callable.'); + } + + const boundCallback = callback.bind(thisArg || undefined); + const mapData = this._mapData; + + // Note that `mapData.length` should be computed on each iteration to + // support iterating over new items in the map that were added after the + // start of the iteration. + for (let i = 0; i < mapData.length; i++) { + const entry = mapData[i]; + if (entry != null) { + boundCallback(entry[1], entry[0], this); + } + } + } + } + + // 23.1.3.12 + Map.prototype[toIterator.ITERATOR_SYMBOL] = Map.prototype.entries; + + class MapIterator { + /** + * 23.1.5.1 + * Create a `MapIterator` for a given `map`. While this class is private it + * will create objects that will be passed around publicily. + * + * @param {map} map + * @param {string} kind + */ + constructor(map, kind) { + if (!(isObject(map) && map._mapData)) { + throw new TypeError('Object is not a map.'); + } + + if ([KIND_KEY, KIND_KEY_VALUE, KIND_VALUE].indexOf(kind) === -1) { + throw new Error('Invalid iteration kind.'); + } + + this._map = map; + this._nextIndex = 0; + this._kind = kind; + } + + /** + * 23.1.5.2.1 + * Get the next iteration. + * + * @return {object} + */ + next() { + if (!this instanceof Map) { + throw new TypeError('Expected to be called on a MapIterator.'); + } + + const map = this._map; + let index = this._nextIndex; + const kind = this._kind; + + if (map == null) { + return createIterResultObject(undefined, true); + } + + const entries = map._mapData; + + while (index < entries.length) { + const record = entries[index]; + + index += 1; + this._nextIndex = index; + + if (record) { + if (kind === KIND_KEY) { + return createIterResultObject(record[0], false); + } else if (kind === KIND_VALUE) { + return createIterResultObject(record[1], false); + } else if (kind) { + return createIterResultObject(record, false); + } + } + } + + this._map = undefined; + + return createIterResultObject(undefined, true); + } + } + + // We can put this in the class definition once we have computed props + // transform. + // 23.1.5.2.2 + MapIterator.prototype[toIterator.ITERATOR_SYMBOL] = function() { + return this; + }; + + /** + * Helper Functions. + */ + + /** + * Return an index to map.[[MapData]] array for a given Key. + * + * @param {map} map + * @param {*} key + * @return {?number} + */ + function getIndex(map, key) { + if (isObject(key)) { + const hash = getHash(key); + return map._objectIndex[hash]; + } else { + const prefixedKey = KEY_PREFIX + key; + if (typeof key === 'string') { + return map._stringIndex[prefixedKey]; + } else { + return map._otherIndex[prefixedKey]; + } + } + } + + /** + * Setup an index that refer to the key's location in map.[[MapData]]. + * + * @param {map} map + * @param {*} key + */ + function setIndex(map, key, index) { + const shouldDelete = index == null; + + if (isObject(key)) { + const hash = getHash(key); + if (shouldDelete) { + delete map._objectIndex[hash]; + } else { + map._objectIndex[hash] = index; + } + } else { + const prefixedKey = KEY_PREFIX + key; + if (typeof key === 'string') { + if (shouldDelete) { + delete map._stringIndex[prefixedKey]; + } else { + map._stringIndex[prefixedKey] = index; + } + } else { + if (shouldDelete) { + delete map._otherIndex[prefixedKey]; + } else { + map._otherIndex[prefixedKey] = index; + } + } + } + } + + /** + * Instantiate a map with internal slots. + * + * @param {map} map + */ + function initMap(map) { + // Data structure design inspired by Traceur's Map implementation. + // We maintain an internal array for all the entries. The array is needed + // to remember order. However, to have a reasonable HashMap performance + // i.e. O(1) for insertion, deletion, and retrieval. We maintain indices + // in objects for fast look ups. Indices are split up according to data + // types to avoid collisions. + map._mapData = []; + + // Object index maps from an object "hash" to index. The hash being a unique + // property of our choosing that we associate with the object. Association + // is done by ways of keeping a non-enumerable property on the object. + // Ideally these would be `Object.create(null)` objects but since we're + // trying to support ES3 we'll have to guard against collisions using + // prefixes on the keys rather than rely on null prototype objects. + map._objectIndex = {}; + + // String index maps from strings to index. + map._stringIndex = {}; + + // Numbers, booleans, undefined, and null. + map._otherIndex = {}; + + // Unfortunately we have to support ES3 and cannot have `Map.prototype.size` + // be a getter method but just a regular method. The biggest problem with + // this is safety. Clients can change the size property easily and possibly + // without noticing (e.g. `if (map.size = 1) {..}` kind of typo). What we + // can do to mitigate use getters and setters in development to disallow + // and issue a warning for changing the `size` property. + if (__DEV__) { + if (isES5) { + // If the `SECRET_SIZE_PROP` property is already defined then we're not + // in the first call to `initMap` (e.g. coming from `map.clear()`) so + // all we need to do is reset the size without defining the properties. + if (hasOwn.call(map, SECRET_SIZE_PROP)) { + map[SECRET_SIZE_PROP] = 0; + } else { + Object.defineProperty(map, SECRET_SIZE_PROP, { + value: 0, + writable: true, + }); + Object.defineProperty(map, 'size', { + set: v => { + console.error( + 'PLEASE FIX ME: You are changing the map size property which ' + + 'should not be writable and will break in production.', + ); + throw new Error('The map size property is not writable.'); + }, + get: () => map[SECRET_SIZE_PROP], + }); + } + + // NOTE: Early return to implement immutable `.size` in DEV. + return; + } + } + + // This is a diviation from the spec. `size` should be a getter on + // `Map.prototype`. However, we have to support IE8. + map.size = 0; + } + + /** + * Check if something is an object. + * + * @param {*} o + * @return {boolean} + */ + function isObject(o) { + return o != null && (typeof o === 'object' || typeof o === 'function'); + } + + /** + * Create an iteration object. + * + * @param {*} value + * @param {boolean} done + * @return {object} + */ + function createIterResultObject(value, done) { + return {value, done}; + } + + // Are we in a legit ES5 environment. Spoiler alert: that doesn't include IE8. + const isES5 = (function() { + try { + Object.defineProperty({}, 'x', {}); + return true; + } catch (e) { + return false; + } + })(); + + /** + * Check if an object can be extended. + * + * @param {object|array|function|regexp} o + * @return {boolean} + */ + function isExtensible(o) { + if (!isES5) { + return true; + } else { + return Object.isExtensible(o); + } + } + + const getHash = (function() { + const propIsEnumerable = Object.prototype.propertyIsEnumerable; + const hashProperty = '__MAP_POLYFILL_INTERNAL_HASH__'; + let hashCounter = 0; + + const nonExtensibleObjects = []; + const nonExtensibleHashes = []; + + /** + * Get the "hash" associated with an object. + * + * @param {object|array|function|regexp} o + * @return {number} + */ + return function getHash(o) { + if (hasOwn.call(o, hashProperty)) { + return o[hashProperty]; + } + + if (!isES5) { + if ( + hasOwn.call(o, 'propertyIsEnumerable') && + hasOwn.call(o.propertyIsEnumerable, hashProperty) + ) { + return o.propertyIsEnumerable[hashProperty]; + } + } + + if (isExtensible(o)) { + if (isES5) { + Object.defineProperty(o, hashProperty, { + enumerable: false, + writable: false, + configurable: false, + value: ++hashCounter, + }); + return hashCounter; + } + + if (o.propertyIsEnumerable) { + // Since we can't define a non-enumerable property on the object + // we'll hijack one of the less-used non-enumerable properties to + // save our hash on it. Additionally, since this is a function it + // will not show up in `JSON.stringify` which is what we want. + o.propertyIsEnumerable = function() { + return propIsEnumerable.apply(this, arguments); + }; + return (o.propertyIsEnumerable[hashProperty] = ++hashCounter); + } + } + + // If the object is not extensible, fall back to storing it in an + // array and using Array.prototype.indexOf to find it. + let index = nonExtensibleObjects.indexOf(o); + if (index < 0) { + index = nonExtensibleObjects.length; + nonExtensibleObjects[index] = o; + nonExtensibleHashes[index] = ++hashCounter; + } + return nonExtensibleHashes[index]; + }; + })(); + + return Map; +})(Function('return this')()); // eslint-disable-line no-new-func diff --git a/Libraries/vendor/core/Set.js b/Libraries/vendor/core/Set.js new file mode 100644 index 0000000000..564f530b9a --- /dev/null +++ b/Libraries/vendor/core/Set.js @@ -0,0 +1,198 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @preventMunge + * @typechecks + */ + +/* eslint-disable no-extend-native */ + +'use strict'; + +const Map = require('./Map'); + +const _shouldPolyfillES6Collection = require('./_shouldPolyfillES6Collection'); +const toIterator = require('./toIterator'); + +module.exports = (function(global) { + // Since our implementation is spec-compliant for the most part we can safely + // delegate to a built-in version if exists and is implemented correctly. + // Firefox had gotten a few implementation details wrong across different + // versions so we guard against that. + // These checks are adapted from es6-shim https://fburl.com/34437854 + if (!_shouldPolyfillES6Collection('Set')) { + return global.Set; + } + + /** + * == ES6 Set Collection == + * + * This module is meant to implement a Set collection as described in chapter + * 23.2 of the ES6 specification. + * + * Set objects are collections of unique values. Where values can be any + * JavaScript value. + * https://people.mozilla.org/~jorendorff/es6-draft.html#sec-map-objects + * + * There only two -- rather small -- diviations from the spec: + * + * 1. The use of frozen objects as keys. @see Map module for more on this. + * + * 2. The `size` property on a map object is a regular property and not a + * computed property on the prototype as described by the spec. + * The reason being is that we simply want to support ES3 environments + * which doesn't implement computed properties. + * + * == Usage == + * + * var set = new set(iterable); + * + * set.set(value); + * set.has(value); // true + * set.delete(value); // true + * + * var iterator = set.keys(); + * iterator.next(); // {value: value, done: false} + * + * var iterator = set.values(); + * iterator.next(); // {value: value, done: false} + * + * var iterator = set.entries(); + * iterator.next(); // {value: [value, value], done: false} + * + * set.forEach(function(value, value){ this === thisArg }, thisArg); + * + * set.clear(); // resets set. + */ + + class Set { + /** + * 23.2.1.1 + * + * Takes an optional `iterable` (which is basically any object that + * implements a Symbol.iterator (@@iterator) method). That is a collection + * of values used to instantiate the set. + * + * @param {*} iterable + */ + constructor(iterable) { + if ( + this == null || + (typeof this !== 'object' && typeof this !== 'function') + ) { + throw new TypeError('Wrong set object type.'); + } + + initSet(this); + + if (iterable != null) { + const it = toIterator(iterable); + let next; + while (!(next = it.next()).done) { + this.add(next.value); + } + } + } + + /** + * 23.2.3.1 + * + * If it doesn't already exist in the collection a `value` is added. + * + * @param {*} value + * @return {set} + */ + add(value) { + this._map.set(value, value); + this.size = this._map.size; + return this; + } + + /** + * 23.2.3.2 + * + * Clears the set. + */ + clear() { + initSet(this); + } + + /** + * 23.2.3.4 + * + * Deletes a `value` from the collection if it exists. + * Returns true if the value was found and deleted and false otherwise. + * + * @param {*} value + * @return {boolean} + */ + delete(value) { + const ret = this._map.delete(value); + this.size = this._map.size; + return ret; + } + + /** + * 23.2.3.5 + * + * Returns an iterator over a collection of [value, value] tuples. + */ + entries() { + return this._map.entries(); + } + + /** + * 23.2.3.6 + * + * Iterate over the collection calling `callback` with (value, value, set). + * + * @param {function} callback + */ + forEach(callback) { + const thisArg = arguments[1]; + const it = this._map.keys(); + let next; + while (!(next = it.next()).done) { + callback.call(thisArg, next.value, next.value, this); + } + } + + /** + * 23.2.3.7 + * + * Iterate over the collection calling `callback` with (value, value, set). + * + * @param {*} value + * @return {boolean} + */ + has(value) { + return this._map.has(value); + } + + /** + * 23.2.3.7 + * + * Returns an iterator over the colleciton of values. + */ + values() { + return this._map.values(); + } + } + + // 23.2.3.11 + Set.prototype[toIterator.ITERATOR_SYMBOL] = Set.prototype.values; + + // 23.2.3.7 + Set.prototype.keys = Set.prototype.values; + + function initSet(set) { + set._map = new Map(); + set.size = set._map.size; + } + + return Set; +})(Function('return this')()); // eslint-disable-line no-new-func diff --git a/Libraries/vendor/core/_shouldPolyfillES6Collection.js b/Libraries/vendor/core/_shouldPolyfillES6Collection.js new file mode 100644 index 0000000000..1ba893c5ae --- /dev/null +++ b/Libraries/vendor/core/_shouldPolyfillES6Collection.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @preventMunge + * @flow strict + */ + +'use strict'; + +/** + * Checks whether a collection name (e.g. "Map" or "Set") has a native polyfill + * that is safe to be used. + */ +function _shouldActuallyPolyfillES6Collection(collectionName: string): boolean { + const Collection = global[collectionName]; + if (Collection == null) { + return true; + } + + // The iterator protocol depends on `Symbol.iterator`. If a collection is + // implemented, but `Symbol` is not, it's going to break iteration because + // we'll be using custom "@@iterator" instead, which is not implemented on + // native collections. + if (typeof global.Symbol !== 'function') { + return true; + } + + const proto = Collection.prototype; + + // These checks are adapted from es6-shim: https://fburl.com/34437854 + // NOTE: `isCallableWithoutNew` and `!supportsSubclassing` are not checked + // because they make debugging with "break on exceptions" difficult. + return ( + Collection == null || + typeof Collection !== 'function' || + typeof proto.clear !== 'function' || + new Collection().size !== 0 || + typeof proto.keys !== 'function' || + typeof proto.forEach !== 'function' + ); +} + +const cache: {[name: string]: boolean} = {}; + +/** + * Checks whether a collection name (e.g. "Map" or "Set") has a native polyfill + * that is safe to be used and caches this result. + * Make sure to make a first call to this function before a corresponding + * property on global was overriden in any way. + */ +function _shouldPolyfillES6Collection(collectionName: string) { + let result = cache[collectionName]; + if (result !== undefined) { + return result; + } + + result = _shouldActuallyPolyfillES6Collection(collectionName); + cache[collectionName] = result; + return result; +} + +module.exports = _shouldPolyfillES6Collection; diff --git a/flow/Map.js b/flow/Map.js new file mode 100644 index 0000000000..c8e8305297 --- /dev/null +++ b/flow/Map.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +// These annotations are copy/pasted from the built-in Flow definitions for +// Native Map. + +declare module 'Map' { + // Use the name "MapPolyfill" so that we don't get confusing error + // messages about "Using Map instead of Map". + declare class MapPolyfill { + @@iterator(): Iterator<[K, V]>; + constructor(_: void): MapPolyfill; + constructor(_: null): MapPolyfill; + constructor( + iterable: Iterable<[Key, Value]>, + ): MapPolyfill; + clear(): void; + delete(key: K): boolean; + entries(): Iterator<[K, V]>; + forEach( + callbackfn: (value: V, index: K, map: MapPolyfill) => mixed, + thisArg?: any, + ): void; + get(key: K): V | void; + has(key: K): boolean; + keys(): Iterator; + set(key: K, value: V): MapPolyfill; + size: number; + values(): Iterator; + } + + declare module.exports: typeof MapPolyfill; +} diff --git a/flow/Set.js b/flow/Set.js new file mode 100644 index 0000000000..64099c2a60 --- /dev/null +++ b/flow/Set.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @nolint + * @format + */ + +// These annotations are copy/pasted from the built-in Flow definitions for +// Native Set. + +declare module 'Set' { + // Use the name "SetPolyfill" so that we don't get confusing error + // messages about "Using Set instead of Set". + declare class SetPolyfill { + @@iterator(): Iterator; + constructor(iterable: ?Iterable): void; + add(value: T): SetPolyfill; + clear(): void; + delete(value: T): boolean; + entries(): Iterator<[T, T]>; + forEach( + callbackfn: (value: T, index: T, set: SetPolyfill) => mixed, + thisArg?: any, + ): void; + has(value: T): boolean; + keys(): Iterator; + size: number; + values(): Iterator; + } + + declare module.exports: typeof SetPolyfill; +}