1
0
mirror of https://github.com/lancedikson/bowser synced 2026-09-22 12:05:23 +00:00
Files
lancedikson_bowser/test/acceptance/test-es5-runtime.js
naorpeled 4f59e5d25e test: guard ES5 runtime APIs and type-check consumers in CI
Follow-up to the ES5 syntax fix, closing the gaps that investigation left.

An ES5-only runtime sandbox. The acorn check catches syntax, but syntax is
only half the contract: preset-env lowers syntax and never polyfills library
calls, so one `Array.prototype.includes` in the parser source compiles
cleanly, passes every test on modern Node, and throws on the browsers es5.js
exists for. The new test runs both legacy bundles in a vm context with the
post-ES5.1 globals, statics and prototype methods deleted, and asserts
bundled.js additionally installs the polyfills its README entry promises.
Includes a test that the sandbox really strips, so it cannot quietly pass
against a modern global.

A consumer type-check across every module resolution mode, run in CI against
the packed tarball. attw already checks that types *resolve* per condition;
it compiles nothing, so it cannot catch a declaration that resolves correctly
and then misdescribes the runtime. Negative cases are asserted too — the maps
must stay non-importable as named exports, which is the line index.d.mts
draws deliberately and only a failing compile can hold.

Also documents two findings that were investigated and deliberately left
alone: the bundled.js size increase is the core-js 2 -> 3 upgrade rather than
waste, and `useBuiltIns: 'usage'` would shrink it by breaking the documented
"all needed polyfills" contract; and the src/*.js ESM-in-CJS wart (publint
warnings, Yarn PnP, Node < 20.19) is longstanding and identical on 2.14.1,
with the nested-package.json fix blocked on @babel/register.

Verified by breaking each guard in turn: an ES6 API call injected into es5.js
fails the sandbox test, and an index.d.mts with its `parse` export removed
fails all three ESM resolution modes while the CJS modes correctly still pass.
2026-08-30 21:59:08 +03:00

89 lines
4.2 KiB
JavaScript

import test from 'ava';
import fs from 'fs';
import path from 'path';
import vm from 'vm';
/**
* Runs the legacy bundles on a global object stripped back to ES5.1.
*
* `test-es5-conformance.js` checks *syntax*. This checks *runtime APIs*, which
* is a separate failure mode babel cannot protect against: `@babel/preset-env`
* lowers syntax, but without `useBuiltIns` it never polyfills library calls. A
* single `Array.prototype.includes` or `Object.assign` in the parser source
* compiles cleanly, passes every test on modern Node, and then throws
* `TypeError: undefined is not a function` on the old browsers `es5.js` targets.
*
* `es5.js` ships with no polyfills at all, so it has to survive here on its own.
* `bundled.js` carries core-js and has to install what it needs and still work.
*
* These are build outputs — run `pnpm build` before `pnpm test`.
*/
const root = path.join(__dirname, '..', '..');
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
+ '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
// Everything below postdates ES5.1. Not exhaustive — it covers the APIs a UA
// parser plausibly reaches for, which is what makes it a useful tripwire.
const ES6_GLOBALS = ['Promise', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Proxy', 'Reflect', 'globalThis', 'BigInt'];
const ES6_STATICS = {
Object: ['assign', 'entries', 'values', 'fromEntries', 'getOwnPropertySymbols', 'setPrototypeOf'],
Array: ['from', 'of'],
String: ['raw', 'fromCodePoint'],
Number: ['isInteger', 'isNaN', 'parseFloat', 'isFinite', 'EPSILON'],
Math: ['trunc', 'sign', 'log2', 'clz32'],
};
const ES6_PROTOS = {
Array: ['includes', 'find', 'findIndex', 'flat', 'flatMap', 'fill', 'copyWithin', 'at'],
String: ['includes', 'startsWith', 'endsWith', 'repeat', 'padStart', 'padEnd', 'trimStart', 'trimEnd', 'matchAll', 'at', 'normalize', 'codePointAt'],
};
function createEs5Context() {
const context = vm.createContext({});
// UMD bundles look for a global; `self` is the browser-shaped one.
vm.runInContext('this.self = this;', context);
const deletions = []
.concat(ES6_GLOBALS.map((g) => `this.${g}`))
.concat(...Object.entries(ES6_STATICS).map(([o, keys]) => keys.map((k) => `${o}.${k}`)))
.concat(...Object.entries(ES6_PROTOS).map(([o, keys]) => keys.map((k) => `${o}.prototype.${k}`)))
.map((ref) => `try { delete ${ref}; } catch (e) {}`)
.join('\n');
vm.runInContext(deletions, context);
return context;
}
test('the ES5 sandbox actually strips the modern APIs', (t) => {
// Guards the guard: if stripping silently stopped working, every assertion
// below would pass against a fully modern global and prove nothing.
const context = createEs5Context();
t.is(vm.runInContext('typeof Promise', context), 'undefined');
t.is(vm.runInContext('typeof Object.assign', context), 'undefined');
t.is(vm.runInContext('typeof [].includes', context), 'undefined');
t.is(vm.runInContext('typeof "".startsWith', context), 'undefined');
});
['es5.js', 'bundled.js'].forEach((file) => {
test(`${file} runs on an ES5-only global`, (t) => {
const context = createEs5Context();
const source = fs.readFileSync(path.join(root, file), 'utf8');
t.notThrows(() => vm.runInContext(source, context), `${file} threw while loading`);
t.is(vm.runInContext('typeof this.bowser', context), 'function');
context.__ua = UA;
t.is(vm.runInContext('this.bowser.parse(this.__ua).browser.name', context), 'Chrome');
t.is(vm.runInContext('this.bowser.parse(this.__ua).os.name', context), 'macOS');
t.true(vm.runInContext('this.bowser.getParser(this.__ua).satisfies({ chrome: ">100" })', context));
});
});
test('bundled.js installs the polyfills it promises', (t) => {
// The README tells consumers to reach for bundled.js when they have no
// polyfills of their own, so it has to actually populate the environment.
const context = createEs5Context();
vm.runInContext(fs.readFileSync(path.join(root, 'bundled.js'), 'utf8'), context);
t.is(vm.runInContext('typeof Promise', context), 'function');
t.is(vm.runInContext('typeof Object.assign', context), 'function');
t.is(vm.runInContext('typeof [].includes', context), 'function');
});