1
0
mirror of https://github.com/lancedikson/bowser synced 2026-10-07 03:11:53 +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:
naorpeled
2026-08-30 21:45:38 +03:00
parent d5a861e8ad
commit b080b831db
4 changed files with 113 additions and 13 deletions

View File

@@ -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,