From 9084dd953d19254fb775900ffcb7d1dd652356f5 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 17 Feb 2021 16:56:36 -0500 Subject: [PATCH] Inline promise polyfill fallback. The UMD wrapping that was used by es6-promise was causing issues in certain contexts (see https://github.com/embroider-build/embroider/issues/677). This moves from es6-promise to the same promise polyfill that is used by qunit@2.14.0 and higher (promise-polyfill@8.2.0 with some tweaks). --- .eslintrc.js | 7 + .../-internal/promise-polyfill.d.ts | 1 + .../-internal/promise-polyfill.js | 391 ++++++++++++++++++ .../@ember/test-helpers/-utils.ts | 2 +- package.json | 5 +- tests/test-helper.js | 5 +- yarn.lock | 2 +- 7 files changed, 406 insertions(+), 7 deletions(-) create mode 100644 addon-test-support/@ember/test-helpers/-internal/promise-polyfill.d.ts create mode 100644 addon-test-support/@ember/test-helpers/-internal/promise-polyfill.js diff --git a/.eslintrc.js b/.eslintrc.js index 8e18f0730..af591f64a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -70,5 +70,12 @@ module.exports = { 'require-jsdoc': 'error', }, }, + { + files: ['addon-test-support/@ember/test-helpers/-internal/promise-polyfill.js'], + rules: { + 'require-jsdoc': 'off', + 'valid-jsdoc': 'off', + }, + }, ], }; diff --git a/addon-test-support/@ember/test-helpers/-internal/promise-polyfill.d.ts b/addon-test-support/@ember/test-helpers/-internal/promise-polyfill.d.ts new file mode 100644 index 000000000..b0d366aa5 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/-internal/promise-polyfill.d.ts @@ -0,0 +1 @@ +export default Promise; diff --git a/addon-test-support/@ember/test-helpers/-internal/promise-polyfill.js b/addon-test-support/@ember/test-helpers/-internal/promise-polyfill.js new file mode 100644 index 000000000..7c73d8d83 --- /dev/null +++ b/addon-test-support/@ember/test-helpers/-internal/promise-polyfill.js @@ -0,0 +1,391 @@ +/* globals globalThis global setImmediate */ + +/* +Using the same promise polyfill that is used in qunit@2.14.0 (see https://git.io/JtMxC). + +https://github.com/taylorhakes/promise-polyfill/tree/8.2.0 + +Copyright 2014 Taylor Hakes +Copyright 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------- + +Patches from promise-polyfill@8.2.0 for use in QUnit: + +- 2021-01-09: Export as module only, don't change global scope as QUnit must not + affect the host context (e.g. people may test their application intentionally + with different or no polyfills and we must not affect that). + +- 2021-01-10: Avoid unconditional reference to setTimeout, which isn't supported + on SpiderMonkey (mozjs 68). Done by re-arranging the code so that we return early + (it has native support for Promise), instead of building an unused polyfill. + +- 2021-01-10: Add 'globalThis' to globalNS implementation to support SpiderMonkey. +*/ +export default (function () { + 'use strict'; + + /** @suppress {undefinedVars} */ + let globalNS = (function () { + // the only reliable means to get the global object is + // `Function('return this')()` + // However, this causes CSP violations in Chrome apps. + if (typeof globalThis !== 'undefined') { + return globalThis; + } + if (typeof self !== 'undefined') { + return self; + } + if (typeof window !== 'undefined') { + return window; + } + if (typeof global !== 'undefined') { + return global; + } + throw new Error('unable to locate global object'); + })(); + + // Expose the polyfill if Promise is undefined or set to a + // non-function value. The latter can be due to a named HTMLElement + // being exposed by browsers for legacy reasons. + // https://github.com/taylorhakes/promise-polyfill/issues/114 + if (typeof globalNS['Promise'] === 'function') { + return globalNS['Promise']; + } + + /** + * @this {Promise} + */ + function finallyConstructor(callback) { + let constructor = this.constructor; + return this.then( + function (value) { + // @ts-ignore + return constructor.resolve(callback()).then(function () { + return value; + }); + }, + function (reason) { + // @ts-ignore + return constructor.resolve(callback()).then(function () { + // @ts-ignore + return constructor.reject(reason); + }); + } + ); + } + + function allSettled(arr) { + let P = this; + return new P(function (resolve, reject) { + if (!(arr && typeof arr.length !== 'undefined')) { + return reject( + new TypeError( + typeof arr + + ' ' + + arr + + ' is not iterable(cannot read property Symbol(Symbol.iterator))' + ) + ); + } + let args = Array.prototype.slice.call(arr); + if (args.length === 0) return resolve([]); + let remaining = args.length; + + function res(i, val) { + if (val && (typeof val === 'object' || typeof val === 'function')) { + let then = val.then; + if (typeof then === 'function') { + then.call( + val, + function (val) { + res(i, val); + }, + function (e) { + args[i] = { status: 'rejected', reason: e }; + if (--remaining === 0) { + resolve(args); + } + } + ); + return; + } + } + args[i] = { status: 'fulfilled', value: val }; + if (--remaining === 0) { + resolve(args); + } + } + + for (let i = 0; i < args.length; i++) { + res(i, args[i]); + } + }); + } + + // Store setTimeout reference so promise-polyfill will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + let setTimeoutFunc = setTimeout; + + function isArray(x) { + return Boolean(x && typeof x.length !== 'undefined'); + } + + function noop() {} + + // Polyfill for Function.prototype.bind + function bind(fn, thisArg) { + return function () { + fn.apply(thisArg, arguments); + }; + } + + /** + * @constructor + * @param {Function} fn + */ + function Promise(fn) { + if (!(this instanceof Promise)) throw new TypeError('Promises must be constructed via new'); + if (typeof fn !== 'function') throw new TypeError('not a function'); + /** @type {!number} */ + this._state = 0; + /** @type {!boolean} */ + this._handled = false; + /** @type {Promise|undefined} */ + this._value = undefined; + /** @type {!Array} */ + this._deferreds = []; + + doResolve(fn, this); + } + + function handle(self, deferred) { + while (self._state === 3) { + self = self._value; + } + if (self._state === 0) { + self._deferreds.push(deferred); + return; + } + self._handled = true; + Promise._immediateFn(function () { + let cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; + if (cb === null) { + (self._state === 1 ? resolve : reject)(deferred.promise, self._value); + return; + } + let ret; + try { + ret = cb(self._value); + } catch (e) { + reject(deferred.promise, e); + return; + } + resolve(deferred.promise, ret); + }); + } + + function resolve(self, newValue) { + try { + // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure + if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.'); + if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { + let then = newValue.then; + if (newValue instanceof Promise) { + self._state = 3; + self._value = newValue; + finale(self); + return; + } else if (typeof then === 'function') { + doResolve(bind(then, newValue), self); + return; + } + } + self._state = 1; + self._value = newValue; + finale(self); + } catch (e) { + reject(self, e); + } + } + + function reject(self, newValue) { + self._state = 2; + self._value = newValue; + finale(self); + } + + function finale(self) { + if (self._state === 2 && self._deferreds.length === 0) { + Promise._immediateFn(function () { + if (!self._handled) { + Promise._unhandledRejectionFn(self._value); + } + }); + } + + for (let i = 0, len = self._deferreds.length; i < len; i++) { + handle(self, self._deferreds[i]); + } + self._deferreds = null; + } + + /** + * @constructor + */ + function Handler(onFulfilled, onRejected, promise) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.promise = promise; + } + + /** + * Take a potentially misbehaving resolver function and make sure + * onFulfilled and onRejected are only called once. + * + * Makes no guarantees about asynchrony. + */ + function doResolve(fn, self) { + let done = false; + try { + fn( + function (value) { + if (done) return; + done = true; + resolve(self, value); + }, + function (reason) { + if (done) return; + done = true; + reject(self, reason); + } + ); + } catch (ex) { + if (done) return; + done = true; + reject(self, ex); + } + } + + Promise.prototype['catch'] = function (onRejected) { + return this.then(null, onRejected); + }; + + Promise.prototype.then = function (onFulfilled, onRejected) { + // @ts-ignore + let prom = new this.constructor(noop); + + handle(this, new Handler(onFulfilled, onRejected, prom)); + return prom; + }; + + Promise.prototype['finally'] = finallyConstructor; + + Promise.all = function (arr) { + return new Promise(function (resolve, reject) { + if (!isArray(arr)) { + return reject(new TypeError('Promise.all accepts an array')); + } + + let args = Array.prototype.slice.call(arr); + if (args.length === 0) return resolve([]); + let remaining = args.length; + + function res(i, val) { + try { + if (val && (typeof val === 'object' || typeof val === 'function')) { + let then = val.then; + if (typeof then === 'function') { + then.call( + val, + function (val) { + res(i, val); + }, + reject + ); + return; + } + } + args[i] = val; + if (--remaining === 0) { + resolve(args); + } + } catch (ex) { + reject(ex); + } + } + + for (let i = 0; i < args.length; i++) { + res(i, args[i]); + } + }); + }; + + Promise.allSettled = allSettled; + + Promise.resolve = function (value) { + if (value && typeof value === 'object' && value.constructor === Promise) { + return value; + } + + return new Promise(function (resolve) { + resolve(value); + }); + }; + + Promise.reject = function (value) { + return new Promise(function (_resolve, reject) { + reject(value); + }); + }; + + Promise.race = function (arr) { + return new Promise(function (resolve, reject) { + if (!isArray(arr)) { + return reject(new TypeError('Promise.race accepts an array')); + } + + for (let i = 0, len = arr.length; i < len; i++) { + Promise.resolve(arr[i]).then(resolve, reject); + } + }); + }; + + // Use polyfill for setImmediate for performance gains + Promise._immediateFn = + // @ts-ignore + (typeof setImmediate === 'function' && + function (fn) { + // @ts-ignore + setImmediate(fn); + }) || + function (fn) { + setTimeoutFunc(fn, 0); + }; + + Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) { + if (typeof console !== 'undefined' && console) { + console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console + } + }; + + return Promise; +})(); diff --git a/addon-test-support/@ember/test-helpers/-utils.ts b/addon-test-support/@ember/test-helpers/-utils.ts index ca66e1efa..ce1c9b15a 100644 --- a/addon-test-support/@ember/test-helpers/-utils.ts +++ b/addon-test-support/@ember/test-helpers/-utils.ts @@ -7,7 +7,7 @@ const HAS_PROMISE = // @ts-ignore this is checking if someone has explicitly done `window.Promise = window.Promise || Ember.RSVP.Promise Promise !== RSVP.Promise; -import { Promise as PromisePolyfill } from 'es6-promise'; +import PromisePolyfill from './-internal/promise-polyfill'; const _Promise: typeof Promise = HAS_PROMISE ? Promise : (PromisePolyfill as typeof Promise); diff --git a/package.json b/package.json index ef19b9d56..6a9c89782 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,9 @@ "@ember/test-waiters": "^2.3.2", "broccoli-debug": "^0.6.5", "broccoli-funnel": "^3.0.3", - "ember-auto-import": "^1.10.1", "ember-cli-babel": "^7.23.1", "ember-cli-htmlbars": "^5.2.0", - "ember-destroyable-polyfill": "^2.0.3", - "es6-promise": "^4.2.8" + "ember-destroyable-polyfill": "^2.0.3" }, "devDependencies": { "@ember/optional-features": "^2.0.0", @@ -55,6 +53,7 @@ "@typescript-eslint/parser": "^4.14.1", "broccoli-merge-trees": "^4.2.0", "documentation": "^13.1.0", + "ember-auto-import": "^1.10.1", "ember-cli": "~3.23.0", "ember-cli-dependency-checker": "^3.2.0", "ember-cli-inject-live-reload": "^2.0.2", diff --git a/tests/test-helper.js b/tests/test-helper.js index 82362e937..1cb11ad83 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -7,10 +7,11 @@ import { isSettled, getSettledState } from '@ember/test-helpers'; import { run } from '@ember/runloop'; import './helpers/resolver'; -import { polyfill } from 'es6-promise'; +import PromisePolyfill from '@ember/test-helpers/-internal/promise-polyfill'; +// This is needed for async/await transpilation :sob: if (typeof Promise === 'undefined') { - polyfill(); + window.Promise = PromisePolyfill; } if (QUnit.config.seed) { diff --git a/yarn.lock b/yarn.lock index aa2ae1dca..d7ed4fb4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6825,7 +6825,7 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-promise@^4.0.3, es6-promise@^4.2.8: +es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==