mirror of
https://github.com/lancedikson/bowser
synced 2026-09-22 12:05:23 +00:00
fix: restore ES5 output for bundled.js, and assert it
`bundled.js` stopped being ES5 when the webpack build was replaced by tsdown in #628. It parses at ecmaVersion 2015 but not 5: var t=(t,e)=>()=>(e||(t((e={exports:{}}).exports,e),t=null),e.exports) That is rolldown's `__commonJS` interop helper. `@rolldown/plugin-babel` only transforms input modules, and rolldown appends the helper afterwards; terser with `ecma: 5` avoids introducing newer syntax but does not transpile, so the arrow functions reached the published file. `es5.js` has no CommonJS dependencies and never gets the helper, which is why only `bundled.js` broke. The effect is total rather than partial: in an ES5 engine the whole script is a SyntaxError, so `bundled.js` — the bundle that exists specifically to serve those engines, polyfills included — does not load at all there. Lower the emitted chunk with a babel renderChunk pass that runs after bundling and before terser, so rolldown's own helpers are covered too. Costs 2.8 kB (+1.6%) on bundled.js; es5.js is unchanged at 34 kB. The existing guard was a grep for backticks, which this syntax slips straight past. Replace it with an acorn parse at ecmaVersion 5 over both legacy bundles, plus a tokeniser check for real template literals (backticks inside core-js string literals are fine, and 2.14.1 shipped three of them). Verified by reverting the build fix: `bundled.js parses as ES5` fails, and passes again once restored. Also confirmed bundled.js loads and parses the live navigator.userAgent in a real browser.
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
"@babel/register": "^7.29.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rolldown/plugin-babel": "0.2.3",
|
||||
"acorn": "^8.18.0",
|
||||
"ava": "^3.0.0",
|
||||
"babel-plugin-add-module-exports": "^1.0.4",
|
||||
"babel-plugin-istanbul": "^8.0.0",
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -42,6 +42,9 @@ importers:
|
||||
'@rolldown/plugin-babel':
|
||||
specifier: 0.2.3
|
||||
version: 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(rolldown@1.2.0)
|
||||
acorn:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
ava:
|
||||
specifier: ^3.0.0
|
||||
version: 3.15.0(supports-color@7.2.0)
|
||||
@@ -1330,8 +1333,8 @@ packages:
|
||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.17.0:
|
||||
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
|
||||
acorn@8.18.0:
|
||||
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5853,15 +5856,15 @@ snapshots:
|
||||
mime-types: 3.0.2
|
||||
negotiator: 1.0.0
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.17.0):
|
||||
acorn-jsx@5.3.2(acorn@8.18.0):
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
|
||||
acorn@8.17.0: {}
|
||||
acorn@8.18.0: {}
|
||||
|
||||
aggregate-error@3.1.0:
|
||||
dependencies:
|
||||
@@ -6006,7 +6009,7 @@ snapshots:
|
||||
ava@3.15.0(supports-color@7.2.0):
|
||||
dependencies:
|
||||
'@concordance/react': 2.0.0
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
acorn-walk: 8.3.5
|
||||
ansi-styles: 5.2.0
|
||||
arrgv: 1.0.2
|
||||
@@ -6993,14 +6996,14 @@ snapshots:
|
||||
|
||||
espree@10.4.0:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn-jsx: 5.3.2(acorn@8.17.0)
|
||||
acorn: 8.18.0
|
||||
acorn-jsx: 5.3.2(acorn@8.18.0)
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
espree@11.2.0:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
acorn-jsx: 5.3.2(acorn@8.17.0)
|
||||
acorn: 8.18.0
|
||||
acorn-jsx: 5.3.2(acorn@8.18.0)
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
esprima@4.0.1: {}
|
||||
@@ -8915,7 +8918,7 @@ snapshots:
|
||||
terser@5.49.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.17.0
|
||||
acorn: 8.18.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
|
||||
50
test/acceptance/test-es5-conformance.js
Normal file
50
test/acceptance/test-es5-conformance.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import test from 'ava';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as acorn from 'acorn';
|
||||
|
||||
/**
|
||||
* `es5.js` and `bundled.js` exist to serve browsers that predate ES2015. If a
|
||||
* single arrow function or template literal reaches either file, the whole
|
||||
* script is a SyntaxError there and bowser is not merely degraded, it is dead.
|
||||
*
|
||||
* A grep for backticks is not enough. When the webpack build was replaced by
|
||||
* tsdown, rolldown's `__commonJS` interop helper — appended *after* babel runs,
|
||||
* and left alone by terser, which avoids introducing new syntax but does not
|
||||
* transpile — shipped arrow functions into `bundled.js`:
|
||||
*
|
||||
* var t=(t,e)=>()=>(e||(t((e={exports:{}}).exports,e),t=null),e.exports)
|
||||
*
|
||||
* Parsing the emitted files at `ecmaVersion: 5` is the only check that covers
|
||||
* the whole file, including helpers no source-level transform ever sees.
|
||||
*
|
||||
* These are build outputs — run `pnpm build` before `pnpm test`.
|
||||
*/
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
|
||||
const legacyBundles = ['es5.js', 'bundled.js'];
|
||||
|
||||
legacyBundles.forEach((file) => {
|
||||
test(`${file} parses as ES5`, (t) => {
|
||||
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
||||
t.notThrows(
|
||||
() => acorn.parse(source, { ecmaVersion: 5 }),
|
||||
`${file} contains syntax newer than ES5 — it will throw on load in the `
|
||||
+ 'browsers this bundle exists to support',
|
||||
);
|
||||
});
|
||||
|
||||
test(`${file} contains no template literals`, (t) => {
|
||||
// Backticks inside string literals are fine (core-js has a few). Only a
|
||||
// real template-literal token is a problem, so tokenise rather than grep.
|
||||
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const templates = [...acorn.tokenizer(source, { ecmaVersion: 2020 })]
|
||||
.filter((token) => token.type.label === '`' || token.type.label === 'template');
|
||||
t.is(templates.length, 0, `${file} contains a template literal`);
|
||||
});
|
||||
});
|
||||
|
||||
test('bowser.mjs is a valid ES module', (t) => {
|
||||
const source = fs.readFileSync(path.join(root, 'bowser.mjs'), 'utf8');
|
||||
t.notThrows(() => acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' }));
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
import babel from '@rolldown/plugin-babel';
|
||||
import { minify } from 'terser';
|
||||
import { transformAsync } from '@babel/core';
|
||||
|
||||
const banner = `/*!
|
||||
* Bowser - a browser detector
|
||||
@@ -34,6 +35,51 @@ const legacyBabel = (useBuiltIns: false | 'entry') => babel({
|
||||
}]],
|
||||
});
|
||||
|
||||
/**
|
||||
* Lowers the *emitted chunk* to ES5, after bundling and before terser.
|
||||
*
|
||||
* `legacyBabel()` above only transforms input modules. Rolldown appends its own
|
||||
* runtime helpers afterwards — notably the `__commonJS` wrapper it injects for
|
||||
* CommonJS dependencies — and emits them in modern syntax:
|
||||
*
|
||||
* var t=(t,e)=>()=>(e||(t((e={exports:{}}).exports,e),t=null),e.exports)
|
||||
*
|
||||
* terser's `ecma: 5` does not transpile; it only avoids *introducing* newer
|
||||
* syntax. So those arrow functions survived into the published `bundled.js`,
|
||||
* making the whole file a SyntaxError in the ES5 engines it exists to serve.
|
||||
* `es5.js` has no CommonJS dependencies, so it never got a helper — which is
|
||||
* why only `bundled.js` was affected, and why this has to run on the output
|
||||
* rather than being folded into `legacyBabel()`.
|
||||
*
|
||||
* `useBuiltIns: false` here on purpose: `bundled.js` already has its polyfills
|
||||
* inlined by the input pass, and re-expanding them would recurse.
|
||||
*/
|
||||
const lowerChunkToEs5 = () => ({
|
||||
name: 'bowser:babel-output',
|
||||
async renderChunk(code: string, chunk: { fileName: string }) {
|
||||
const result = await transformAsync(code, {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
// The emitted chunk is a UMD IIFE, i.e. a script, not a module.
|
||||
sourceType: 'script',
|
||||
// core-js is large and already ES5; skipping its size guard keeps babel
|
||||
// from silently bailing out of compiling `bundled.js`.
|
||||
compact: false,
|
||||
generatorOpts: { comments: true },
|
||||
presets: [['@babel/preset-env', {
|
||||
modules: false,
|
||||
loose: true,
|
||||
useBuiltIns: false,
|
||||
targets: legacyTargets,
|
||||
}]],
|
||||
});
|
||||
if (typeof result?.code !== 'string') {
|
||||
throw new Error(`babel produced no output for ${chunk.fileName}`);
|
||||
}
|
||||
return { code: result.code };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Minifies the UMD chunks with terser instead of rolldown's built-in (oxc)
|
||||
* minifier.
|
||||
@@ -78,7 +124,7 @@ const umd = (name: string, entry: string, useBuiltIns: false | 'entry') => ({
|
||||
},
|
||||
outDir: '.',
|
||||
platform: 'browser' as const,
|
||||
plugins: [legacyBabel(useBuiltIns), terser()],
|
||||
plugins: [legacyBabel(useBuiltIns), lowerChunkToEs5(), terser()],
|
||||
// webpack ran in `mode: 'production'`; minification happens in `terser()`
|
||||
// above, so rolldown's own minifier stays off. See its comment for why.
|
||||
minify: false,
|
||||
|
||||
Reference in New Issue
Block a user