1
0
mirror of https://github.com/lancedikson/bowser synced 2026-10-18 00:54:17 +00:00

fix: keep ESM consumers on ES5, enable tree-shaking, trim bundled.js (#634)

This commit is contained in:
Naor Peled
2026-09-27 12:10:11 +03:00
committed by GitHub
parent c1208b4a56
commit fd556800ee
6 changed files with 119 additions and 23 deletions

View File

@@ -44,7 +44,19 @@ legacyBundles.forEach((file) => {
});
});
test('bowser.mjs is a valid ES module', (t) => {
/**
* `bowser.mjs` is what `import 'bowser'` resolves to in every bundler that
* reads the exports map. Before the map existed those same bundlers read the
* `browser` field and got `es5.js`, so ES5 builds — esbuild `--target=es5`,
* webpack without transpiling node_modules — depend on this file being ES5
* too. acorn's ES5 grammar in module mode accepts `import`/`export` and
* nothing else from ES2015, which is exactly the contract.
*/
test('bowser.mjs parses as ES5 apart from its export statement', (t) => {
const source = fs.readFileSync(path.join(root, 'bowser.mjs'), 'utf8');
t.notThrows(() => acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' }));
t.notThrows(
() => acorn.parse(source, { ecmaVersion: 5, sourceType: 'module' }),
'bowser.mjs contains syntax newer than ES5 — ES5 bundler targets will '
+ 'fail to build or ship it untranspiled',
);
});

View File

@@ -131,3 +131,51 @@ test('bundled.js installs the polyfills it promises', (t) => {
t.is(vm.runInContext('typeof Object.assign', context), 'function');
t.is(vm.runInContext('typeof [].includes', context), 'function');
});
/**
* What every released `bundled.js` installed, back when it was built from
* `@babel/polyfill`: ES2015, the handful of ES2016/2017 additions that package
* pulled in individually, the `timers`/`immediate`/DOM-iterable web polyfills,
* and `regenerator-runtime`. `build/entries/bundled.js` picks its core-js
* entries to stay a superset of this; trimming them must not break it.
*
* Representative rather than exhaustive — one or more members per polyfill
* group, so that dropping any group fails here.
*/
const BABEL_POLYFILL_SURFACE = [
'this.Promise', 'Promise.prototype.finally', 'this.Symbol', 'Symbol.iterator', 'Symbol.asyncIterator',
'this.Map', 'this.Set', 'this.WeakMap', 'this.WeakSet', 'this.Reflect', 'Reflect.ownKeys',
'Object.assign', 'Object.is', 'Object.setPrototypeOf', 'Object.getOwnPropertySymbols',
'Object.entries', 'Object.values', 'Object.getOwnPropertyDescriptors',
'Array.from', 'Array.of', 'Array.prototype.includes', 'Array.prototype.find', 'Array.prototype.findIndex',
'Array.prototype.fill', 'Array.prototype.copyWithin', 'Array.prototype.flatMap',
'Array.prototype.entries', 'Array.prototype.keys', 'Array.prototype.values',
'String.raw', 'String.fromCodePoint', 'String.prototype.includes', 'String.prototype.startsWith',
'String.prototype.endsWith', 'String.prototype.repeat', 'String.prototype.codePointAt',
'String.prototype.padStart', 'String.prototype.padEnd', 'String.prototype.trimStart', 'String.prototype.trimEnd',
// Not `Number.EPSILON`: it is non-configurable, so the sandbox cannot strip it.
'Number.isInteger', 'Number.isFinite', 'Number.isNaN', 'Number.isSafeInteger', 'Number.parseInt',
'Math.trunc', 'Math.sign', 'Math.log2', 'Math.clz32', 'Math.hypot',
'this.setImmediate', 'this.clearImmediate', 'this.regeneratorRuntime',
// core-js 2 made DOM collections iterable; `forEach` on them is core-js 3.
'NodeList.prototype[Symbol.iterator]',
];
test('bundled.js still installs everything @babel/polyfill did', (t) => {
const context = createEs5Context();
// Strip whatever on the list the ES5 sandbox left in place, and give core-js
// a DOM collection to patch, so the check cannot pass against Node's own
// implementations.
vm.runInContext(BABEL_POLYFILL_SURFACE
.filter((ref) => /^[\w.]+$/.test(ref))
.map((ref) => `try { delete ${ref}; } catch (e) {}`)
.join('\n'), context);
vm.runInContext('this.NodeList = function NodeList() {};', context);
const present = (ref) => {
try { return vm.runInContext(`typeof (${ref})`, context) !== 'undefined'; } catch (e) { return false; }
};
t.deepEqual(BABEL_POLYFILL_SURFACE.filter(present), [], 'the sandbox did not strip these');
vm.runInContext(fs.readFileSync(path.join(root, 'bundled.js'), 'utf8'), context);
t.deepEqual(BABEL_POLYFILL_SURFACE.filter((ref) => !present(ref)), [], 'bundled.js no longer installs these');
});

View File

@@ -101,6 +101,12 @@ check('require.resolve("bowser/package.json") works', function () {
assert.ok(fs.existsSync(require.resolve('bowser/package.json')));
});
// Extensionless, like `bowser/es5` — CommonJS resolution appended `.json`
// before the exports map existed.
check('require("bowser/package") works', function () {
assert.strictEqual(require('bowser/package').name, 'bowser');
});
// Before the exports map existed, *every* published file was reachable by
// subpath. An exports map is a closed list, so anything published but not
// enumerated silently becomes ERR_PACKAGE_PATH_NOT_EXPORTED. These four are
@@ -124,6 +130,14 @@ check('main/browser/module/types fields are unchanged', function () {
assert.strictEqual(pkg.types, 'index.d.ts');
});
// Lets esbuild and Vite drop an unused `import 'bowser'` (webpack and Rollup
// already do). `bundled.js` is the one file whose import *is* the point: it
// installs polyfills, so it must never be tree-shaken away.
check('"sideEffects" lists exactly bundled.js', function () {
var pkg = require('bowser/package.json');
assert.deepStrictEqual(pkg.sideEffects, ['./bundled.js']);
});
check('no "engines" field (would warn/fail installs on old Node)', function () {
var pkg = require('bowser/package.json');
assert.strictEqual(pkg.engines, undefined);