From 2a12b9740d178240078b81af7daccbe3b38a55c5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:24:50 +0000 Subject: [PATCH 1/3] test(bundler): bundle the esbuild extra suite's sibling programs together and pin their output --- test/bundler/esbuild/extra.test.ts | 627 +++++++++++------------------ 1 file changed, 232 insertions(+), 395 deletions(-) diff --git a/test/bundler/esbuild/extra.test.ts b/test/bundler/esbuild/extra.test.ts index 9354073c7229..e643ba68de86 100644 --- a/test/bundler/esbuild/extra.test.ts +++ b/test/bundler/esbuild/extra.test.ts @@ -5,8 +5,32 @@ import { itBundled } from "../expectBundled"; // https://github.com/evanw/esbuild // most of these are from scripts/end-to-end-tests.js but some are from other files +/** + * Bundles a group of the suite's single-file programs as one bundle and runs it + * once, instead of one bundle and one subprocess per program. Each program + * becomes its own module, `in.js` imports them in order, and each program logs + * its file name after its checks. The exact stdout proves that every program + * ran, and when one throws, stdout ends at the program before it. + * + * `support` holds the files the programs import or require. + * + * The export keeps the output an ES module. Without it, bun runs an output whose + * top level declares a hoisted `exports`, `module` or `require` (CommonJSSymbol) + * as a CommonJS module, where those names are the wrapper's. + */ +function programs(sources: Record, support: Record = {}) { + const names = Object.keys(sources); + const files: Record = { + "in.js": [...names.map(name => `import './${name}'`), `export const ran = ${JSON.stringify(names)}`].join("\n"), + }; + for (const name of names) { + files[name] = `${sources[name]}\nconsole.log(${JSON.stringify(name)})`; + } + return { files: { ...files, ...support }, run: { stdout: names.join("\n") } }; +} + // For debug, all files are written to $TEMP/bun-bundle-tests/extra -describe("bundler", () => { +describe.concurrent("bundler", () => { itBundled("extra/FileAsDirectoryBreak", { files: { "/index.js": ` @@ -61,47 +85,25 @@ describe("bundler", () => { }); // Test arbitrary module namespace identifier names // See https://github.com/tc39/ecma262/pull/2154 - itBundled("extra/ArbitraryModuleNamespaceIdentifiers1", { - files: { - "entry.js": `import {'*' as star} from './export.js'; if (star !== 123) throw 'fail'`, - "export.js": `let foo = 123; export {foo as '*'}`, - }, - run: true, - }); - itBundled("extra/ArbitraryModuleNamespaceIdentifiers2", { - files: { - "entry.js": `import {'\\0' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, - "export.js": `let foo = 123; export {foo as '\\0'}`, - }, - run: true, - }); - itBundled("extra/ArbitraryModuleNamespaceIdentifiers3", { - files: { - "entry.js": `import {'\\uD800\\uDC00' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, - "export.js": `let foo = 123; export {foo as '\\uD800\\uDC00'}`, - }, - run: true, - }); - itBundled("extra/ArbitraryModuleNamespaceIdentifiers4", { - files: { - "entry.js": `import {'🍕' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, - "export.js": `let foo = 123; export {foo as '🍕'}`, - }, - run: true, - }); - itBundled("extra/ArbitraryModuleNamespaceIdentifiers5", { - files: { - "entry.js": `import {' ' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, - "export.js": `export let foo = 123; export {foo as ' '} from './export.js'`, - }, - run: true, - }); - itBundled("extra/ArbitraryModuleNamespaceIdentifiers6", { - files: { - "entry.js": `import {'' as ab} from './export.js'; if (ab.foo !== 123 || ab.bar !== 234) throw 'fail'`, - "export.js": `export let foo = 123, bar = 234; export * as '' from './export.js'`, - }, - run: true, + itBundled("extra/ArbitraryModuleNamespaceIdentifiers", { + ...programs( + { + "1.js": `import {'*' as star} from './export1.js'; if (star !== 123) throw 'fail'`, + "2.js": `import {'\\0' as bar} from './export2.js'; if (bar !== 123) throw 'fail'`, + "3.js": `import {'\\uD800\\uDC00' as bar} from './export3.js'; if (bar !== 123) throw 'fail'`, + "4.js": `import {'🍕' as bar} from './export4.js'; if (bar !== 123) throw 'fail'`, + "5.js": `import {' ' as bar} from './export5.js'; if (bar !== 123) throw 'fail'`, + "6.js": `import {'' as ab} from './export6.js'; if (ab.foo !== 123 || ab.bar !== 234) throw 'fail'`, + }, + { + "export1.js": `let foo = 123; export {foo as '*'}`, + "export2.js": `let foo = 123; export {foo as '\\0'}`, + "export3.js": `let foo = 123; export {foo as '\\uD800\\uDC00'}`, + "export4.js": `let foo = 123; export {foo as '🍕'}`, + "export5.js": `export let foo = 123; export {foo as ' '} from './export5.js'`, + "export6.js": `export let foo = 123, bar = 234; export * as '' from './export6.js'`, + }, + ), }); itBundled("extra/RemoveASMDirective", { @@ -179,47 +181,25 @@ describe("bundler", () => { }, run: { file: "runtime.js" }, }); - itBundled("extra/CJSExport1", { - files: { - "in.js": `const out = require('./foo'); if (out.__esModule || out.foo !== 123) throw 'fail'`, - "foo.js": `exports.foo = 123`, - }, - run: true, - }); - itBundled("extra/CJSExport2", { - files: { - "in.js": `const out = require('./foo'); if (out.__esModule || out !== 123) throw 'fail'`, - "foo.js": `module.exports = 123`, - }, - run: true, - }); - itBundled("extra/CJSExport3", { - files: { - "in.js": `const out = require('./foo'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, - "foo.js": `export const foo = 123`, - }, - run: true, - }); - itBundled("extra/CJSExport4", { - files: { - "in.js": `const out = require('./foo'); if (!out.__esModule || out.default !== 123) throw 'fail'`, - "foo.js": `export default 123`, - }, - run: true, - }); - itBundled("extra/CJSExport5", { - files: { - "in.js": `const out = require('./foo'); if (!out.__esModule || out.default !== null) throw 'fail'`, - "foo.js": `export default function x() {} x = null`, - }, - run: true, - }); - itBundled("extra/CJSExport6", { - files: { - "in.js": `const out = require('./foo'); if (!out.__esModule || out.default !== null) throw 'fail'`, - "foo.js": `export default class x {} x = null`, - }, - run: true, + itBundled("extra/CJSExport", { + ...programs( + { + "1.js": `const out = require('./foo1'); if (out.__esModule || out.foo !== 123) throw 'fail'`, + "2.js": `const out = require('./foo2'); if (out.__esModule || out !== 123) throw 'fail'`, + "3.js": `const out = require('./foo3'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, + "4.js": `const out = require('./foo4'); if (!out.__esModule || out.default !== 123) throw 'fail'`, + "5.js": `const out = require('./foo5'); if (!out.__esModule || out.default !== null) throw 'fail'`, + "6.js": `const out = require('./foo6'); if (!out.__esModule || out.default !== null) throw 'fail'`, + }, + { + "foo1.js": `exports.foo = 123`, + "foo2.js": `module.exports = 123`, + "foo3.js": `export const foo = 123`, + "foo4.js": `export default 123`, + "foo5.js": `export default function x() {} x = null`, + "foo6.js": `export default class x {} x = null`, + }, + ), }); itBundled("extra/CJSExport7", { files: { @@ -390,7 +370,9 @@ describe("bundler", () => { format: "cjs", run: true, }); + // The `export *` in inner.ts also lands on the entry's module.exports, so out.b is 'b'. itBundled("extra/ESBuildIssue1894", { + todo: true, files: { "in.ts": ` export * from './a.cjs' @@ -406,7 +388,7 @@ describe("bundler", () => { `, }, format: "cjs", - run: true, + run: { file: "node.js" }, }); // Validate internal and external export correctness regarding "__esModule". // An ES module importing itself should not see "__esModule". But a CommonJS @@ -672,9 +654,9 @@ describe("bundler", () => { }); // Various ESM cases - itBundled("extra/CatchScope1", { - files: { - "in.js": ` + itBundled("extra/CatchScope", { + ...programs({ + "1.js": ` var x = 0, y = [] try { throw 1 @@ -686,15 +668,7 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,0') throw 'fail: ' + y `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope2", { - files: { - "in.js": ` + "2.js": ` var x = 0, y = [] try { throw 1 @@ -707,15 +681,7 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,3') throw 'fail: ' + y `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope3", { - files: { - "in.js": ` + "3.js": ` var y = [] try { throw 1 @@ -727,15 +693,7 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,') throw 'fail: ' + y `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope4", { - files: { - "in.js": ` + "4.js": ` var y = [] try { throw 1 @@ -747,15 +705,7 @@ describe("bundler", () => { y.push(typeof x) if (y + '' !== '1,2,undefined') throw 'fail: ' + y `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope5", { - files: { - "in.js": ` + "5.js": ` var y = [] try { throw 1 @@ -773,15 +723,7 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,3,1,') throw 'fail: ' + y `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope6", { - files: { - "in.js": ` + "6.js": ` var y = [] try { x; y.push('fail') } catch (e) {} try { @@ -792,17 +734,8 @@ describe("bundler", () => { try { x; y.push('fail') } catch (e) {} if (y + '' !== '1') throw 'fail: ' + y `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - - // https://github.com/evanw/esbuild/issues/1812 - itBundled("extra/CatchScope7", { - files: { - "in.js": ` + // https://github.com/evanw/esbuild/issues/1812 + "7.js": ` let a = 1; let def = "PASS2"; try { @@ -812,15 +745,7 @@ describe("bundler", () => { if (b !== 'PASS1' || d !== 'PASS2') throw 'fail: ' + b + ' ' + d } `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope8", { - files: { - "in.js": ` + "8.js": ` let a = 1; let def = "PASS2"; try { @@ -830,33 +755,26 @@ describe("bundler", () => { if (b !== 'PASS1' || d !== 'PASS2') throw 'fail: ' + b + ' ' + d } `, - }, - minifyIdentifiers: true, - minifySyntax: true, - minifyWhitespace: true, - run: true, - }); - itBundled("extra/CatchScope9", { - files: { - "in.js": ` + "9.js": ` try { throw { x: 'z', z: 123 } } catch ({ x, [x]: y }) { if (y !== 123) throw 'fail' } `, - }, + }), minifyIdentifiers: true, minifySyntax: true, minifyWhitespace: true, - run: true, }); + // Test cyclic import issues (shouldn't crash on evaluation) itBundled("extra/CyclicImport2", { files: { "entry.js": `import * as foo from './foo'; export default {foo, bar: require('./bar')}`, "foo.js": `import * as a from './entry'; import * as b from './bar'; export default {a, b}`, "bar.js": `const entry = require('./entry'); export function foo() { return entry }`, }, + run: true, }); // Test certain minification transformations @@ -871,19 +789,16 @@ describe("bundler", () => { }, { value: {}, label: "" }, ]) { - itBundled(`extra/${minify.label || "NoMinify"}1`, { - files: { - "in.js": `let fn = (x) => { if (x && y) return; function y() {} throw 'fail' }; fn(fn)`, - }, - ...minify.value, - }); - itBundled(`extra/${minify.label || "NoMinify"}2`, { - files: { - "in.js": `let fn = (a, b) => { if (a && (x = () => y) && b) return; var x; let y = 123; if (x() !== 123) throw 'fail' }; fn(fn)`, - }, + const prefix = minify.label || "NoMinify"; + itBundled(`extra/${prefix}Hoisting`, { + ...programs({ + "1.js": `let fn = (x) => { if (x && y) return; function y() {} throw 'fail' }; fn(fn)`, + "2.js": `let fn = (a, b) => { if (a && (x = () => y) && b) return; var x; let y = 123; if (x() !== 123) throw 'fail' }; fn(fn)`, + }), ...minify.value, }); + // Check property access simplification for (const { access, label } of [ { access: ".a", @@ -894,12 +809,11 @@ describe("bundler", () => { label: minify.label + "BracketAccess", }, ]) { + // All of the snippets below go into one bundle, `extra/${label}`, as the modules `1.js`, `2.js`, ... + const snippets: Record = {}; function add(n: number, files: Record) { - itBundled(`extra/${label}${n}`, { - files, - run: true, - target: "bun", - }); + if (`${n}.js` in snippets) throw new Error(`${label}${n} is defined twice`); + snippets[`${n}.js`] = files["in.js"]; } add(1, { "in.js": `if ({a: 1}${access} !== 1) throw 'fail'`, @@ -977,19 +891,24 @@ describe("bundler", () => { catch (e) { if (e === 'fail') throw e } `, }); - add(22, { + add(23, { "in.js": ` let x = 1; ({ set a(y) { x = y } }${access} = 2); if (x !== 2) throw 'fail' `, }); + itBundled(`extra/${label}`, { + ...programs(snippets), + ...minify.value, + target: "bun", + }); } // Check try/catch simplification - itBundled(`extra/${minify.label || "NoMinify"}CatchScope1`, { - files: { - "in.js": ` + itBundled(`extra/${prefix}CatchScope`, { + ...programs({ + "1.js": ` try { try { throw 0 @@ -1000,26 +919,7 @@ describe("bundler", () => { } if (x !== 1) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/${minify.label || "NoMinify"}CatchScope2`, { - files: { - "in.js": ` - let y - try { - throw 1 - } catch (x) { - eval('y = x') - } - if (y !== 1) throw 'fail' - `, - }, - run: true, - }); - itBundled(`extra/${minify.label || "NoMinify"}CatchScope3`, { - files: { - "in.js": ` + "3.js": ` try { throw 0 } catch (x) { @@ -1027,24 +927,14 @@ describe("bundler", () => { } if (x !== void 0) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/${minify.label || "NoMinify"}CatchScope4`, { - files: { - "in.js": ` + "4.js": ` let works try { throw { get a() { works = true } } } catch ({ a }) {} if (!works) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/${minify.label || "NoMinify"}CatchScope5`, { - files: { - "in.js": ` + "5.js": ` let works try { throw { *[Symbol.iterator]() { works = true } } @@ -1052,16 +942,35 @@ describe("bundler", () => { } if (!works) throw 'fail' `, + }), + ...minify.value, + }); + // Minified, this fails: the direct eval does not stop `y` from being renamed (#35955 fixes that). + itBundled(`extra/${prefix}CatchScope2`, { + todo: minify.label === "Minify", + files: { + "in.js": ` + let y + try { + throw 1 + } catch (x) { + eval('y = x') + } + if (y !== 1) throw 'fail' + `, }, + ...minify.value, run: true, }); - // Check variable initializer inlining - itBundled(`extra/${minify.label || "NoMinify"}VariableInitializerInlining`, { + // Check variable initializer inlining. `fn` must not be inlined as the call `obj.bar()`, which + // would pass `obj` as `this`. Bun runs out.js as an ES module, so a plain call gets `undefined` + // instead of the `globalThis` the esbuild version expects. + itBundled(`extra/${prefix}VariableInitializerInlining`, { files: { "in.js": ` function foo() { - if (this !== globalThis) throw 'fail' + if (this !== globalThis && this !== undefined) throw 'fail' } function main() { let obj = { bar: foo }; @@ -1071,11 +980,13 @@ describe("bundler", () => { main() `, }, + ...minify.value, + run: true, }); // Check global constructor behavior - itBundled(`extra/${minify.label || "NoMinify"}GlobalConstructorBehavior1`, { - files: { - "in.js": ` + itBundled(`extra/${prefix}GlobalConstructorBehavior`, { + ...programs({ + "1.js": ` const check = (before, after) => { if (Boolean(before) !== after) throw 'fail: Boolean(' + before + ') should not be ' + Boolean(before) if (new Boolean(before) === after) throw 'fail: new Boolean(' + before + ') should not be ' + new Boolean(before) @@ -1094,12 +1005,7 @@ describe("bundler", () => { checkSpread([0], false); check([1], true) checkSpread([], false) `, - }, - run: true, - }); - itBundled(`extra/${minify.label || "NoMinify"}GlobalConstructorBehavior2`, { - files: { - "in.js": ` + "2.js": ` class ToPrimitive { [Symbol.toPrimitive]() { return '100.001' } } const someObject = { toString: () => 123, valueOf: () => 321 } @@ -1125,12 +1031,7 @@ describe("bundler", () => { checkSpread(['123'], 123) checkSpread([], 0) `, - }, - run: true, - }); - itBundled(`extra/${minify.label || "NoMinify"}GlobalConstructorBehavior3`, { - files: { - "in.js": ` + "3.js": ` class ToPrimitive { [Symbol.toPrimitive]() { return 100.001 } } const someObject = { toString: () => 123, valueOf: () => 321 } @@ -1167,8 +1068,8 @@ describe("bundler", () => { } checkAndExpectNewToThrow(Symbol('abc'), 'Symbol(abc)') `, - }, - run: true, + }), + ...minify.value, }); } // Test minification of hoisted top-level symbols declared in nested scopes. @@ -1209,112 +1110,84 @@ describe("bundler", () => { // wrappers. Previously this didn't work due to a bug that considered for // loop initializers to already be in the top-level scope. For more info // see: https://github.com/evanw/esbuild/issues/1455. - itBundled(`extra/ForLoopInitializerHoisting1`, { - files: { - "in.js": ` - if (require('./nested').foo() !== 10) throw 'fail' - `, - "nested.js": ` - for (var i = 0; i < 10; i++) ; - export function foo() { return i } - `, - }, - run: true, - }); - itBundled(`extra/ForLoopInitializerHoisting2`, { - files: { - "in.js": ` - if (require('./nested').foo() !== 'c') throw 'fail' - `, - "nested.js": ` - for (var i in {a: 1, b: 2, c: 3}) ; - export function foo() { return i } - `, - }, - run: true, - }); - itBundled(`extra/ForLoopInitializerHoisting3`, { - files: { - "in.js": ` - if (require('./nested').foo() !== 3) throw 'fail' - `, - "nested.js": ` - for (var i of [1, 2, 3]) ; - export function foo() { return i } - `, - }, - run: true, + itBundled(`extra/ForLoopInitializerHoisting`, { + ...programs( + { + "1.js": `if (require('./nested1').foo() !== 10) throw 'fail'`, + "2.js": `if (require('./nested2').foo() !== 'c') throw 'fail'`, + "3.js": `if (require('./nested3').foo() !== 3) throw 'fail'`, + }, + { + "nested1.js": ` + for (var i = 0; i < 10; i++) ; + export function foo() { return i } + `, + "nested2.js": ` + for (var i in {a: 1, b: 2, c: 3}) ; + export function foo() { return i } + `, + "nested3.js": ` + for (var i of [1, 2, 3]) ; + export function foo() { return i } + `, + }, + ), }); // Test tree shaking - itBundled(`extra/TreeShaking1`, { - files: { - "entry.js": `import * as foo from './foo'; if (global.dce0 !== 123 || foo.abc !== 'abc') throw 'fail'`, - "foo/index.js": `global.dce0 = 123; export const abc = 'abc'`, - "foo/package.json": `{ "sideEffects": false }`, - }, - run: true, - }); - itBundled(`extra/TreeShaking2`, { - files: { - "entry.js": `import * as foo from './foo'; if (global.dce1 !== void 0) throw 'fail'`, - "foo/index.js": `global.dce1 = 123; export const abc = 'abc'`, - "foo/package.json": `{ "sideEffects": false }`, - }, - run: true, - }); - itBundled(`extra/TreeShaking3`, { - files: { - "entry.js": `import * as foo from './foo'; if (global.dce2 !== 123) throw 'fail'`, - "foo/index.js": `global.dce2 = 123; export const abc = 'abc'`, - "foo/package.json": `{ "sideEffects": true }`, - }, - run: true, - }); - itBundled(`extra/TreeShaking4`, { - files: { - "entry.js": `import foo from './foo'; if (global.dce3 !== 123 || foo.abc !== 'abc') throw 'fail'`, - "foo/index.js": `global.dce3 = 123; exports.abc = 'abc'`, - "foo/package.json": `{ "sideEffects": false }`, - }, - run: true, - }); - itBundled(`extra/TreeShaking5`, { - files: { - "entry.js": `import foo from './foo'; if (global.dce4 !== void 0) throw 'fail'`, - "foo/index.js": `global.dce4 = 123; exports.abc = 'abc'`, - "foo/package.json": `{ "sideEffects": false }`, - }, - run: true, - }); - itBundled(`extra/TreeShaking6`, { - files: { - "entry.js": `import foo from './foo'; if (global.dce5 !== 123) throw 'fail'`, - "foo/index.js": `global.dce5 = 123; exports.abc = 'abc'`, - "foo/package.json": `{ "sideEffects": true }`, - }, - run: true, - }); - // Note: Tree shaking this could technically be considered incorrect because - // the import is for a property whose getter in this case has a side effect. - // However, this is very unlikely and the vast majority of the time people - // would likely rather have the code be tree-shaken. This test case enforces - // the technically incorrect behavior as documentation that this edge case - // is being ignored. - itBundled(`extra/TreeShaking7`, { - files: { - "entry.js": `import {foo, bar} from './foo'; let unused = foo; if (bar) throw 'expected "foo" to be tree-shaken'`, - "foo.js": `module.exports = {get foo() { module.exports.bar = 1 }, bar: 0}`, + itBundled(`extra/TreeShaking`, { + ...programs( + { + // Keep because used (ES6) + "1.js": `import * as foo from './foo1'; if (global.dce0 !== 123 || foo.abc !== 'abc') throw 'fail'`, + // Remove because unused (ES6) + "2.js": `import * as foo from './foo2'; if (global.dce1 !== void 0) throw 'fail'`, + // Keep because side effects (ES6) + "3.js": `import * as foo from './foo3'; if (global.dce2 !== 123) throw 'fail'`, + // Keep because used (CommonJS) + "4.js": `import foo from './foo4'; if (global.dce3 !== 123 || foo.abc !== 'abc') throw 'fail'`, + // Remove because unused (CommonJS) + "5.js": `import foo from './foo5'; if (global.dce4 !== void 0) throw 'fail'`, + // Keep because side effects (CommonJS) + "6.js": `import foo from './foo6'; if (global.dce5 !== 123) throw 'fail'`, + // Note: Tree shaking this could technically be considered incorrect because + // the import is for a property whose getter in this case has a side effect. + // However, this is very unlikely and the vast majority of the time people + // would likely rather have the code be tree-shaken. This test case enforces + // the technically incorrect behavior as documentation that this edge case + // is being ignored. + "7.js": `import {foo, bar} from './foo7'; let unused = foo; if (bar) throw 'expected "foo" to be tree-shaken'`, + }, + { + "foo1/index.js": `global.dce0 = 123; export const abc = 'abc'`, + "foo1/package.json": `{ "sideEffects": false }`, + "foo2/index.js": `global.dce1 = 123; export const abc = 'abc'`, + "foo2/package.json": `{ "sideEffects": false }`, + "foo3/index.js": `global.dce2 = 123; export const abc = 'abc'`, + "foo3/package.json": `{ "sideEffects": true }`, + "foo4/index.js": `global.dce3 = 123; exports.abc = 'abc'`, + "foo4/package.json": `{ "sideEffects": false }`, + "foo5/index.js": `global.dce4 = 123; exports.abc = 'abc'`, + "foo5/package.json": `{ "sideEffects": false }`, + "foo6/index.js": `global.dce5 = 123; exports.abc = 'abc'`, + "foo6/package.json": `{ "sideEffects": true }`, + "foo7.js": `module.exports = {get foo() { module.exports.bar = 1 }, bar: 0}`, + }, + ), + // foo2 and foo5 are removed from the bundle, and so is `let unused = foo` in 7.js. + assertNotPresent: { + "/out.js": ["dce1 = 123", "dce4 = 123", "unused"], }, - run: true, }); + // Test for an implicit and explicit "**/" prefix (see https://github.com/evanw/esbuild/issues/1184) itBundled(`extra/TreeShaking8`, { + // Bun joins the pattern onto the package directory, so "x.*" does not match dir/x.js. + todo: true, files: { "entry.js": `import './foo'; if (global.dce6 !== 123) throw 'fail'`, "foo/dir/x.js": `global.dce6 = 123`, "foo/package.json": `{ "main": "dir/x", "sideEffects": ["x.*"] }`, }, - skipIfWeDidNotImplementWildcardSideEffects: true, run: true, }); itBundled(`extra/TreeShaking9`, { @@ -1323,7 +1196,6 @@ describe("bundler", () => { "foo/dir/x.js": `global.dce6 = 123`, "foo/package.json": `{ "main": "dir/x", "sideEffects": ["**/x.*"] }`, }, - skipIfWeDidNotImplementWildcardSideEffects: true, run: true, }); itBundled(`extra/TreeShaking10`, { @@ -1362,49 +1234,30 @@ describe("bundler", () => { }); // Test obscure CommonJS symbol edge cases - itBundled(`extra/CommonJSSymbol1`, { - files: { - "in.js": `const ns = require('./foo'); if (ns.foo !== 123 || ns.bar !== 123) throw 'fail'`, - "foo.js": `var exports, module; module.exports.foo = 123; exports.bar = exports.foo`, - }, - }); - itBundled(`extra/CommonJSSymbol2`, { - files: { - "in.js": `require('./foo'); require('./bar')`, - "foo.js": `let exports; if (exports !== void 0) throw 'fail'`, - "bar.js": `let module; if (module !== void 0) throw 'fail'`, - }, - }); - itBundled(`extra/CommonJSSymbol3`, { - files: { - "in.js": `const ns = require('./foo'); if (ns.foo !== void 0 || ns.default.foo !== 123) throw 'fail'`, - "foo.js": `var exports = {foo: 123}; export default exports`, - }, - }); - itBundled(`extra/CommonJSSymbol4`, { - files: { - "in.js": `const ns = require('./foo'); if (ns !== 123) throw 'fail'`, - "foo.ts": `let module = 123; export = module`, - }, - }); - itBundled(`extra/CommonJSSymbol5`, { - files: { - "in.js": `require('./foo')`, - "foo.js": `var require; if (require !== void 0) throw 'fail'`, - }, - }); - itBundled(`extra/CommonJSSymbol6`, { - files: { - "in.js": `require('./foo')`, - "foo.js": `var require = x => x; if (require('does not exist') !== 'does not exist') throw 'fail'`, - }, - }); - itBundled(`extra/CommonJSSymbol7`, { - files: { - "in.js": `const ns = require('./foo'); if (ns.a !== 123 || ns.b.a !== 123) throw 'fail'`, - "foo.js": `exports.a = 123; exports.b = this`, - }, + itBundled(`extra/CommonJSSymbol`, { + ...programs( + { + "1.js": `const ns = require('./foo1'); if (ns.foo !== 123 || ns.bar !== 123) throw 'fail'`, + "2.js": `require('./foo2'); require('./bar2')`, + "3.js": `const ns = require('./foo3'); if (ns.foo !== void 0 || ns.default.foo !== 123) throw 'fail'`, + "4.js": `const ns = require('./foo4'); if (ns !== 123) throw 'fail'`, + "5.js": `require('./foo5')`, + "6.js": `require('./foo6')`, + "7.js": `const ns = require('./foo7'); if (ns.a !== 123 || ns.b.a !== 123) throw 'fail'`, + }, + { + "foo1.js": `var exports, module; module.exports.foo = 123; exports.bar = exports.foo`, + "foo2.js": `let exports; if (exports !== void 0) throw 'fail'`, + "bar2.js": `let module; if (module !== void 0) throw 'fail'`, + "foo3.js": `var exports = {foo: 123}; export default exports`, + "foo4.ts": `let module = 123; export = module`, + "foo5.js": `var require; if (require !== void 0) throw 'fail'`, + "foo6.js": `var require = x => x; if (require('does not exist') !== 'does not exist') throw 'fail'`, + "foo7.js": `exports.a = 123; exports.b = this`, + }, + ), }); + // The top-level `this` of the ES module becomes null instead of undefined, so this one only bundles. itBundled(`extra/CommonJSSymbol8`, { files: { "in.js": `const ns = require('./foo'); if (ns.a !== 123 || ns.b !== void 0) throw 'fail'`, @@ -1702,9 +1555,9 @@ describe("bundler", () => { }); // Object rest pattern tests // Test the correctness of side effect order for the TypeScript namespace exports - itBundled(`extra/ObjectRestPattern1`, { - files: { - "in.ts": ` + itBundled(`extra/ObjectRestPattern`, { + ...programs({ + "1.ts": ` function fn() { let trail = [] let t = k => (trail.push(k), k) @@ -1727,12 +1580,7 @@ describe("bundler", () => { } if (fn() !== ns.result) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/ObjectRestPattern2`, { - files: { - "in.ts": ` + "2.ts": ` let obj = {}; ({a: obj.a, ...obj.b} = {a: 1, b: 2, c: 3}); [obj.c, , ...obj.d] = [1, 2, 3]; @@ -1746,26 +1594,15 @@ describe("bundler", () => { } if (JSON.stringify(obj) !== JSON.stringify(ns)) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/ObjectRestPattern3`, { - files: { - "in.ts": ` + "3.ts": ` var z = {x: {z: 'z'}, y: 'y'}, {x: z, ...y} = z if (y.y !== 'y' || z.z !== 'z') throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/ObjectRestPattern4`, { - files: { - "in.ts": ` + "4.ts": ` var z = {x: {x: 'x'}, y: 'y'}, {[(z = {z: 'z'}, 'x')]: x, ...y} = z if (x.x !== 'x' || y.y !== 'y' || z.z !== 'z') throw 'fail' `, - }, - run: true, + }), }); itBundled("extra/CaseSensitiveImport", { From 2b23823704a500df8c5f104906ac958ef0ee1ca5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:12:59 +0000 Subject: [PATCH 2/3] test(bundler): keep esbuild/extra.test.ts serial until itBundled guards todo cases too --- test/bundler/esbuild/extra.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundler/esbuild/extra.test.ts b/test/bundler/esbuild/extra.test.ts index e643ba68de86..52d000f604b3 100644 --- a/test/bundler/esbuild/extra.test.ts +++ b/test/bundler/esbuild/extra.test.ts @@ -30,7 +30,7 @@ function programs(sources: Record, support: Record { +describe("bundler", () => { itBundled("extra/FileAsDirectoryBreak", { files: { "/index.js": ` From 9432f01d5e244f64d0913216c2d3cd0a4fca80b9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:30:57 +0000 Subject: [PATCH 3/3] test(bundler): keep every program its own entry point and group more of the extra suite --- test/bundler/esbuild/extra.test.ts | 789 +++++++++++++++-------------- 1 file changed, 421 insertions(+), 368 deletions(-) diff --git a/test/bundler/esbuild/extra.test.ts b/test/bundler/esbuild/extra.test.ts index 52d000f604b3..99c1618cfdfa 100644 --- a/test/bundler/esbuild/extra.test.ts +++ b/test/bundler/esbuild/extra.test.ts @@ -6,27 +6,37 @@ import { itBundled } from "../expectBundled"; // most of these are from scripts/end-to-end-tests.js but some are from other files /** - * Bundles a group of the suite's single-file programs as one bundle and runs it - * once, instead of one bundle and one subprocess per program. Each program - * becomes its own module, `in.js` imports them in order, and each program logs - * its file name after its checks. The exact stdout proves that every program - * ran, and when one throws, stdout ends at the program before it. - * - * `support` holds the files the programs import or require. - * - * The export keeps the output an ES module. Without it, bun runs an output whose - * top level declares a hoisted `exports`, `module` or `require` (CommonJSSymbol) - * as a CommonJS module, where those names are the wrapper's. + * Builds a block of sibling cases as one build and runs their outputs in one + * subprocess, instead of one build and one subprocess per case. Each value is + * the `files` of one of the old cases, written to a directory named after the + * key (the number the case used to have), and its first file is an entry point. + * `runner.js` logs each key and then imports that program's output, so the + * exact stdout proves that every program ran, and the last line names the one + * that threw. Each program keeps its own output, so any cases with the same + * build options can share a group. */ -function programs(sources: Record, support: Record = {}) { - const names = Object.keys(sources); - const files: Record = { - "in.js": [...names.map(name => `import './${name}'`), `export const ran = ${JSON.stringify(names)}`].join("\n"), - }; - for (const name of names) { - files[name] = `${sources[name]}\nconsole.log(${JSON.stringify(name)})`; +function programs(cases: Record>) { + const files: Record = {}; + const entryPoints: string[] = []; + const outputPaths: string[] = []; + const runner: string[] = []; + for (const [name, caseFiles] of Object.entries(cases)) { + for (const [file, contents] of Object.entries(caseFiles)) files[`${name}/${file}`] = contents; + const entry = Object.keys(caseFiles)[0]; + const output = `/out/${name}/${entry.replace(/\.[jt]sx?$/, ".js")}`; + entryPoints.push(`${name}/${entry}`); + outputPaths.push(output); + runner.push(`console.log(${JSON.stringify(name)}); await import(${JSON.stringify("." + output)});`); } - return { files: { ...files, ...support }, run: { stdout: names.join("\n") } }; + files["runner.js"] = runner.join("\n"); + // `root` keeps the output of program `1` at out/1/ even when a group holds one program. + return { + files, + entryPoints, + outputPaths, + root: ".", + run: { file: "runner.js", stdout: Object.keys(cases).join("\n") }, + }; } // For debug, all files are written to $TEMP/bun-bundle-tests/extra @@ -86,24 +96,32 @@ describe("bundler", () => { // Test arbitrary module namespace identifier names // See https://github.com/tc39/ecma262/pull/2154 itBundled("extra/ArbitraryModuleNamespaceIdentifiers", { - ...programs( - { - "1.js": `import {'*' as star} from './export1.js'; if (star !== 123) throw 'fail'`, - "2.js": `import {'\\0' as bar} from './export2.js'; if (bar !== 123) throw 'fail'`, - "3.js": `import {'\\uD800\\uDC00' as bar} from './export3.js'; if (bar !== 123) throw 'fail'`, - "4.js": `import {'🍕' as bar} from './export4.js'; if (bar !== 123) throw 'fail'`, - "5.js": `import {' ' as bar} from './export5.js'; if (bar !== 123) throw 'fail'`, - "6.js": `import {'' as ab} from './export6.js'; if (ab.foo !== 123 || ab.bar !== 234) throw 'fail'`, + ...programs({ + 1: { + "entry.js": `import {'*' as star} from './export.js'; if (star !== 123) throw 'fail'`, + "export.js": `let foo = 123; export {foo as '*'}`, }, - { - "export1.js": `let foo = 123; export {foo as '*'}`, - "export2.js": `let foo = 123; export {foo as '\\0'}`, - "export3.js": `let foo = 123; export {foo as '\\uD800\\uDC00'}`, - "export4.js": `let foo = 123; export {foo as '🍕'}`, - "export5.js": `export let foo = 123; export {foo as ' '} from './export5.js'`, - "export6.js": `export let foo = 123, bar = 234; export * as '' from './export6.js'`, - }, - ), + 2: { + "entry.js": `import {'\\0' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, + "export.js": `let foo = 123; export {foo as '\\0'}`, + }, + 3: { + "entry.js": `import {'\\uD800\\uDC00' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, + "export.js": `let foo = 123; export {foo as '\\uD800\\uDC00'}`, + }, + 4: { + "entry.js": `import {'🍕' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, + "export.js": `let foo = 123; export {foo as '🍕'}`, + }, + 5: { + "entry.js": `import {' ' as bar} from './export.js'; if (bar !== 123) throw 'fail'`, + "export.js": `export let foo = 123; export {foo as ' '} from './export.js'`, + }, + 6: { + "entry.js": `import {'' as ab} from './export.js'; if (ab.foo !== 123 || ab.bar !== 234) throw 'fail'`, + "export.js": `export let foo = 123, bar = 234; export * as '' from './export.js'`, + }, + }), }); itBundled("extra/RemoveASMDirective", { @@ -117,29 +135,27 @@ describe("bundler", () => { }); // See https://github.com/evanw/esbuild/issues/421 - itBundled("extra/ImportOrder1", { - files: { - "in.js": ` + itBundled("extra/ImportOrder", { + ...programs({ + 1: { + "in.js": ` import {foo} from './cjs' import {bar} from './esm' if (foo !== 1 || bar !== 2) throw 'fail' `, - "cjs.js": `exports.foo = 1; global.internal_import_order_test1 = 2`, - "esm.js": `export let bar = global.internal_import_order_test1`, - }, - run: true, - }); - itBundled("extra/ImportOrder2", { - files: { - "in.js": ` + "cjs.js": `exports.foo = 1; global.internal_import_order_test1 = 2`, + "esm.js": `export let bar = global.internal_import_order_test1`, + }, + 2: { + "in.js": ` if (foo !== 3 || bar !== 4) throw 'fail' import {foo} from './cjs' import {bar} from './esm' `, - "cjs.js": `exports.foo = 3; global.internal_import_order_test2 = 4`, - "esm.js": `export let bar = global.internal_import_order_test2`, - }, - run: true, + "cjs.js": `exports.foo = 3; global.internal_import_order_test2 = 4`, + "esm.js": `export let bar = global.internal_import_order_test2`, + }, + }), }); // See https://github.com/evanw/esbuild/issues/542 let simpleCyclicImportTestCase542 = { @@ -182,28 +198,33 @@ describe("bundler", () => { run: { file: "runtime.js" }, }); itBundled("extra/CJSExport", { - ...programs( - { - "1.js": `const out = require('./foo1'); if (out.__esModule || out.foo !== 123) throw 'fail'`, - "2.js": `const out = require('./foo2'); if (out.__esModule || out !== 123) throw 'fail'`, - "3.js": `const out = require('./foo3'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, - "4.js": `const out = require('./foo4'); if (!out.__esModule || out.default !== 123) throw 'fail'`, - "5.js": `const out = require('./foo5'); if (!out.__esModule || out.default !== null) throw 'fail'`, - "6.js": `const out = require('./foo6'); if (!out.__esModule || out.default !== null) throw 'fail'`, + ...programs({ + 1: { + "in.js": `const out = require('./foo'); if (out.__esModule || out.foo !== 123) throw 'fail'`, + "foo.js": `exports.foo = 123`, }, - { - "foo1.js": `exports.foo = 123`, - "foo2.js": `module.exports = 123`, - "foo3.js": `export const foo = 123`, - "foo4.js": `export default 123`, - "foo5.js": `export default function x() {} x = null`, - "foo6.js": `export default class x {} x = null`, - }, - ), - }); - itBundled("extra/CJSExport7", { - files: { - "in.js": ` + 2: { + "in.js": `const out = require('./foo'); if (out.__esModule || out !== 123) throw 'fail'`, + "foo.js": `module.exports = 123`, + }, + 3: { + "in.js": `const out = require('./foo'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, + "foo.js": `export const foo = 123`, + }, + 4: { + "in.js": `const out = require('./foo'); if (!out.__esModule || out.default !== 123) throw 'fail'`, + "foo.js": `export default 123`, + }, + 5: { + "in.js": `const out = require('./foo'); if (!out.__esModule || out.default !== null) throw 'fail'`, + "foo.js": `export default function x() {} x = null`, + }, + 6: { + "in.js": `const out = require('./foo'); if (!out.__esModule || out.default !== null) throw 'fail'`, + "foo.js": `export default class x {} x = null`, + }, + 7: { + "in.js": ` // This is the JavaScript generated by "tsc" for the following TypeScript: // // import fn from './foo' @@ -218,80 +239,77 @@ describe("bundler", () => { if (typeof foo_1.default !== 'function') throw 'fail'; `, - "foo.js": `export default function fn() {}`, - }, - run: true, - }); - itBundled("extra/CJSSelfExport1", { - files: { - "in.js": `exports.foo = 123; const out = require('./in'); if (out.__esModule || out.foo !== 123) throw 'fail'`, - }, - run: true, + "foo.js": `export default function fn() {}`, + }, + }), }); - itBundled("extra/CJSSelfExport2", { - files: { - "in.js": `module.exports = 123; const out = require('./in'); if (out.__esModule || out !== 123) throw 'fail'`, - }, - run: true, + // Self export. The esbuild suite bundles 3, 4 and 5 with --format=cjs and minifies 4 and 7. + itBundled("extra/CJSSelfExport", { + ...programs({ + 1: { + "in.js": `exports.foo = 123; const out = require('./in'); if (out.__esModule || out.foo !== 123) throw 'fail'`, + }, + 2: { + "in.js": `module.exports = 123; const out = require('./in'); if (out.__esModule || out !== 123) throw 'fail'`, + }, + 6: { + "in.js": `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, + }, + 8: { + "in.js": `export default 123; const out = require('./in'); if (!out.__esModule || out.default !== 123) throw 'fail'`, + }, + }), }); - itBundled("extra/CJSSelfExport3", { - files: { - "in.js": `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, - }, - run: true, + itBundled("extra/CJSSelfExportCJS", { + ...programs({ + 3: { + "in.js": `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, + }, + 5: { + "in.js": `export default 123; const out = require('./in'); if (!out.__esModule || out.default !== 123) throw 'fail'`, + }, + }), + format: "cjs", }); itBundled("extra/CJSSelfExport4", { files: { "in.js": `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, }, - run: true, - }); - itBundled("extra/CJSSelfExport5", { - files: { - "in.js": `export default 123; const out = require('./in'); if (!out.__esModule || out.default !== 123) throw 'fail'`, - }, - run: true, - }); - itBundled("extra/CJSSelfExport6", { - files: { - "in.js": `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, - }, + format: "cjs", + minifyIdentifiers: true, + minifySyntax: true, + minifyWhitespace: true, run: true, }); itBundled("extra/CJSSelfExport7", { files: { "in.js": `export const foo = 123; const out = require('./in'); if (!out.__esModule || out.foo !== 123) throw 'fail'`, }, + minifyIdentifiers: true, + minifySyntax: true, + minifyWhitespace: true, run: true, }); - itBundled("extra/CJSSelfExport8", { - files: { - "in.js": `export default 123; const out = require('./in'); if (!out.__esModule || out.default !== 123) throw 'fail'`, - }, - run: true, - }); - itBundled("extra/DoubleExportStar1", { - files: { - "node.ts": ` + itBundled("extra/DoubleExportStar", { + ...programs({ + 1: { + "node.ts": ` import {a, b} from './re-export' if (a !== 'a' || b !== 'b') throw 'fail' `, - "re-export.ts": ` + "re-export.ts": ` export * from './a' export * from './b' `, - "a.ts": ` + "a.ts": ` export let a = 'a' `, - "b.ts": ` + "b.ts": ` export let b = 'b' `, - }, - run: true, - }); - itBundled("extra/DoubleExportStar2", { - files: { - "node.ts": ` + }, + 2: { + "node.ts": ` import {a, b} from './re-export' if (a !== 'a' || b !== 'b') throw 'fail' @@ -301,22 +319,19 @@ describe("bundler", () => { require('./a') require('./b') `, - "re-export.ts": ` + "re-export.ts": ` export * from './a' export * from './b' `, - "a.ts": ` + "a.ts": ` export let a = 'a' `, - "b.ts": ` + "b.ts": ` export let b = 'b' `, - }, - run: true, - }); - itBundled("extra/DoubleExportStar3", { - files: { - "node.ts": ` + }, + 3: { + "node.ts": ` import {a, b, c, d} from './re-export' if (a !== 'a' || b !== 'b' || c !== 'c' || d !== 'd') throw 'fail' @@ -326,26 +341,26 @@ describe("bundler", () => { require('./a') require('./b') `, - "re-export.ts": ` + "re-export.ts": ` export * from './a' export * from './b' export * from './d' `, - "a.ts": ` + "a.ts": ` export let a = 'a' `, - "b.ts": ` + "b.ts": ` exports.b = 'b' `, - "c.ts": ` + "c.ts": ` exports.c = 'c' `, - "d.ts": ` + "d.ts": ` export * from './c' export let d = 'd' `, - }, - run: true, + }, + }), }); // Complex circular bundled and non-bundled import case (https://github.com/evanw/esbuild/issues/758) itBundled("extra/ESBuildIssue758", { @@ -370,7 +385,8 @@ describe("bundler", () => { format: "cjs", run: true, }); - // The `export *` in inner.ts also lands on the entry's module.exports, so out.b is 'b'. + // With format cjs, the `export *` of inner.ts is also copied onto the entry's module.exports, + // so out.b is 'b' (#37829 fixes that). itBundled("extra/ESBuildIssue1894", { todo: true, files: { @@ -411,40 +427,36 @@ describe("bundler", () => { }); // Use "eval" to access CommonJS variables - itBundled("extra/CJSEval1", { - files: { - "in.js": `if (require('./eval').foo !== 123) throw 'fail'`, - "eval.js": `exports.foo=234;eval('exports.foo = 123')`, - }, - run: true, - }); - itBundled("extra/CJSEval2", { - files: { - "in.js": `if (require('./eval').foo !== 123) throw 'fail'`, - "eval.js": `module.exports={foo:234};eval('module.exports = {foo: 123}')`, - }, - run: true, + itBundled("extra/CJSEval", { + ...programs({ + 1: { + "in.js": `if (require('./eval').foo !== 123) throw 'fail'`, + "eval.js": `exports.foo=234;eval('exports.foo = 123')`, + }, + 2: { + "in.js": `if (require('./eval').foo !== 123) throw 'fail'`, + "eval.js": `module.exports={foo:234};eval('module.exports = {foo: 123}')`, + }, + }), }); - itBundled("extra/EnumerableFalse1", { - files: { - "in.js": ` + itBundled("extra/EnumerableFalse", { + ...programs({ + 1: { + "in.js": ` import {foo} from './esm' if (foo !== 123) throw 'fail' `, - "esm.js": `Object.defineProperty(exports, 'foo', {value: 123, enumerable: false})`, - }, - run: true, - }); - // Test imports not being able to access the namespace object - itBundled("extra/EnumerableFalse2", { - files: { - "in.js": ` + "esm.js": `Object.defineProperty(exports, 'foo', {value: 123, enumerable: false})`, + }, + // Test imports not being able to access the namespace object + 2: { + "in.js": ` import * as ns from './esm' if (ns[Math.random() < 2 && 'foo'] !== 123) throw 'fail' `, - "esm.js": `Object.defineProperty(exports, 'foo', {value: 123, enumerable: false})`, - }, - run: true, + "esm.js": `Object.defineProperty(exports, 'foo', {value: 123, enumerable: false})`, + }, + }), }); // Test imports of properties from the prototype chain of "module.exports" for Webpack compatibility itBundled("extra/PrototypeChain1", { @@ -656,7 +668,8 @@ describe("bundler", () => { // Various ESM cases itBundled("extra/CatchScope", { ...programs({ - "1.js": ` + 1: { + "in.js": ` var x = 0, y = [] try { throw 1 @@ -668,7 +681,9 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,0') throw 'fail: ' + y `, - "2.js": ` + }, + 2: { + "in.js": ` var x = 0, y = [] try { throw 1 @@ -681,7 +696,9 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,3') throw 'fail: ' + y `, - "3.js": ` + }, + 3: { + "in.js": ` var y = [] try { throw 1 @@ -693,7 +710,9 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,') throw 'fail: ' + y `, - "4.js": ` + }, + 4: { + "in.js": ` var y = [] try { throw 1 @@ -705,7 +724,9 @@ describe("bundler", () => { y.push(typeof x) if (y + '' !== '1,2,undefined') throw 'fail: ' + y `, - "5.js": ` + }, + 5: { + "in.js": ` var y = [] try { throw 1 @@ -723,7 +744,9 @@ describe("bundler", () => { y.push(x) if (y + '' !== '1,2,3,1,') throw 'fail: ' + y `, - "6.js": ` + }, + 6: { + "in.js": ` var y = [] try { x; y.push('fail') } catch (e) {} try { @@ -734,8 +757,11 @@ describe("bundler", () => { try { x; y.push('fail') } catch (e) {} if (y + '' !== '1') throw 'fail: ' + y `, + }, + // https://github.com/evanw/esbuild/issues/1812 - "7.js": ` + 7: { + "in.js": ` let a = 1; let def = "PASS2"; try { @@ -745,7 +771,9 @@ describe("bundler", () => { if (b !== 'PASS1' || d !== 'PASS2') throw 'fail: ' + b + ' ' + d } `, - "8.js": ` + }, + 8: { + "in.js": ` let a = 1; let def = "PASS2"; try { @@ -755,13 +783,16 @@ describe("bundler", () => { if (b !== 'PASS1' || d !== 'PASS2') throw 'fail: ' + b + ' ' + d } `, - "9.js": ` + }, + 9: { + "in.js": ` try { throw { x: 'z', z: 123 } } catch ({ x, [x]: y }) { if (y !== 123) throw 'fail' } `, + }, }), minifyIdentifiers: true, minifySyntax: true, @@ -792,8 +823,12 @@ describe("bundler", () => { const prefix = minify.label || "NoMinify"; itBundled(`extra/${prefix}Hoisting`, { ...programs({ - "1.js": `let fn = (x) => { if (x && y) return; function y() {} throw 'fail' }; fn(fn)`, - "2.js": `let fn = (a, b) => { if (a && (x = () => y) && b) return; var x; let y = 123; if (x() !== 123) throw 'fail' }; fn(fn)`, + 1: { + "in.js": `let fn = (x) => { if (x && y) return; function y() {} throw 'fail' }; fn(fn)`, + }, + 2: { + "in.js": `let fn = (a, b) => { if (a && (x = () => y) && b) return; var x; let y = 123; if (x() !== 123) throw 'fail' }; fn(fn)`, + }, }), ...minify.value, }); @@ -809,11 +844,11 @@ describe("bundler", () => { label: minify.label + "BracketAccess", }, ]) { - // All of the snippets below go into one bundle, `extra/${label}`, as the modules `1.js`, `2.js`, ... - const snippets: Record = {}; + // add() collects the snippets, and the itBundled call after the last one builds them as one group. + const snippets: Record> = {}; function add(n: number, files: Record) { - if (`${n}.js` in snippets) throw new Error(`${label}${n} is defined twice`); - snippets[`${n}.js`] = files["in.js"]; + if (n in snippets) throw new Error(`${label}${n} is added twice`); + snippets[n] = files; } add(1, { "in.js": `if ({a: 1}${access} !== 1) throw 'fail'`, @@ -908,7 +943,8 @@ describe("bundler", () => { // Check try/catch simplification itBundled(`extra/${prefix}CatchScope`, { ...programs({ - "1.js": ` + 1: { + "in.js": ` try { try { throw 0 @@ -919,7 +955,9 @@ describe("bundler", () => { } if (x !== 1) throw 'fail' `, - "3.js": ` + }, + 3: { + "in.js": ` try { throw 0 } catch (x) { @@ -927,14 +965,18 @@ describe("bundler", () => { } if (x !== void 0) throw 'fail' `, - "4.js": ` + }, + 4: { + "in.js": ` let works try { throw { get a() { works = true } } } catch ({ a }) {} if (!works) throw 'fail' `, - "5.js": ` + }, + 5: { + "in.js": ` let works try { throw { *[Symbol.iterator]() { works = true } } @@ -942,9 +984,11 @@ describe("bundler", () => { } if (!works) throw 'fail' `, + }, }), ...minify.value, }); + // Minified, this fails: the direct eval does not stop `y` from being renamed (#35955 fixes that). itBundled(`extra/${prefix}CatchScope2`, { todo: minify.label === "Minify", @@ -986,7 +1030,8 @@ describe("bundler", () => { // Check global constructor behavior itBundled(`extra/${prefix}GlobalConstructorBehavior`, { ...programs({ - "1.js": ` + 1: { + "in.js": ` const check = (before, after) => { if (Boolean(before) !== after) throw 'fail: Boolean(' + before + ') should not be ' + Boolean(before) if (new Boolean(before) === after) throw 'fail: new Boolean(' + before + ') should not be ' + new Boolean(before) @@ -1005,7 +1050,9 @@ describe("bundler", () => { checkSpread([0], false); check([1], true) checkSpread([], false) `, - "2.js": ` + }, + 2: { + "in.js": ` class ToPrimitive { [Symbol.toPrimitive]() { return '100.001' } } const someObject = { toString: () => 123, valueOf: () => 321 } @@ -1031,7 +1078,9 @@ describe("bundler", () => { checkSpread(['123'], 123) checkSpread([], 0) `, - "3.js": ` + }, + 3: { + "in.js": ` class ToPrimitive { [Symbol.toPrimitive]() { return 100.001 } } const someObject = { toString: () => 123, valueOf: () => 321 } @@ -1068,6 +1117,7 @@ describe("bundler", () => { } checkAndExpectNewToThrow(Symbol('abc'), 'Symbol(abc)') `, + }, }), ...minify.value, }); @@ -1111,77 +1161,91 @@ describe("bundler", () => { // loop initializers to already be in the top-level scope. For more info // see: https://github.com/evanw/esbuild/issues/1455. itBundled(`extra/ForLoopInitializerHoisting`, { - ...programs( - { - "1.js": `if (require('./nested1').foo() !== 10) throw 'fail'`, - "2.js": `if (require('./nested2').foo() !== 'c') throw 'fail'`, - "3.js": `if (require('./nested3').foo() !== 3) throw 'fail'`, + ...programs({ + 1: { + "in.js": ` + if (require('./nested').foo() !== 10) throw 'fail' + `, + "nested.js": ` + for (var i = 0; i < 10; i++) ; + export function foo() { return i } + `, }, - { - "nested1.js": ` - for (var i = 0; i < 10; i++) ; - export function foo() { return i } - `, - "nested2.js": ` - for (var i in {a: 1, b: 2, c: 3}) ; - export function foo() { return i } - `, - "nested3.js": ` - for (var i of [1, 2, 3]) ; - export function foo() { return i } - `, + 2: { + "in.js": ` + if (require('./nested').foo() !== 'c') throw 'fail' + `, + "nested.js": ` + for (var i in {a: 1, b: 2, c: 3}) ; + export function foo() { return i } + `, + }, + 3: { + "in.js": ` + if (require('./nested').foo() !== 3) throw 'fail' + `, + "nested.js": ` + for (var i of [1, 2, 3]) ; + export function foo() { return i } + `, }, - ), + }), }); // Test tree shaking itBundled(`extra/TreeShaking`, { - ...programs( - { - // Keep because used (ES6) - "1.js": `import * as foo from './foo1'; if (global.dce0 !== 123 || foo.abc !== 'abc') throw 'fail'`, - // Remove because unused (ES6) - "2.js": `import * as foo from './foo2'; if (global.dce1 !== void 0) throw 'fail'`, - // Keep because side effects (ES6) - "3.js": `import * as foo from './foo3'; if (global.dce2 !== 123) throw 'fail'`, - // Keep because used (CommonJS) - "4.js": `import foo from './foo4'; if (global.dce3 !== 123 || foo.abc !== 'abc') throw 'fail'`, - // Remove because unused (CommonJS) - "5.js": `import foo from './foo5'; if (global.dce4 !== void 0) throw 'fail'`, - // Keep because side effects (CommonJS) - "6.js": `import foo from './foo6'; if (global.dce5 !== 123) throw 'fail'`, - // Note: Tree shaking this could technically be considered incorrect because - // the import is for a property whose getter in this case has a side effect. - // However, this is very unlikely and the vast majority of the time people - // would likely rather have the code be tree-shaken. This test case enforces - // the technically incorrect behavior as documentation that this edge case - // is being ignored. - "7.js": `import {foo, bar} from './foo7'; let unused = foo; if (bar) throw 'expected "foo" to be tree-shaken'`, + ...programs({ + 1: { + "entry.js": `import * as foo from './foo'; if (global.dce0 !== 123 || foo.abc !== 'abc') throw 'fail'`, + "foo/index.js": `global.dce0 = 123; export const abc = 'abc'`, + "foo/package.json": `{ "sideEffects": false }`, }, - { - "foo1/index.js": `global.dce0 = 123; export const abc = 'abc'`, - "foo1/package.json": `{ "sideEffects": false }`, - "foo2/index.js": `global.dce1 = 123; export const abc = 'abc'`, - "foo2/package.json": `{ "sideEffects": false }`, - "foo3/index.js": `global.dce2 = 123; export const abc = 'abc'`, - "foo3/package.json": `{ "sideEffects": true }`, - "foo4/index.js": `global.dce3 = 123; exports.abc = 'abc'`, - "foo4/package.json": `{ "sideEffects": false }`, - "foo5/index.js": `global.dce4 = 123; exports.abc = 'abc'`, - "foo5/package.json": `{ "sideEffects": false }`, - "foo6/index.js": `global.dce5 = 123; exports.abc = 'abc'`, - "foo6/package.json": `{ "sideEffects": true }`, - "foo7.js": `module.exports = {get foo() { module.exports.bar = 1 }, bar: 0}`, - }, - ), - // foo2 and foo5 are removed from the bundle, and so is `let unused = foo` in 7.js. + 2: { + "entry.js": `import * as foo from './foo'; if (global.dce1 !== void 0) throw 'fail'`, + "foo/index.js": `global.dce1 = 123; export const abc = 'abc'`, + "foo/package.json": `{ "sideEffects": false }`, + }, + 3: { + "entry.js": `import * as foo from './foo'; if (global.dce2 !== 123) throw 'fail'`, + "foo/index.js": `global.dce2 = 123; export const abc = 'abc'`, + "foo/package.json": `{ "sideEffects": true }`, + }, + 4: { + "entry.js": `import foo from './foo'; if (global.dce3 !== 123 || foo.abc !== 'abc') throw 'fail'`, + "foo/index.js": `global.dce3 = 123; exports.abc = 'abc'`, + "foo/package.json": `{ "sideEffects": false }`, + }, + 5: { + "entry.js": `import foo from './foo'; if (global.dce4 !== void 0) throw 'fail'`, + "foo/index.js": `global.dce4 = 123; exports.abc = 'abc'`, + "foo/package.json": `{ "sideEffects": false }`, + }, + 6: { + "entry.js": `import foo from './foo'; if (global.dce5 !== 123) throw 'fail'`, + "foo/index.js": `global.dce5 = 123; exports.abc = 'abc'`, + "foo/package.json": `{ "sideEffects": true }`, + }, + // Note: Tree shaking this could technically be considered incorrect because + // the import is for a property whose getter in this case has a side effect. + // However, this is very unlikely and the vast majority of the time people + // would likely rather have the code be tree-shaken. This test case enforces + // the technically incorrect behavior as documentation that this edge case + // is being ignored. + 7: { + "entry.js": `import {foo, bar} from './foo'; let unused = foo; if (bar) throw 'expected "foo" to be tree-shaken'`, + "foo.js": `module.exports = {get foo() { module.exports.bar = 1 }, bar: 0}`, + }, + }), + // The packages of 2 and 5 and the `let unused = foo` of 7 are removed. assertNotPresent: { - "/out.js": ["dce1 = 123", "dce4 = 123", "unused"], + "/out/2/entry.js": "dce1 = 123", + "/out/5/entry.js": "dce4 = 123", + "/out/7/entry.js": "unused", }, }); - // Test for an implicit and explicit "**/" prefix (see https://github.com/evanw/esbuild/issues/1184) + // Test for an implicit and explicit "**/" prefix (see https://github.com/evanw/esbuild/issues/1184). + // Bun joins the pattern onto the package directory, so "x.*" does not match dir/x.js. itBundled(`extra/TreeShaking8`, { - // Bun joins the pattern onto the package directory, so "x.*" does not match dir/x.js. todo: true, files: { "entry.js": `import './foo'; if (global.dce6 !== 123) throw 'fail'`, @@ -1235,29 +1299,40 @@ describe("bundler", () => { // Test obscure CommonJS symbol edge cases itBundled(`extra/CommonJSSymbol`, { - ...programs( - { - "1.js": `const ns = require('./foo1'); if (ns.foo !== 123 || ns.bar !== 123) throw 'fail'`, - "2.js": `require('./foo2'); require('./bar2')`, - "3.js": `const ns = require('./foo3'); if (ns.foo !== void 0 || ns.default.foo !== 123) throw 'fail'`, - "4.js": `const ns = require('./foo4'); if (ns !== 123) throw 'fail'`, - "5.js": `require('./foo5')`, - "6.js": `require('./foo6')`, - "7.js": `const ns = require('./foo7'); if (ns.a !== 123 || ns.b.a !== 123) throw 'fail'`, + ...programs({ + 1: { + "in.js": `const ns = require('./foo'); if (ns.foo !== 123 || ns.bar !== 123) throw 'fail'`, + "foo.js": `var exports, module; module.exports.foo = 123; exports.bar = exports.foo`, }, - { - "foo1.js": `var exports, module; module.exports.foo = 123; exports.bar = exports.foo`, - "foo2.js": `let exports; if (exports !== void 0) throw 'fail'`, - "bar2.js": `let module; if (module !== void 0) throw 'fail'`, - "foo3.js": `var exports = {foo: 123}; export default exports`, - "foo4.ts": `let module = 123; export = module`, - "foo5.js": `var require; if (require !== void 0) throw 'fail'`, - "foo6.js": `var require = x => x; if (require('does not exist') !== 'does not exist') throw 'fail'`, - "foo7.js": `exports.a = 123; exports.b = this`, - }, - ), + 2: { + "in.js": `require('./foo'); require('./bar')`, + "foo.js": `let exports; if (exports !== void 0) throw 'fail'`, + "bar.js": `let module; if (module !== void 0) throw 'fail'`, + }, + 3: { + "in.js": `const ns = require('./foo'); if (ns.foo !== void 0 || ns.default.foo !== 123) throw 'fail'`, + "foo.js": `var exports = {foo: 123}; export default exports`, + }, + 4: { + "in.js": `const ns = require('./foo'); if (ns !== 123) throw 'fail'`, + "foo.ts": `let module = 123; export = module`, + }, + 5: { + "in.js": `require('./foo')`, + "foo.js": `var require; if (require !== void 0) throw 'fail'`, + }, + 6: { + "in.js": `require('./foo')`, + "foo.js": `var require = x => x; if (require('does not exist') !== 'does not exist') throw 'fail'`, + }, + 7: { + "in.js": `const ns = require('./foo'); if (ns.a !== 123 || ns.b.a !== 123) throw 'fail'`, + "foo.js": `exports.a = 123; exports.b = this`, + }, + }), }); - // The top-level `this` of the ES module becomes null instead of undefined, so this one only bundles. + // Not in the group: the top-level `this` of foo.js is printed as null instead of undefined + // (#32173 fixes that), so this one only checks that it bundles. itBundled(`extra/CommonJSSymbol8`, { files: { "in.js": `const ns = require('./foo'); if (ns.a !== 123 || ns.b !== void 0) throw 'fail'`, @@ -1265,8 +1340,10 @@ describe("bundler", () => { }, }); - // Function hoisting tests + // Function hoisting tests. The esbuild suite does not bundle 1 and 14 (4 and 15 are their bundled + // versions). Transformed, bun does not hoist the functions out of the blocks (#23633). itBundled(`extra/FunctionHoisting1`, { + todo: true, files: { "in.js": ` if (1) { @@ -1278,11 +1355,33 @@ describe("bundler", () => { if (typeof f !== 'function' || f() !== null) throw 'fail' `, }, + bundling: false, run: true, }); - itBundled(`extra/FunctionHoisting2`, { + itBundled(`extra/FunctionHoisting14`, { + todo: true, files: { - "in.js": ` + "in.ts": ` + if (1) { + var a = 'a' + for (var b = 'b'; 0; ) ; + for (var c in { c: 0 }) ; + for (var d of ['d']) ; + for (var e = 'e' in {}) ; + function f() { return 'f' } + } + const observed = JSON.stringify({ a, b, c, d, e, f: f() }) + const expected = JSON.stringify({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f' }) + if (observed !== expected) throw observed + `, + }, + bundling: false, + run: true, + }); + itBundled(`extra/FunctionHoisting`, { + ...programs({ + 2: { + "in.js": ` 'use strict' if (1) { function f() { @@ -1292,12 +1391,9 @@ describe("bundler", () => { } if (typeof f !== 'undefined') throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting3`, { - files: { - "in.js": ` + }, + 3: { + "in.js": ` export {} if (1) { function f() { @@ -1307,12 +1403,9 @@ describe("bundler", () => { } if (typeof f !== 'undefined') throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting4`, { - files: { - "in.js": ` + }, + 4: { + "in.js": ` if (1) { function f() { return f @@ -1321,12 +1414,9 @@ describe("bundler", () => { } if (typeof f !== 'function' || f() !== null) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting5`, { - files: { - "in.js": ` + }, + 5: { + "in.js": ` var f if (1) { function f() { @@ -1336,12 +1426,9 @@ describe("bundler", () => { } if (typeof f !== 'function' || f() !== null) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting6`, { - files: { - "in.js": ` + }, + 6: { + "in.js": ` 'use strict' if (1) { function f() { @@ -1350,12 +1437,9 @@ describe("bundler", () => { } if (typeof f !== 'undefined') throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting7`, { - files: { - "in.js": ` + }, + 7: { + "in.js": ` export {} if (1) { function f() { @@ -1364,12 +1448,9 @@ describe("bundler", () => { } if (typeof f !== 'undefined') throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting8`, { - files: { - "in.js": ` + }, + 8: { + "in.js": ` var f = 1 if (1) { function f() { @@ -1379,12 +1460,9 @@ describe("bundler", () => { } if (typeof f !== 'function' || f() !== null) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting9`, { - files: { - "in.js": ` + }, + 9: { + "in.js": ` 'use strict' var f = 1 if (1) { @@ -1394,12 +1472,9 @@ describe("bundler", () => { } if (f !== 1) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting10`, { - files: { - "in.js": ` + }, + 10: { + "in.js": ` export {} var f = 1 if (1) { @@ -1409,16 +1484,13 @@ describe("bundler", () => { } if (f !== 1) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting11`, { - files: { - "in.js": ` + }, + 11: { + "in.js": ` import {f, g} from './other' if (f !== void 0 || g !== 'g') throw 'fail' `, - "other.js": ` + "other.js": ` 'use strict' var f if (1) { @@ -1429,12 +1501,9 @@ describe("bundler", () => { exports.f = f exports.g = 'g' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting12`, { - files: { - "in.js": ` + }, + 12: { + "in.js": ` let f = 1 // This should not be turned into "if (1) let f" because that's a syntax error if (1) @@ -1443,39 +1512,15 @@ describe("bundler", () => { } if (f !== 1) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting13`, { - files: { - "in.js": ` + }, + 13: { + "in.js": ` x: function f() { return 1 } if (f() !== 1) throw 'fail' `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting14`, { - files: { - "in.ts": ` - if (1) { - var a = 'a' - for (var b = 'b'; 0; ) ; - for (var c in { c: 0 }) ; - for (var d of ['d']) ; - for (var e = 'e' in {}) ; - function f() { return 'f' } - } - const observed = JSON.stringify({ a, b, c, d, e, f: f() }) - const expected = JSON.stringify({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f' }) - if (observed !== expected) throw observed - `, - }, - run: true, - }); - itBundled(`extra/FunctionHoisting15`, { - files: { - "in.ts": ` + }, + 15: { + "in.ts": ` if (1) { var a = 'a' for (var b = 'b'; 0; ) ; @@ -1488,8 +1533,8 @@ describe("bundler", () => { const expected = JSON.stringify({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f' }) if (observed !== expected) throw observed `, - }, - run: true, + }, + }), }); itBundled(`extra/FunctionHoistingKeepNames1`, { todo: true, // keepNames requires Object.defineProperty implementation @@ -1557,7 +1602,8 @@ describe("bundler", () => { // Test the correctness of side effect order for the TypeScript namespace exports itBundled(`extra/ObjectRestPattern`, { ...programs({ - "1.ts": ` + 1: { + "in.ts": ` function fn() { let trail = [] let t = k => (trail.push(k), k) @@ -1580,7 +1626,9 @@ describe("bundler", () => { } if (fn() !== ns.result) throw 'fail' `, - "2.ts": ` + }, + 2: { + "in.ts": ` let obj = {}; ({a: obj.a, ...obj.b} = {a: 1, b: 2, c: 3}); [obj.c, , ...obj.d] = [1, 2, 3]; @@ -1594,14 +1642,19 @@ describe("bundler", () => { } if (JSON.stringify(obj) !== JSON.stringify(ns)) throw 'fail' `, - "3.ts": ` + }, + 3: { + "in.ts": ` var z = {x: {z: 'z'}, y: 'y'}, {x: z, ...y} = z if (y.y !== 'y' || z.z !== 'z') throw 'fail' `, - "4.ts": ` + }, + 4: { + "in.ts": ` var z = {x: {x: 'x'}, y: 'y'}, {[(z = {z: 'z'}, 'x')]: x, ...y} = z if (x.x !== 'x' || y.y !== 'y' || z.z !== 'z') throw 'fail' `, + }, }), });