From 07ed34e7c2723a2d418f3d7bf1b2852f93c557c5 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 27 Apr 2022 21:28:21 -0700 Subject: [PATCH 01/15] add polyfill originals --- rollup/jsPolyfills/README.md | 17 +++++++ rollup/jsPolyfills/_asyncNullishCoalesce.js | 15 ++++++ rollup/jsPolyfills/_asyncOptionalChain.js | 48 +++++++++++++++++++ .../jsPolyfills/_asyncOptionalChainDelete.js | 9 ++++ rollup/jsPolyfills/_createNamedExportFrom.js | 10 ++++ rollup/jsPolyfills/_createStarExport.js | 19 ++++++++ rollup/jsPolyfills/_interopDefault.js | 10 ++++ rollup/jsPolyfills/_interopNamespace.js | 18 +++++++ rollup/jsPolyfills/_interopRequireDefault.js | 10 ++++ rollup/jsPolyfills/_interopRequireWildcard.js | 23 +++++++++ rollup/jsPolyfills/_nullishCoalesce.js | 15 ++++++ rollup/jsPolyfills/_optionalChain.js | 48 +++++++++++++++++++ rollup/jsPolyfills/_optionalChainDelete.js | 9 ++++ rollup/jsPolyfills/index.js | 12 +++++ rollup/npmHelpers.js | 4 ++ 15 files changed, 267 insertions(+) create mode 100644 rollup/jsPolyfills/README.md create mode 100644 rollup/jsPolyfills/_asyncNullishCoalesce.js create mode 100644 rollup/jsPolyfills/_asyncOptionalChain.js create mode 100644 rollup/jsPolyfills/_asyncOptionalChainDelete.js create mode 100644 rollup/jsPolyfills/_createNamedExportFrom.js create mode 100644 rollup/jsPolyfills/_createStarExport.js create mode 100644 rollup/jsPolyfills/_interopDefault.js create mode 100644 rollup/jsPolyfills/_interopNamespace.js create mode 100644 rollup/jsPolyfills/_interopRequireDefault.js create mode 100644 rollup/jsPolyfills/_interopRequireWildcard.js create mode 100644 rollup/jsPolyfills/_nullishCoalesce.js create mode 100644 rollup/jsPolyfills/_optionalChain.js create mode 100644 rollup/jsPolyfills/_optionalChainDelete.js create mode 100644 rollup/jsPolyfills/index.js diff --git a/rollup/jsPolyfills/README.md b/rollup/jsPolyfills/README.md new file mode 100644 index 000000000000..9fec3423eca7 --- /dev/null +++ b/rollup/jsPolyfills/README.md @@ -0,0 +1,17 @@ +## Javascript Polyfills + +This is a collection of syntax polyfills either copied directly from or heavily inspired by those used by [Rollup](https://github.com/rollup/rollup) and [Sucrase](https://github.com/alangpierce/sucrase). When either tool uses one of these polyfills during a build, it injects the function source code into each file needing the function, which can lead to a great deal of duplication. For our builds, we have therefore implemented something similar to [`tsc`'s `importHelpers` behavior](https://www.typescriptlang.org/tsconfig#importHelpers): Instead of leaving the polyfills injected in multiple places, we instead replace each injected function with an `import` or `require` statement. This directory is the location from which we import. For simplicity (and greater treeshaking ability when using tools which only work on a file-by-file level), each polyfill lives in its own file. + +During build, this directory is copied over to the root level of `@sentry/utils`, and the polyfills are compiled into esm and cjs versions. When the injected implementations are replaced by either `import` or `require` by our rollup plugin, each `import`/`require` pulls from the correct module format's folder. (In other words, the injected `import` statements import from `@sentry/utils/jsPolyfills/esm` and the injected `require` statements pull from `@sentry/utils/jsPolyfills/cjs`.) + +Note that not all polyfills are currently used by the SDK, but all are included here for future compatitibility, should they ever be needed. + +-------- + +_Code from both Rollup and Sucrase is used under the MIT license, copyright 2017 and 2012-2018, respectively._ + +_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._ diff --git a/rollup/jsPolyfills/_asyncNullishCoalesce.js b/rollup/jsPolyfills/_asyncNullishCoalesce.js new file mode 100644 index 000000000000..0bb5eba8e6da --- /dev/null +++ b/rollup/jsPolyfills/_asyncNullishCoalesce.js @@ -0,0 +1,15 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export async function _asyncNullishCoalesce(lhs, rhsFn) { + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return lhs != null ? lhs : rhsFn(); +} + +// Sucrase version: +// async function _asyncNullishCoalesce(lhs, rhsFn) { +// if (lhs != null) { +// return lhs; +// } else { +// return await rhsFn(); +// } +// } diff --git a/rollup/jsPolyfills/_asyncOptionalChain.js b/rollup/jsPolyfills/_asyncOptionalChain.js new file mode 100644 index 000000000000..4bdc8f127437 --- /dev/null +++ b/rollup/jsPolyfills/_asyncOptionalChain.js @@ -0,0 +1,48 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export async function _asyncOptionalChain(ops) { + let lastAccessLHS = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i]; + const fn = ops[i + 1]; + i += 2; + // by checking for loose equality to `null`, we catch both `null` and `undefined` + if (op in ['optionalAccess', 'optionalCall'] && value == null) { + // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it + return; + } + if (op in ['access', 'optionalAccess']) { + lastAccessLHS = value; + value = await fn(value); + } else if (op in ['call', 'optionalCall']) { + value = await fn((...args) => value.call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// Sucrase version: +// async function _asyncOptionalChain(ops) { +// let lastAccessLHS = undefined; +// let value = ops[0]; +// let i = 1; +// while (i < ops.length) { +// const op = ops[i]; +// const fn = ops[i + 1]; +// i += 2; +// if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { +// return undefined; +// } +// if (op === 'access' || op === 'optionalAccess') { +// lastAccessLHS = value; +// value = await fn(value); +// } else if (op === 'call' || op === 'optionalCall') { +// value = await fn((...args) => value.call(lastAccessLHS, ...args)); +// lastAccessLHS = undefined; +// } +// } +// return value; +// } diff --git a/rollup/jsPolyfills/_asyncOptionalChainDelete.js b/rollup/jsPolyfills/_asyncOptionalChainDelete.js new file mode 100644 index 000000000000..cdabfc17cb10 --- /dev/null +++ b/rollup/jsPolyfills/_asyncOptionalChainDelete.js @@ -0,0 +1,9 @@ +// originally from Sucrase (https://github.com/alangpierce/sucrase) + +import { _asyncOptionalChain } from './_asyncOptionalChain'; + +export async function _asyncOptionalChainDelete(ops) { + const result = await _asyncOptionalChain(ops); + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return result == null ? true : result; +} diff --git a/rollup/jsPolyfills/_createNamedExportFrom.js b/rollup/jsPolyfills/_createNamedExportFrom.js new file mode 100644 index 000000000000..41d2ac450a82 --- /dev/null +++ b/rollup/jsPolyfills/_createNamedExportFrom.js @@ -0,0 +1,10 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export function _createNamedExportFrom(obj, localName, importedName) { + exports[localName] = obj[importedName]; +} + +// Sucrase version: +// function _createNamedExportFrom(obj, localName, importedName) { +// Object.defineProperty(exports, localName, {enumerable: true, get: () => obj[importedName]}); +// } diff --git a/rollup/jsPolyfills/_createStarExport.js b/rollup/jsPolyfills/_createStarExport.js new file mode 100644 index 000000000000..c2301b537da3 --- /dev/null +++ b/rollup/jsPolyfills/_createStarExport.js @@ -0,0 +1,19 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export function _createStarExport(obj) { + Object.keys(obj) + .filter(key => key !== 'default' && key !== '__esModule' && !(key in exports)) + .forEach(key => (exports[key] = obj[key])); +} + +// Sucrase version: +// function _createStarExport(obj) { +// Object.keys(obj) +// .filter(key => key !== 'default' && key !== '__esModule') +// .forEach(key => { +// if (exports.hasOwnProperty(key)) { +// return; +// } +// Object.defineProperty(exports, key, { enumerable: true, get: () => obj[key] }); +// }); +// } diff --git a/rollup/jsPolyfills/_interopDefault.js b/rollup/jsPolyfills/_interopDefault.js new file mode 100644 index 000000000000..99f13aff6eaa --- /dev/null +++ b/rollup/jsPolyfills/_interopDefault.js @@ -0,0 +1,10 @@ +// adapted from Rollup (https://github.com/rollup/rollup) + +export function _interopDefault(importTarget) { + return importTarget.__esModule ? importTarget.default : importTarget; +} + +// Rollup version: +// function _interopDefault(e) { +// return e && e.__esModule ? e['default'] : e; +// } diff --git a/rollup/jsPolyfills/_interopNamespace.js b/rollup/jsPolyfills/_interopNamespace.js new file mode 100644 index 000000000000..15cbbb0b06d9 --- /dev/null +++ b/rollup/jsPolyfills/_interopNamespace.js @@ -0,0 +1,18 @@ +// adapted from Rollup (https://github.com/rollup/rollup) + +export function _interopNamespace(importTarget) { + return importTarget.__esModule ? importTarget : { ...importTarget, default: importTarget }; +} + +// Rollup version (with `output.externalLiveBindings` and `output.freeze` both set to false) +// function _interopNamespace(e) { +// if (e && e.__esModule) return e; +// var n = Object.create(null); +// if (e) { +// for (var k in e) { +// n[k] = e[k]; +// } +// } +// n["default"] = e; +// return n; +// } diff --git a/rollup/jsPolyfills/_interopRequireDefault.js b/rollup/jsPolyfills/_interopRequireDefault.js new file mode 100644 index 000000000000..6eb6199ee11f --- /dev/null +++ b/rollup/jsPolyfills/_interopRequireDefault.js @@ -0,0 +1,10 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export function _interopRequireDefault(importTarget) { + return importTarget.__esModule ? importTarget : { default: importTarget }; +} + +// Sucrase version +// function _interopRequireDefault(obj) { +// return obj && obj.__esModule ? obj : { default: obj }; +// } diff --git a/rollup/jsPolyfills/_interopRequireWildcard.js b/rollup/jsPolyfills/_interopRequireWildcard.js new file mode 100644 index 000000000000..577296e83826 --- /dev/null +++ b/rollup/jsPolyfills/_interopRequireWildcard.js @@ -0,0 +1,23 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export function _interopRequireWildcard(importTarget) { + return importTarget.__esModule ? importTarget : { ...importTarget, default: importTarget }; +} + +// Sucrase version +// function _interopRequireWildcard(obj) { +// if (obj && obj.__esModule) { +// return obj; +// } else { +// var newObj = {}; +// if (obj != null) { +// for (var key in obj) { +// if (Object.prototype.hasOwnProperty.call(obj, key)) { +// newObj[key] = obj[key]; +// } +// } +// } +// newObj.default = obj; +// return newObj; +// } +// } diff --git a/rollup/jsPolyfills/_nullishCoalesce.js b/rollup/jsPolyfills/_nullishCoalesce.js new file mode 100644 index 000000000000..5a1474edaa95 --- /dev/null +++ b/rollup/jsPolyfills/_nullishCoalesce.js @@ -0,0 +1,15 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export function _nullishCoalesce(lhs, rhsFn) { + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return lhs != null ? lhs : rhsFn(); +} + +// Sucrase version +// function _nullishCoalesce(lhs, rhsFn) { +// if (lhs != null) { +// return lhs; +// } else { +// return rhsFn(); +// } +// } diff --git a/rollup/jsPolyfills/_optionalChain.js b/rollup/jsPolyfills/_optionalChain.js new file mode 100644 index 000000000000..bb8105e792df --- /dev/null +++ b/rollup/jsPolyfills/_optionalChain.js @@ -0,0 +1,48 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +export function _optionalChain(ops) { + let lastAccessLHS = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i]; + const fn = ops[i + 1]; + i += 2; + // by checking for loose equality to `null`, we catch both `null` and `undefined` + if (op in ['optionalAccess', 'optionalCall'] && value == null) { + // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it + return; + } + if (op in ['access', 'optionalAccess']) { + lastAccessLHS = value; + value = fn(value); + } else if (op in ['call', 'optionalCall']) { + value = fn((...args) => value.call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// Sucrase version +// function _optionalChain(ops) { +// let lastAccessLHS = undefined; +// let value = ops[0]; +// let i = 1; +// while (i < ops.length) { +// const op = ops[i]; +// const fn = ops[i + 1]; +// i += 2; +// if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { +// return undefined; +// } +// if (op === 'access' || op === 'optionalAccess') { +// lastAccessLHS = value; +// value = fn(value); +// } else if (op === 'call' || op === 'optionalCall') { +// value = fn((...args) => value.call(lastAccessLHS, ...args)); +// lastAccessLHS = undefined; +// } +// } +// return value; +// } diff --git a/rollup/jsPolyfills/_optionalChainDelete.js b/rollup/jsPolyfills/_optionalChainDelete.js new file mode 100644 index 000000000000..60f0f90d5b0a --- /dev/null +++ b/rollup/jsPolyfills/_optionalChainDelete.js @@ -0,0 +1,9 @@ +// originally from Sucrase (https://github.com/alangpierce/sucrase) + +import { _optionalChain } from './_optionalChain'; + +export function _optionalChainDelete(ops) { + const result = _optionalChain(ops); + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return result == null ? true : result; +} diff --git a/rollup/jsPolyfills/index.js b/rollup/jsPolyfills/index.js new file mode 100644 index 000000000000..f1c46088f61b --- /dev/null +++ b/rollup/jsPolyfills/index.js @@ -0,0 +1,12 @@ +export { _asyncNullishCoalesce } from './_asyncNullishCoalesce'; +export { _asyncOptionalChain } from './_asyncOptionalChain'; +export { _asyncOptionalChainDelete } from './_asyncOptionalChainDelete'; +export { _createNamedExportFrom } from './_createNamedExportFrom'; +export { _createStarExport } from './_createStarExport'; +export { _interopDefault } from './_interopDefault'; +export { _interopNamespace } from './_interopNamespace'; +export { _interopRequireDefault } from './_interopRequireDefault'; +export { _interopRequireWildcard } from './_interopRequireWildcard'; +export { _nullishCoalesce } from './_nullishCoalesce'; +export { _optionalChain } from './_optionalChain'; +export { _optionalChainDelete } from './_optionalChainDelete'; diff --git a/rollup/npmHelpers.js b/rollup/npmHelpers.js index 79a14189a880..440223c7e757 100644 --- a/rollup/npmHelpers.js +++ b/rollup/npmHelpers.js @@ -59,6 +59,10 @@ export function makeBaseNPMConfig(options = {}) { // }); externalLiveBindings: false, + // Don't call `Object.freeze` on the results of `import * as someModule from '...'` + // (We don't need it, so why waste the bytes?) + freeze: false, + // Equivalent to `esModuleInterop` in tsconfig. // Controls whether rollup emits helpers to handle special cases where turning // `import * as dogs from 'dogs'` From 18ee6a46c536ec92d84f1d61e3ff6b90502be957 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 27 Apr 2022 21:30:39 -0700 Subject: [PATCH 02/15] add acorn and recast --- package.json | 2 ++ yarn.lock | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/package.json b/package.json index f2b81a98123e..3d2a67cea99f 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@types/mocha": "^5.2.0", "@types/node": "~10.17.0", "@types/sinon": "^7.0.11", + "acorn": "^8.7.0", "chai": "^4.1.2", "codecov": "^3.6.5", "deepmerge": "^4.2.2", @@ -81,6 +82,7 @@ "mocha": "^6.1.4", "npm-run-all": "^4.1.5", "prettier": "2.5.1", + "recast": "^0.20.5", "replace-in-file": "^4.0.0", "rimraf": "^3.0.2", "rollup": "^2.67.1", diff --git a/yarn.lock b/yarn.lock index bf506395377a..450364bcbec1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5772,6 +5772,11 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== +acorn@^8.7.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + adjust-sourcemap-loader@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e" @@ -6272,6 +6277,13 @@ ast-types@0.13.3: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7" integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA== +ast-types@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" + integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== + dependencies: + tslib "^2.0.1" + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -21204,6 +21216,16 @@ recast@^0.18.1: private "^0.1.8" source-map "~0.6.1" +recast@^0.20.5: + version "0.20.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.20.5.tgz#8e2c6c96827a1b339c634dd232957d230553ceae" + integrity sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ== + dependencies: + ast-types "0.14.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -24347,6 +24369,11 @@ tslib@^2.0.0, tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.0.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^2.3.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" From 89a1c227f812de316fbf10ac693e2d9a3fa0110d Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 2 May 2022 15:24:35 -0700 Subject: [PATCH 03/15] create plugin for extracting polyfills --- rollup/npmHelpers.js | 11 +- rollup/plugins/extractPolyfillsPlugin.js | 208 +++++++++++++++++++++++ rollup/plugins/npmPlugins.js | 2 + 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 rollup/plugins/extractPolyfillsPlugin.js diff --git a/rollup/npmHelpers.js b/rollup/npmHelpers.js index 440223c7e757..0ac0ad33a01b 100644 --- a/rollup/npmHelpers.js +++ b/rollup/npmHelpers.js @@ -9,6 +9,7 @@ import deepMerge from 'deepmerge'; import { makeConstToVarPlugin, + makeExtractPolyfillsPlugin, makeNodeResolvePlugin, makeRemoveBlankLinesPlugin, makeRemoveESLintCommentsPlugin, @@ -30,6 +31,7 @@ export function makeBaseNPMConfig(options = {}) { const constToVarPlugin = makeConstToVarPlugin(); const removeESLintCommentsPlugin = makeRemoveESLintCommentsPlugin(); const removeBlankLinesPlugin = makeRemoveBlankLinesPlugin(); + const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); // return { const config = { @@ -75,7 +77,14 @@ export function makeBaseNPMConfig(options = {}) { interop: esModuleInterop ? 'auto' : 'esModule', }, - plugins: [nodeResolvePlugin, sucrasePlugin, constToVarPlugin, removeESLintCommentsPlugin, removeBlankLinesPlugin], + plugins: [ + nodeResolvePlugin, + sucrasePlugin, + constToVarPlugin, + removeESLintCommentsPlugin, + removeBlankLinesPlugin, + extractPolyfillsPlugin, + ], // don't include imported modules from outside the package in the final output external: [ diff --git a/rollup/plugins/extractPolyfillsPlugin.js b/rollup/plugins/extractPolyfillsPlugin.js new file mode 100644 index 000000000000..8c527bb94c34 --- /dev/null +++ b/rollup/plugins/extractPolyfillsPlugin.js @@ -0,0 +1,208 @@ +import * as path from 'path'; + +import * as recast from 'recast'; +import * as acornParser from 'recast/parsers/acorn'; + +const POLYFILL_NAMES = new Set([ + '_asyncNullishCoalesce', + '_asyncOptionalChain', + '_asyncOptionalChainDelete', + '_createNamedExportFrom', + '_createStarExport', + '_interopDefault', // rollup's version + '_interopNamespace', // rollup's version + '_interopRequireDefault', // sucrase's version + '_interopRequireWildcard', // sucrase's version + '_nullishCoalesce', + '_optionalChain', + '_optionalChainDelete', +]); + +/** + * Create a plugin which will replace function definitions of any of the above funcions with an `import` or `require` + * statement pulling them in from a central source. Mimics tsc's `importHelpers` option. + */ +export function makeExtractPolyfillsPlugin() { + let moduleFormat; + + // For more on the hooks used in this plugin, see https://rollupjs.org/guide/en/#output-generation-hooks + return { + name: 'extractPolyfills', + + // Figure out which build we're currently in (esm or cjs) + outputOptions(options) { + moduleFormat = options.format; + }, + + // This runs after both the sucrase transpilation (which happens in the `transform` hook) and rollup's own + // esm-i-fying or cjs-i-fying work (which happens right before `renderChunk`), in other words, after all polyfills + // will have been injected + renderChunk(code, chunk) { + const sourceFile = chunk.fileName; + const parserOptions = { + sourceFileName: sourceFile, + // We supply a custom parser which wraps the provided `acorn` parser in order to override the `ecmaVersion` value. + // See https://github.com/benjamn/recast/issues/578. + parser: { + parse(source, options) { + return acornParser.parse(source, { + ...options, + // By this point in the build, everything should already have been down-compiled to whatever JS version + // we're targeting. Setting this parser to `latest` just means that whatever that version is (or changes + // to in the future), this parser will be able to handle the generated code. + ecmaVersion: 'latest', + }); + }, + }, + }; + + const ast = recast.parse(code, parserOptions); + + // Find function definitions and function expressions whose identifiers match a known polyfill name + const polyfillNodes = findPolyfillNodes(ast); + + if (polyfillNodes.length === 0) { + return null; + } + + console.log(`${sourceFile} - polyfills: ${polyfillNodes.map(node => node.name)}`); + + // Depending on the output format, generate `import { x, y, z } from '...'` or `var { x, y, z } = require('...')` + const importOrRequireNode = createImportOrRequireNode(polyfillNodes, sourceFile, moduleFormat); + + // Insert our new `import` or `require` node at the top of the file, and then delete the function definitions it's + // meant to replace (polyfill nodes get marked for deletion in `findPolyfillNodes`) + ast.program.body = [importOrRequireNode, ...ast.program.body.filter(node => !node.shouldDelete)]; + + // In spite of the name, this doesn't actually print anything - it just stringifies the code, and keeps track of + // where original nodes end up in order to generate a sourcemap. + const result = recast.print(ast, { + sourceMapName: `${sourceFile}.map`, + quote: 'single', + }); + + return { code: result.code, map: result.map }; + }, + }; +} + +/** + * Extract the function name, regardless of the format in which the function is declared + */ +function getNodeName(node) { + // Function expressions and functions pulled from objects + if (node.type === 'VariableDeclaration') { + // In practice sucrase and rollup only ever declare one polyfill at a time, so it's safe to just grab the first + // entry here + const declarationId = node.declarations[0].id; + + // Note: Sucrase and rollup seem to only use the first type of variable declaration for their polyfills, but good to + // cover our bases + + // Declarations of the form + // `const dogs = function() { return "are great"; };` + // or + // `const dogs = () => "are great"; + if (declarationId.type === 'Identifier') { + return declarationId.name; + } + // Declarations of the form + // `const { dogs } = { dogs: function() { return "are great"; } }` + // or + // `const { dogs } = { dogs: () => "are great" }` + else if (declarationId.type === 'ObjectPattern') { + return declarationId.properties[0].key.name; + } + // Any other format + else { + return 'unknown variable'; + } + } + + // Regular old functions, of the form + // `function dogs() { return "are great"; }` + else if (node.type === 'FunctionDeclaration') { + return node.id.name; + } + + // If we get here, this isn't a node we're interested in, so just return a string we know will never match any of the + // polyfill names + else { + return 'nope'; + } +} + +/** + * Find all nodes whose identifiers match a known polyfill name. + * + * Note: In theory, this could yield false positives, if any of the magic names were assigned to something other than a + * polyfill function, but the chances of that are slim. Also, it only searches the module global scope, but that's + * always where the polyfills appear, so no reason to traverse the whole tree. + */ +function findPolyfillNodes(ast) { + const isPolyfillNode = node => { + const nodeName = getNodeName(node); + if (POLYFILL_NAMES.has(nodeName)) { + // Mark this node for later deletion, since we're going to replace it with an import statement + node.shouldDelete = true; + // Store the name in a consistent spot, regardless of node type + node.name = nodeName; + + return true; + } + + return false; + }; + + return ast.program.body.filter(isPolyfillNode); +} + +/** + * Create a node representing an `import` or `require` statement of the form + * + * import { < polyfills > } from '...' + * or + * var { < polyfills > } = require('...') + * + * @param polyfillNodes The nodes from the current version of the code, defining the polyfill functions + * @param currentSourceFile The path, relative to `src/`, of the file currently being transpiled + * @param moduleFormat Either 'cjs' or 'esm' + * @returns A single node which can be subbed in for the polyfill definition nodes + */ +function createImportOrRequireNode(polyfillNodes, currentSourceFile, moduleFormat) { + const { + callExpression, + identifier, + importDeclaration, + importSpecifier, + literal, + objectPattern, + property, + variableDeclaration, + variableDeclarator, + } = recast.types.builders; + + // Since our polyfills live in `@sentry/utils`, if we're importing or requiring them there the path will have to be + // relative + const isUtilsPackage = process.cwd().endsWith('packages/utils'); + const importSource = literal( + isUtilsPackage + ? path.relative(path.dirname(currentSourceFile), `../jsPolyfills/${moduleFormat}`) + : `@sentry/utils/jsPolyfills/${moduleFormat}`, + ); + + // This is the `x, y, z` of inside of `import { x, y, z }` or `var { x, y, z }` + const importees = polyfillNodes.map(({ name: fnName }) => + moduleFormat === 'esm' + ? importSpecifier(identifier(fnName)) + : property.from({ kind: 'init', key: identifier(fnName), value: identifier(fnName), shorthand: true }), + ); + + const requireFn = identifier('require'); + + return moduleFormat === 'esm' + ? importDeclaration(importees, importSource) + : variableDeclaration('var', [ + variableDeclarator(objectPattern(importees), callExpression(requireFn, [importSource])), + ]); +} diff --git a/rollup/plugins/npmPlugins.js b/rollup/plugins/npmPlugins.js index 568ef6b69e10..eec8eab81cda 100644 --- a/rollup/plugins/npmPlugins.js +++ b/rollup/plugins/npmPlugins.js @@ -97,3 +97,5 @@ export function makeRemoveBlankLinesPlugin() { ], }); } + +export { makeExtractPolyfillsPlugin } from './extractPolyfillsPlugin.js'; From aab38fa6b27246cf2908b146124daeaf8a65ec66 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 2 May 2022 16:19:38 -0700 Subject: [PATCH 04/15] create rollup config for processing polyfills --- packages/utils/rollup.polyfills.config.js | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/utils/rollup.polyfills.config.js diff --git a/packages/utils/rollup.polyfills.config.js b/packages/utils/rollup.polyfills.config.js new file mode 100644 index 000000000000..4eb1d8a139ac --- /dev/null +++ b/packages/utils/rollup.polyfills.config.js @@ -0,0 +1,28 @@ +// TODO: Swich out the sucrase hack for the real, currently-commented-out code once we switch sucrase builds on. + +// export default ['esm', 'cjs'].map(format => ({ +export default ['esm', 'cjs'].map(format => { + const config = { + input: '../../rollup/jsPolyfills/index.js', + output: { + // preserveModules: true, + dir: `jsPolyfills/${format}`, + format, + strict: false, + }, + }; + // })); + + // temporary hack for testing sucrase bundles before we switch over + if (!process.version.startsWith('v8')) { + // eslint-disable-next-line no-console + console.log('Doing normal preserveModules in polyfill config'); + config.output.preserveModules = true; + } else { + // eslint-disable-next-line no-console + console.log('Doing node 8 preserveModules in polyfill config'); + config.preserveModules = true; + } + + return config; +}); From 672520f746819ef772a2dc277092cdfc093b7467 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 27 Apr 2022 21:27:41 -0700 Subject: [PATCH 05/15] add polyfill processing to `build:rollup` --- packages/utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index d67412868965..a0f709202cb5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -28,7 +28,7 @@ "build:dev": "run-s build", "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", "build:esm": "tsc -p tsconfig.esm.json", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:rollup": "rollup -c rollup.npm.config.js && rollup -c rollup.polyfills.config.js && cp ../../rollup/jsPolyfills/README.md jsPolyfills", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", From 3bcedff852a2a35d9b7b180ff6f1b721f4acf2fd Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 20 Apr 2022 20:32:06 -0700 Subject: [PATCH 06/15] make all packages depend on @sentry/utils --- packages/gatsby/package.json | 1 + packages/wasm/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 50811fce6b03..504b34015793 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -22,6 +22,7 @@ "dependencies": { "@sentry/react": "7.0.0-beta.0", "@sentry/tracing": "7.0.0-beta.0", + "@sentry/utils": "7.0.0-beta.0", "@sentry/webpack-plugin": "1.18.9" }, "peerDependencies": { diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 831d45671b0c..0dfffd50df12 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -18,6 +18,7 @@ "dependencies": { "@sentry/browser": "7.0.0-beta.0", "@sentry/types": "7.0.0-beta.0", + "@sentry/utils": "7.0.0-beta.0", "tslib": "^1.9.3" }, "devDependencies": { From 951ca5f520f20e36e9d48438a1948e4f066869cb Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 27 Apr 2022 21:25:30 -0700 Subject: [PATCH 07/15] add built polyfills to gitignore --- packages/utils/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/utils/.gitignore diff --git a/packages/utils/.gitignore b/packages/utils/.gitignore new file mode 100644 index 000000000000..7bbaced569d9 --- /dev/null +++ b/packages/utils/.gitignore @@ -0,0 +1 @@ +jsPolyfills/ From dd5f15f0d9528c70341e570614fb8b2f765f6530 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Fri, 29 Apr 2022 20:59:53 -0700 Subject: [PATCH 08/15] remove built polyfills when cleaning --- packages/utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index a0f709202cb5..583d2764bdea 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -39,7 +39,7 @@ "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage", + "clean": "rimraf build coverage jsPolyfills", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", From fc8012c93f14501e3f2d4244f5b219f83a9555a6 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 25 Apr 2022 13:41:29 -0700 Subject: [PATCH 09/15] don't lint built jsPolyfills --- packages/utils/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/utils/.eslintrc.js b/packages/utils/.eslintrc.js index 5a2cc7f1ec08..3f722edfaed4 100644 --- a/packages/utils/.eslintrc.js +++ b/packages/utils/.eslintrc.js @@ -1,3 +1,4 @@ module.exports = { extends: ['../../.eslintrc.js'], + ignorePatterns: ['jsPolyfills/**'], }; From 9257613a241cdc85baa3162b738d027689e0cb8c Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 4 May 2022 13:45:56 -0700 Subject: [PATCH 10/15] STASH add tests --- rollup/jsPolyfills/originals.js | 140 ++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 rollup/jsPolyfills/originals.js diff --git a/rollup/jsPolyfills/originals.js b/rollup/jsPolyfills/originals.js new file mode 100644 index 000000000000..87b9783e6540 --- /dev/null +++ b/rollup/jsPolyfills/originals.js @@ -0,0 +1,140 @@ +// Originals of the jsPolyfills from Sucrase and Rollup we use (which we have adapted in various ways), preserved here for testing, to prove that +// the modified versions do the same thing the originals do. + +// From Sucrase +export async function _asyncNullishCoalesce(lhs, rhsFn) { + if (lhs != null) { + return lhs; + } else { + return await rhsFn(); + } +} + +// From Sucrase +export async function _asyncOptionalChain(ops) { + let lastAccessLHS = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i]; + const fn = ops[i + 1]; + i += 2; + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + return undefined; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = await fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = await fn((...args) => value.call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// From Sucrase +export async function _asyncOptionalChainDelete(ops) { + const result = await _asyncOptionalChain(ops); + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return result == null ? true : result; +} + +// From Sucrase +export function createNamedExportFrom(obj, localName, importedName) { + Object.defineProperty(exports, localName, { enumerable: true, get: () => obj[importedName] }); +} + +// From Sucrase +export function _createStarExport(obj) { + Object.keys(obj) + .filter(key => key !== 'default' && key !== '__esModule') + .forEach(key => { + // eslint-disable-next-line no-prototype-builtins + if (exports.hasOwnProperty(key)) { + return; + } + Object.defineProperty(exports, key, { enumerable: true, get: () => obj[key] }); + }); +} + +// From Rollup +export function _interopDefault(e) { + return e && e.__esModule ? e['default'] : e; +} + +// From Rollup +export function _interopNamespace(e) { + if (e && e.__esModule) return e; + var n = Object.create(null); + if (e) { + // eslint-disable-next-line guard-for-in + for (var k in e) { + n[k] = e[k]; + } + } + n['default'] = e; + return n; +} + +// From Sucrase +export function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} + +// From Sucrase +export function _interopRequireWildcard(obj) { + if (obj && obj.__esModule) { + return obj; + } else { + var newObj = {}; + if (obj != null) { + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + return newObj; + } +} + +// From Sucrase +export function _nullishCoalesce(lhs, rhsFn) { + if (lhs != null) { + return lhs; + } else { + return rhsFn(); + } +} + +// From Sucrase +export function _optionalChain(ops) { + let lastAccessLHS = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i]; + const fn = ops[i + 1]; + i += 2; + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + return undefined; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = fn((...args) => value.call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// From Sucrase +export function _optionalChainDelete(ops) { + const result = _optionalChain(ops); + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return result == null ? true : result; +} From 992b03aaf5a135fda16e127b6eef328f6755ff26 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 27 Apr 2022 21:27:41 -0700 Subject: [PATCH 11/15] lint utils scripts --- packages/utils/.eslintrc.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/utils/.eslintrc.js b/packages/utils/.eslintrc.js index 3f722edfaed4..f0112704e4a1 100644 --- a/packages/utils/.eslintrc.js +++ b/packages/utils/.eslintrc.js @@ -1,4 +1,12 @@ module.exports = { extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['scripts/**/*.ts'], + parserOptions: { + project: ['../../tsconfig.dev.json'], + }, + }, + ], ignorePatterns: ['jsPolyfills/**'], }; From 7b8bc0271d4d01bddc066a3266d22df4ec3247cd Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 27 Apr 2022 21:26:55 -0700 Subject: [PATCH 12/15] allowlist built polyfills in npmignore --- packages/utils/.npmignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/utils/.npmignore b/packages/utils/.npmignore index 329293958886..81ff5ae73ae7 100644 --- a/packages/utils/.npmignore +++ b/packages/utils/.npmignore @@ -13,3 +13,6 @@ !/build/cjs/**/* !/build/esm/**/* !/build/types/**/* + +# polyfills for language features and import helpers +!jsPolyfills/**/* From d86f012d9d8e0374f93b95ca35a27ba0dd1f0fd8 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Tue, 3 May 2022 17:22:38 -0700 Subject: [PATCH 13/15] add fs-extra --- package.json | 1 + yarn.lock | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/package.json b/package.json index 3d2a67cea99f..a1630f24cdfc 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "codecov": "^3.6.5", "deepmerge": "^4.2.2", "eslint": "7.32.0", + "fs-extra": "^10.1.0", "jest": "^27.5.1", "jest-environment-node": "^27.5.1", "jsdom": "^19.0.0", diff --git a/yarn.lock b/yarn.lock index 450364bcbec1..faa60565f6b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13030,6 +13030,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^4.0.2, fs-extra@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" From 1b52cc85b6c1b211ef14a562adf85930dc1fdb66 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 20 Apr 2022 23:51:59 -0700 Subject: [PATCH 14/15] add prepack script --- packages/utils/scripts/prepack.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/utils/scripts/prepack.ts diff --git a/packages/utils/scripts/prepack.ts b/packages/utils/scripts/prepack.ts new file mode 100644 index 000000000000..f7712d99fb19 --- /dev/null +++ b/packages/utils/scripts/prepack.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-console */ + +// DO NOT RUN this script yourself! +// This is invoked from the main `prepack.ts` script in `sentry-javascript/scripts/prepack.ts`. + +import * as fs from 'fs'; +import * as fse from 'fs-extra'; +import * as path from 'path'; + +export function prepack(buildDir: string): boolean { + // copy package-specific assets to build dir + const assetPath = path.resolve('jsPolyfills'); + const destinationPath = path.resolve(buildDir, 'jsPolyfills'); + try { + if (!fs.existsSync(assetPath)) { + console.error( + "\nERROR: Missing 'packages/utils/jsPolyfills' directory. Please run `yarn build` in the `utils` package before running this script again.", + ); + return false; + } + console.log(`Copying jsPolyfills to ${path.relative('../..', destinationPath)}.`); + fse.copySync(assetPath, destinationPath); + } catch (error) { + console.error(`\nERROR: Error while copying jsPolyfills to ${path.relative('../..', destinationPath)}:\n${error}`); + return false; + } + return true; +} From 9b564f8caddb3725ea205c093edc183e6f2fd524 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 25 Apr 2022 19:09:59 -0700 Subject: [PATCH 15/15] add npm tarballs to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 430a977178a7..be80e41d00b7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ scratch/ scenarios/*/dist/ # transpiled transformers jest/transformers/*.js +# node tarballs +packages/*/sentry-*.tgz # logs yarn-error.log