diff options
| author | 2023-04-11 21:18:49 -0400 | |
|---|---|---|
| committer | 2023-04-11 18:18:49 -0700 | |
| commit | 519f9aac8081d961f8e79062f4c0f26ef256027b (patch) | |
| tree | e65dbc411e6f745726904e19349dbd2af7ebbd28 | |
| parent | a744f5369d356424c3039f9dcf6d7f9412565ecf (diff) | |
| download | bun-519f9aac8081d961f8e79062f4c0f26ef256027b.tar.gz bun-519f9aac8081d961f8e79062f4c0f26ef256027b.tar.zst bun-519f9aac8081d961f8e79062f4c0f26ef256027b.zip | |
finish refining bundler tests (#2623)
* bundler tests
* test refining, 257/847
* bundler tests, 298/849
* more bundler tests
* finish dce
| -rwxr-xr-x | test/bun.lockb | bin | 36614 -> 36614 bytes | |||
| -rw-r--r-- | test/bundler/bundler_cjs2esm.test.ts | 8 | ||||
| -rw-r--r-- | test/bundler/bundler_edgecase.test.ts | 28 | ||||
| -rw-r--r-- | test/bundler/esbuild/dce.test.ts | 1235 | ||||
| -rw-r--r-- | test/bundler/esbuild/default.test.ts | 930 | ||||
| -rw-r--r-- | test/bundler/expectBundled.md | 6 | ||||
| -rw-r--r-- | test/bundler/expectBundled.ts | 133 | ||||
| -rw-r--r-- | test/bundler/report-bundler-test-progress.sh | 43 | ||||
| -rwxr-xr-x | test/bundler/run-single-bundler-test.sh | 2 | ||||
| -rw-r--r-- | test/bundler/transpiler.test.js | 10 | ||||
| -rw-r--r-- | test/package.json | 2 |
11 files changed, 1568 insertions, 829 deletions
diff --git a/test/bun.lockb b/test/bun.lockb Binary files differindex f95cf3658..d1c1732e4 100755 --- a/test/bun.lockb +++ b/test/bun.lockb diff --git a/test/bundler/bundler_cjs2esm.test.ts b/test/bundler/bundler_cjs2esm.test.ts new file mode 100644 index 000000000..4c37e9591 --- /dev/null +++ b/test/bundler/bundler_cjs2esm.test.ts @@ -0,0 +1,8 @@ +import assert from "assert"; +import dedent from "dedent"; +import { bundlerTest, expectBundled, itBundled, testForFile } from "./expectBundled"; +var { describe, test, expect } = testForFile(import.meta.path); + +describe("bundler", () => { + return; +}); diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index d57fd3281..cb8a90b81 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -23,4 +23,32 @@ describe("bundler", () => { }, run: { stdout: "foo" }, }); + itBundled("edgecase/ImportStarSyntaxErrorBug", { + // bug: 'import {ns}, * as import_x from "x";' + files: { + "/entry.js": /* js */ ` + export {ns} from 'x' + export * as ns2 from 'x' + `, + }, + external: ["x"], + runtimeFiles: { + "/node_modules/x/index.js": `export const ns = 1`, + }, + run: true, + }); + // itBundled("edgecase/PureCommentInLineComment", { + // files: { + // "/entry.js": /* js */ ` + // (function () { + // // Some text that contains a pure comment in it like /* @__PURE__ */, with other text around it. + + // // console.log; + + // fn2("TODO: should this call be kept?"); + // })(); + // `, + // }, + // dce: true, + // }); }); diff --git a/test/bundler/esbuild/dce.test.ts b/test/bundler/esbuild/dce.test.ts index 2cd8a971b..8b3e2c289 100644 --- a/test/bundler/esbuild/dce.test.ts +++ b/test/bundler/esbuild/dce.test.ts @@ -1,3 +1,5 @@ +import assert from "assert"; +import dedent from "dedent"; import { expectBundled, itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); @@ -6,10 +8,10 @@ var { describe, test, expect } = testForFile(import.meta.path); // For debug, all files are written to $TEMP/bun-bundle-tests/dce +// To understand what `dce: true` is doing, see ../expectBundled.md's "dce: true" section + describe("bundler", () => { - return; itBundled("dce/PackageJsonSideEffectsFalseKeepNamedImportES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -25,9 +27,11 @@ describe("bundler", () => { } `, }, + run: { + stdout: "hello\n123", + }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepNamedImportCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -43,13 +47,15 @@ describe("bundler", () => { } `, }, + run: { + stdout: "hello\n123", + }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepStarImportES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import * as ns from "demo-pkg" - console.log(ns) + console.log(JSON.stringify(ns)) `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` export const foo = 123 @@ -61,13 +67,15 @@ describe("bundler", () => { } `, }, + run: { + stdout: 'hello\n{"foo":123}', + }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepStarImportCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import * as ns from "demo-pkg" - console.log(ns) + console.log(JSON.stringify(ns)) `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` exports.foo = 123 @@ -79,16 +87,18 @@ describe("bundler", () => { } `, }, + run: { + stdout: 'hello\n{"default":{"foo":123},"foo":123}', + }, }); itBundled("dce/PackageJsonSideEffectsTrueKeepES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - export const foo = 123 + export const foo = "FAILED" console.log('hello') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -97,9 +107,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "hello\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsTrueKeepCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -115,9 +128,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "hello\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -134,12 +150,12 @@ describe("bundler", () => { } `, }, - /* TODO FIX expectedScanLog: `Users/user/project/src/entry.js: WARNING: Ignoring this import because "Users/user/project/node_modules/demo-pkg/index.js" was marked as having no side effects - Users/user/project/node_modules/demo-pkg/package.json: NOTE: "sideEffects" is false in the enclosing "package.json" file: - `, */ + dce: true, + run: { + stdout: "hello\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseKeepBareImportAndRequireCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -156,20 +172,20 @@ describe("bundler", () => { } `, }, - /* TODO FIX expectedScanLog: `Users/user/project/src/entry.js: WARNING: Ignoring this import because "Users/user/project/node_modules/demo-pkg/index.js" was marked as having no side effects - Users/user/project/node_modules/demo-pkg/package.json: NOTE: "sideEffects" is false in the enclosing "package.json" file: - `, */ + dce: true, + run: { + stdout: "hello\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveBareImportES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - export const foo = 123 - console.log('hello') + export const foo = "TEST FAILED" + console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -177,20 +193,20 @@ describe("bundler", () => { } `, }, - /* TODO FIX expectedScanLog: `Users/user/project/src/entry.js: WARNING: Ignoring this import because "Users/user/project/node_modules/demo-pkg/index.js" was marked as having no side effects - Users/user/project/node_modules/demo-pkg/package.json: NOTE: "sideEffects" is false in the enclosing "package.json" file: - `, */ + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveBareImportCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - exports.foo = 123 - console.log('hello') + exports.foo = "TEST FAILED" + console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -198,20 +214,20 @@ describe("bundler", () => { } `, }, - /* TODO FIX expectedScanLog: `Users/user/project/src/entry.js: WARNING: Ignoring this import because "Users/user/project/node_modules/demo-pkg/index.js" was marked as having no side effects - Users/user/project/node_modules/demo-pkg/package.json: NOTE: "sideEffects" is false in the enclosing "package.json" file: - `, */ + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveNamedImportES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - export const foo = 123 - console.log('hello') + export const foo = "TEST FAILED" + console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -219,17 +235,20 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveNamedImportCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - exports.foo = 123 - console.log('hello') + exports.foo = "TEST FAILED" + console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -237,17 +256,20 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveStarImportES6", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import * as ns from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - export const foo = 123 - console.log('hello') + export const foo = "TEST FAILED" + console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -255,17 +277,20 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseRemoveStarImportCommonJS", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import * as ns from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - exports.foo = 123 - console.log('hello') + exports.foo = "TEST FAILED" + console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -273,16 +298,19 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayRemove", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('hello') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -291,17 +319,20 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeep", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` - export const foo = 123 - console.log('hello') + export const foo = "TEST FAILED" + console.log("hello") `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -309,20 +340,23 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "hello\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepMainUseModule", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index-main.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -333,20 +367,24 @@ describe("bundler", () => { } `, }, + dce: true, + mainFields: ["module"], + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepMainUseMain", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index-main.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('this should be kept') `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -357,20 +395,24 @@ describe("bundler", () => { } `, }, + dce: true, + mainFields: ["main"], + run: { + stdout: "this should be kept\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepMainImplicitModule", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index-main.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -381,9 +423,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepMainImplicitMain", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -395,11 +440,11 @@ describe("bundler", () => { require('demo-pkg') `, "/Users/user/project/node_modules/demo-pkg/index-main.js": /* js */ ` - export const foo = 123 + export const foo = "POSSIBLE_REMOVAL" console.log('this should be kept') `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -410,20 +455,23 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "this should be kept\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleUseModule", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg/index-main.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('this should be kept') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -434,9 +482,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "this should be kept\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleUseMain", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -448,7 +499,7 @@ describe("bundler", () => { `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` export const foo = 123 - console.log('TEST FAILED') + console.log('hello') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` { @@ -458,9 +509,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "hello\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleImplicitModule", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -482,9 +536,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "this should be kept\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayKeepModuleImplicitMain", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -496,11 +553,11 @@ describe("bundler", () => { require('demo-pkg') `, "/Users/user/project/node_modules/demo-pkg/index-main.js": /* js */ ` - export const foo = 123 + export const foo = "POSSIBLE_REMOVAL" console.log('this should be kept') `, "/Users/user/project/node_modules/demo-pkg/index-module.js": /* js */ ` - export const foo = 123 + export const foo = "TEST FAILED" console.log('TEST FAILED') `, "/Users/user/project/node_modules/demo-pkg/package.json": /* json */ ` @@ -511,9 +568,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "this should be kept\nunused import", + }, }); itBundled("dce/PackageJsonSideEffectsArrayGlob", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg/keep/this/file" @@ -531,12 +591,12 @@ describe("bundler", () => { } `, }, - /* TODO FIX expectedScanLog: `Users/user/project/src/entry.js: WARNING: Ignoring this import because "Users/user/project/node_modules/demo-pkg/remove/this/file.js" was marked as having no side effects - Users/user/project/node_modules/demo-pkg/package.json: NOTE: It was excluded from the "sideEffects" array in the enclosing "package.json" file: - `, */ + dce: true, + run: { + stdout: "this should be kept", + }, }); itBundled("dce/PackageJsonSideEffectsNestedDirectoryRemove", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg/a/b/c" @@ -548,13 +608,16 @@ describe("bundler", () => { } `, "/Users/user/project/node_modules/demo-pkg/a/b/c/index.js": /* js */ ` - export const foo = 123 - console.log('hello') + export const foo = "TEST FAILED" + console.log('TEST FAILED') `, }, + dce: true, + run: { + stdout: "unused import", + }, }); itBundled("dce/PackageJsonSideEffectsKeepExportDefaultExpr", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import foo from "demo-pkg" @@ -567,9 +630,18 @@ describe("bundler", () => { } `, }, + runtimeFiles: { + "/test.js": /* js */ ` + globalThis.exprWithSideEffects = () => 1; + await import('./out'); + `, + }, + run: { + file: "/test.js", + stdout: "1", + }, }); itBundled("dce/PackageJsonSideEffectsFalseNoWarningInNodeModulesIssue999", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg" @@ -580,8 +652,8 @@ describe("bundler", () => { console.log('unused import') `, "/Users/user/project/node_modules/demo-pkg2/index.js": /* js */ ` - export const foo = 123 - console.log('hello') + export const foo = "FAILED" + console.log('FAILED') `, "/Users/user/project/node_modules/demo-pkg2/package.json": /* json */ ` { @@ -589,9 +661,12 @@ describe("bundler", () => { } `, }, + dce: true, + run: { + stdout: "unused import\nused import", + }, }); itBundled("dce/PackageJsonSideEffectsFalseIntermediateFilesUnused", { - // GENERATED files: { "/Users/user/project/src/entry.js": `import {foo} from "demo-pkg"`, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` @@ -601,9 +676,12 @@ describe("bundler", () => { "/Users/user/project/node_modules/demo-pkg/foo.js": `export const foo = 123`, "/Users/user/project/node_modules/demo-pkg/package.json": `{ "sideEffects": false }`, }, + dce: true, + onAfterBundle(api) { + api.expectFile("/out.js").toBe(""); + }, }); itBundled("dce/PackageJsonSideEffectsFalseIntermediateFilesUsed", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "demo-pkg" @@ -611,11 +689,15 @@ describe("bundler", () => { `, "/Users/user/project/node_modules/demo-pkg/index.js": /* js */ ` export {foo} from "./foo.js" - throw 'keep this' + console.log('hello') `, "/Users/user/project/node_modules/demo-pkg/foo.js": `export const foo = 123`, "/Users/user/project/node_modules/demo-pkg/package.json": `{ "sideEffects": false }`, }, + dce: true, + run: { + stdout: "hello\n123", + }, }); itBundled("dce/PackageJsonSideEffectsFalseIntermediateFilesChainAll", { // GENERATED @@ -628,7 +710,7 @@ describe("bundler", () => { "/Users/user/project/node_modules/a/package.json": `{ "sideEffects": false }`, "/Users/user/project/node_modules/b/index.js": /* js */ ` export {foo} from "c" - throw 'keep this' + console.log('hello') `, "/Users/user/project/node_modules/b/package.json": `{ "sideEffects": false }`, "/Users/user/project/node_modules/c/index.js": `export {foo} from "d"`, @@ -636,9 +718,12 @@ describe("bundler", () => { "/Users/user/project/node_modules/d/index.js": `export const foo = 123`, "/Users/user/project/node_modules/d/package.json": `{ "sideEffects": false }`, }, + dce: true, + run: { + stdout: "hello\n123", + }, }); itBundled("dce/PackageJsonSideEffectsFalseIntermediateFilesChainOne", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "a" @@ -647,15 +732,18 @@ describe("bundler", () => { "/Users/user/project/node_modules/a/index.js": `export {foo} from "b"`, "/Users/user/project/node_modules/b/index.js": /* js */ ` export {foo} from "c" - throw 'keep this' + console.log('hello') `, "/Users/user/project/node_modules/b/package.json": `{ "sideEffects": false }`, "/Users/user/project/node_modules/c/index.js": `export {foo} from "d"`, "/Users/user/project/node_modules/d/index.js": `export const foo = 123`, }, + dce: true, + run: { + stdout: "hello\n123", + }, }); itBundled("dce/PackageJsonSideEffectsFalseIntermediateFilesDiamond", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import {foo} from "a" @@ -667,22 +755,25 @@ describe("bundler", () => { `, "/Users/user/project/node_modules/b1/index.js": /* js */ ` export {foo} from "c" - throw 'keep this 1' + console.log('hello 1') `, "/Users/user/project/node_modules/b1/package.json": `{ "sideEffects": false }`, "/Users/user/project/node_modules/b2/index.js": /* js */ ` export {foo} from "c" - throw 'keep this 2' + console.log('hello 2') `, "/Users/user/project/node_modules/b2/package.json": `{ "sideEffects": false }`, "/Users/user/project/node_modules/c/index.js": `export {foo} from "d"`, "/Users/user/project/node_modules/d/index.js": `export const foo = 123`, }, + dce: true, + run: { + stdout: "hello 1\nhello 2\n123", + }, }); itBundled("dce/PackageJsonSideEffectsFalseOneFork", { - // GENERATED files: { - "/Users/user/project/src/entry.js": `import("a").then(x => assert(x.foo === "foo"))`, + "/Users/user/project/src/entry.js": `import("a").then(x => console.log(x.foo))`, "/Users/user/project/node_modules/a/index.js": `export {foo} from "b"`, "/Users/user/project/node_modules/b/index.js": /* js */ ` export {foo, bar} from "c" @@ -695,11 +786,14 @@ describe("bundler", () => { `, "/Users/user/project/node_modules/d/index.js": `export let baz = "baz"`, }, + dce: true, + run: { + stdout: "foo", + }, }); itBundled("dce/PackageJsonSideEffectsFalseAllFork", { - // GENERATED files: { - "/Users/user/project/src/entry.js": `import("a").then(x => assert(x.foo === "foo"))`, + "/Users/user/project/src/entry.js": `import("a").then(x => console.log(x.foo))`, "/Users/user/project/node_modules/a/index.js": `export {foo} from "b"`, "/Users/user/project/node_modules/b/index.js": /* js */ ` export {foo, bar} from "c" @@ -714,70 +808,102 @@ describe("bundler", () => { "/Users/user/project/node_modules/d/index.js": `export let baz = "baz"`, "/Users/user/project/node_modules/d/package.json": `{ "sideEffects": false }`, }, + dce: true, + run: { + stdout: "foo", + }, }); itBundled("dce/JSONLoaderRemoveUnused", { - // GENERATED files: { "/entry.js": /* js */ ` import unused from "./example.json" console.log('unused import') `, - "/example.json": `{"data": true}`, + "/example.json": `{"data": "FAILED"}`, + }, + dce: true, + run: { + stdout: "unused import", }, }); itBundled("dce/TextLoaderRemoveUnused", { - // GENERATED files: { "/entry.js": /* js */ ` import unused from "./example.txt" console.log('unused import') `, - "/example.txt": `some data`, + "/example.txt": `TEST FAILED`, + }, + dce: true, + run: { + stdout: "unused import", }, }); itBundled("dce/Base64LoaderRemoveUnused", { - // GENERATED files: { "/entry.js": /* js */ ` import unused from "./example.data" console.log('unused import') `, - "/example.data": `some data`, + "/example.data": `TEST FAILED`, + }, + dce: true, + run: { + stdout: "unused import", + }, + loader: { + ".data": "base64", }, }); itBundled("dce/DataURLLoaderRemoveUnused", { - // GENERATED files: { "/entry.js": /* js */ ` import unused from "./example.data" console.log('unused import') `, - "/example.data": `some data`, + "/example.data": `TEST FAILED`, + }, + dce: true, + run: { + stdout: "unused import", + }, + loader: { + ".data": "dataurl", }, }); itBundled("dce/FileLoaderRemoveUnused", { - // GENERATED files: { "/entry.js": /* js */ ` import unused from "./example.data" console.log('unused import') `, - "/example.data": `some data`, + "/example.data": `TEST FAILED`, + }, + dce: true, + run: { + stdout: "unused import", + }, + loader: { + ".data": "file", }, }); itBundled("dce/RemoveUnusedImportMeta", { - // GENERATED files: { "/entry.js": /* js */ ` function foo() { - console.log(import.meta.url, import.meta.path) + console.log(import.meta.url, import.meta.path, 'FAILED') } console.log('foo is unused') `, }, + dce: true, + run: { + stdout: "foo is unused", + }, }); itBundled("dce/RemoveUnusedPureCommentCalls", { - // GENERATED + // in this test, the bundler must drop all `_yes` variables entirely, and then + // preserve the pure comments in the same way esbuild does files: { "/entry.js": /* js */ ` function bar() {} @@ -841,54 +967,100 @@ describe("bundler", () => { let new_exp_no = /* @__PURE__ */ new foo() ** foo(); `, }, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + assert(!code.includes("_yes"), "should not contain any *_yes variables"); + assert(code.includes("var bare = foo(bar)"), "should contain `var bare = foo(bar)`"); + const keep = [ + ["at_no", true], + ["new_at_no", true], + ["nospace_at_no", true], + ["nospace_new_at_no", true], + ["num_no", true], + ["new_num_no", true], + ["nospace_num_no", true], + ["nospace_new_num_no", true], + ["dot_no", true], + ["new_dot_no", true], + ["nested_no", true], + ["new_nested_no", true], + ["single_at_no", true], + ["new_single_at_no", true], + ["single_num_no", true], + ["new_single_num_no", true], + ["bad_no", false], + ["new_bad_no", false], + ["parens_no", false], + ["new_parens_no", false], + ["exp_no", true], + ["new_exp_no", true], + ]; + for (const [name, pureComment] of keep) { + const regex = new RegExp(`${name}\\s*=[^\/\n]*(\\/\\*.*?\\*\\/)?`, "g"); + const match = regex.exec(code); + assert(!!match, `should contain ${name}`); + assert(pureComment ? !!match[1] : !match[1], `should contain a pure comment for ${name}`); + } + }, }); itBundled("dce/TreeShakingReactElements", { - // GENERATED files: { "/entry.jsx": /* jsx */ ` function Foo() {} - let a = <div/> - let b = <Foo>{a}</Foo> - let c = <>{b}</> + let DROP_a = <div/> + let DROP_b = <Foo>{DROP_a}</Foo> + let DROP_c = <>{DROP_b}</> let d = <div/> let e = <Foo>{d}</Foo> let f = <>{e}</> - console.log(f) - `, - }, - }); - itBundled("dce/DisableTreeShaking", { - // GENERATED - files: { - "/entry.jsx": /* jsx */ ` - import './remove-me' - function RemoveMe1() {} - let removeMe2 = 0 - class RemoveMe3 {} - - import './keep-me' - function KeepMe1() {} - let keepMe2 = <KeepMe1/> - function keepMe3() { console.log('side effects') } - let keepMe4 = /* @__PURE__ */ keepMe3() - let keepMe5 = pure() - let keepMe6 = some.fn() + console.log(JSON.stringify(f)) `, - "/remove-me.js": `export default 'unused'`, - "/keep-me/index.js": `console.log('side effects')`, - "/keep-me/package.json": `{ "sideEffects": false }`, - }, - // TODO: Unsure how to port this: https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_dce_test.go#L1249 - ignoreDCEAnnotations: true, - define: { - pure: "???", - "some.fn": "???", - }, - }); + + "/node_modules/react/index.js": `export const Fragment = 'F'`, + "/node_modules/react/jsx-dev-runtime.js": `export const jsxDEV = (a,b) => [a,b]; export const Fragment = 'F'`, + }, + jsx: { + development: true, + automaticRuntime: true, + }, + dce: true, + run: { + stdout: `["F",{"children":[null,{"children":["div",{}]}]}]`, + }, + }); + // itBundled("dce/DisableTreeShaking", { + // // GENERATED + // files: { + // "/entry.jsx": /* jsx */ ` + // import './remove-me' + // function RemoveMe1() {} + // let removeMe2 = 0 + // class RemoveMe3 {} + + // import './keep-me' + // function KeepMe1() {} + // let keepMe2 = <KeepMe1/> + // function keepMe3() { console.log('side effects') } + // let keepMe4 = /* @__PURE__ */ keepMe3() + // let keepMe5 = pure() + // let keepMe6 = some.fn() + // `, + // "/remove-me.js": `export default 'unused'`, + // "/keep-me/index.js": `console.log('side effects')`, + // "/keep-me/package.json": `{ "sideEffects": false }`, + // }, + // // TODO: Unsure how to port this: https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_dce_test.go#L1249 + // ignoreDCEAnnotations: true, + // define: { + // pure: "???", + // "some.fn": "???", + // }, + // }); + 0; // the commented out test has a pure comment which unless this 0 line is here, that test will be removed + itBundled("dce/DeadCodeFollowingJump", { - // GENERATED files: { "/entry.js": /* js */ ` function testReturn() { @@ -971,9 +1143,10 @@ describe("bundler", () => { testStmts() `, }, + dce: true, + minifySyntax: true, }); itBundled("dce/RemoveTrailingReturn", { - // GENERATED files: { "/entry.js": /* js */ ` function foo() { @@ -1007,9 +1180,16 @@ describe("bundler", () => { `, }, minifySyntax: true, + dce: true, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + assert( + [...code.matchAll(/return/g)].length === 2, + "should remove 3 trailing returns and the arrow function return", + ); + }, }); itBundled("dce/ImportReExportOfNamespaceImport", { - // GENERATED files: { "/Users/user/project/entry.js": /* js */ ` import * as ns from 'pkg' @@ -1021,26 +1201,33 @@ describe("bundler", () => { `, "/Users/user/project/node_modules/pkg/package.json": `{ "sideEffects": false }`, "/Users/user/project/node_modules/pkg/foo.js": `module.exports = 123`, - "/Users/user/project/node_modules/pkg/bar.js": `module.exports = 'abc'`, + "/Users/user/project/node_modules/pkg/bar.js": `module.exports = 'FAILED'`, + }, + dce: true, + run: { + stdout: "123", }, }); itBundled("dce/TreeShakingImportIdentifier", { - // GENERATED files: { "/entry.js": /* js */ ` import * as a from './a' - new a.Keep() + new a.Keep().x().y() `, "/a.js": /* js */ ` import * as b from './b' - export class Keep extends b.Base {} - export class REMOVE extends b.Base {} + export class Keep extends b.Base { y() { console.log(2); return this; } } + export class REMOVE extends b.Base { y() { console.log(3); return this; } } `, - "/b.js": `export class Base {}`, + "/b.js": `export class Base { x() { console.log(1); return this; } }`, + }, + dce: true, + dceKeepMarkerCount: false, + run: { + stdout: "1\n2", }, }); itBundled("dce/TreeShakingObjectProperty", { - // GENERATED files: { "/entry.js": /* js */ ` let remove1 = { x: 'x' } @@ -1072,10 +1259,9 @@ describe("bundler", () => { `, }, treeShaking: true, - mode: "passthrough", + dce: true, }); itBundled("dce/TreeShakingClassProperty", { - // GENERATED files: { "/entry.js": /* js */ ` let remove1 = class { x } @@ -1095,20 +1281,22 @@ describe("bundler", () => { let remove15 = class { [false] = 'x' } let remove16 = class { [0n] = 'x' } let remove17 = class { toString() {} } - + let keep1 = class { [x] = 'x' } let keep2 = class { [x]() {} } let keep3 = class { get [x]() {} } let keep4 = class { set [x](_) {} } let keep5 = class { async [x]() {} } - let keep6 = class { [{ toString() {} }] = 'x' } + let keep6 = class { [{ toString() { console.log(1); } }] = 'x' } + + let POSSIBLE_REMOVAL_1 = class { [{ toString() {} }] = 'x' } `, }, + mode: "transform", treeShaking: true, - mode: "passthrough", + dce: true, }); itBundled("dce/TreeShakingClassStaticProperty", { - // GENERATED files: { "/entry.js": /* js */ ` let remove1 = class { static x } @@ -1134,14 +1322,16 @@ describe("bundler", () => { let keep5 = class { static get [x]() {} } let keep6 = class { static set [x](_) {} } let keep7 = class { static async [x]() {} } - let keep8 = class { static [{ toString() {} }] = 'x' } + let keep8 = class { static [{ toString() { console.log(1); } }] = 'x' } + + let POSSIBLE_REMOVAL_1 = class { static [{ toString() {} }] = 'x' } `, }, + mode: "transform", treeShaking: true, - mode: "passthrough", + dce: true, }); itBundled("dce/TreeShakingUnaryOperators", { - // GENERATED files: { "/entry.js": /* js */ ` // These operators may have side effects @@ -1161,9 +1351,10 @@ describe("bundler", () => { void REMOVE; `, }, + format: "iife", + dce: true, }); itBundled("dce/TreeShakingBinaryOperators", { - // GENERATED files: { "/entry.js": /* js */ ` // These operators may have side effects @@ -1215,30 +1406,34 @@ describe("bundler", () => { REMOVE && REMOVE2; `, }, + dce: true, + format: "iife", }); itBundled("dce/TreeShakingNoBundleESM", { - // GENERATED files: { "/entry.js": /* js */ ` function keep() {} - function unused() {} + function REMOVE() {} keep() `, }, format: "esm", - mode: "convertformat", + mode: "transform", + treeShaking: true, + dce: true, }); itBundled("dce/TreeShakingNoBundleCJS", { // GENERATED files: { "/entry.js": /* js */ ` function keep() {} - function unused() {} + function REMOVE() {} keep() `, }, format: "cjs", - mode: "convertformat", + treeShaking: true, + mode: "transform", }); itBundled("dce/TreeShakingNoBundleIIFE", { // GENERATED @@ -1250,14 +1445,14 @@ describe("bundler", () => { `, }, format: "iife", - mode: "convertformat", + treeShaking: true, + mode: "transform", }); itBundled("dce/TreeShakingInESMWrapper", { - // GENERATED files: { "/entry.js": /* js */ ` import {keep1} from './lib' - console.log(keep1(), require('./cjs')) + console.log(JSON.stringify([keep1(), require('./cjs')])) `, "/cjs.js": /* js */ ` import {keep2} from './lib' @@ -1270,6 +1465,11 @@ describe("bundler", () => { `, }, format: "esm", + dce: true, + dceKeepMarkerCount: false, + run: { + stdout: '["keep1",{"default":"keep2"}]', + }, }); itBundled("dce/DCETypeOf", { // GENERATED @@ -1286,7 +1486,7 @@ describe("bundler", () => { function* g_REMOVE() {} async function a_REMOVE() {} - // These technically have side effects due to TDZ, but this is not currently handled + // TODO: These technically have side effects due to TDZ, but this is not currently handled typeof c_remove typeof l_remove typeof s_remove @@ -1296,31 +1496,31 @@ describe("bundler", () => { `, }, format: "esm", + dce: true, }); itBundled("dce/DCETypeOfEqualsString", { - // GENERATED files: { "/entry.js": /* js */ ` - var hasBar = typeof bar !== 'undefined' + var hasBar = typeof REMOVE !== 'undefined' if (false) console.log(hasBar) `, }, format: "iife", + dce: true, }); itBundled("dce/DCETypeOfEqualsStringMangle", { - // GENERATED files: { "/entry.js": /* js */ ` // Everything here should be removed as dead code due to tree shaking - var hasBar = typeof bar !== 'undefined' - if (false) console.log(hasBar) + var REMOVE1 = typeof REMOVE2 !== 'undefined' + if (false) console.log(REMOVE1) `, }, format: "iife", minifySyntax: true, + dce: true, }); itBundled("dce/DCETypeOfEqualsStringGuardCondition", { - // GENERATED files: { "/entry.js": /* js */ ` // Everything here should be removed as dead code due to tree shaking @@ -1415,9 +1615,9 @@ describe("bundler", () => { `, }, format: "iife", + dce: true, }); itBundled("dce/DCETypeOfCompareStringGuardCondition", { - // GENERATED files: { "/entry.js": /* js */ ` // Everything here should be removed as dead code due to tree shaking @@ -1476,48 +1676,56 @@ describe("bundler", () => { `, }, format: "iife", + dce: true, }); itBundled("dce/RemoveUnusedImports", { - // GENERATED files: { "/entry.js": /* js */ ` - import a from 'a' - import * as b from 'b' - import {c} from 'c' + import REMOVE1 from 'a' + import * as REMOVE2 from 'b' + import {REMOVE3} from 'c' `, }, minifySyntax: true, - mode: "passthrough", + mode: "transform", + dce: true, + onAfterBundle(api) { + api.expectFile("/out.js").toBe( + dedent` + import "a"; + import "b"; + import "c"; + ` + "\n", + ); + }, }); itBundled("dce/RemoveUnusedImportsEval", { - // GENERATED files: { "/entry.js": /* js */ ` - import a from 'a' - import * as b from 'b' - import {c} from 'c' - eval('foo(a, b, c)') + import keep_a from 'a' + import * as keep_b from 'b' + import {keep_c} from 'c' + eval('foo(keep_a, keep_b, keep_c)') `, }, minifySyntax: true, - mode: "passthrough", + mode: "transform", + dce: true, }); itBundled("dce/RemoveUnusedImportsEvalTS", { - // GENERATED files: { "/entry.ts": /* ts */ ` - import a from 'a' - import * as b from 'b' - import {c} from 'c' + import drop_a from 'a' + import * as drop_b from 'b' + import {drop_c} from 'c' eval('foo(a, b, c)') `, }, - entryPoints: ["/entry.js"], + dce: true, minifySyntax: true, - mode: "passthrough", + mode: "transform", }); itBundled("dce/DCEClassStaticBlocks", { - // GENERATED files: { "/entry.ts": /* ts */ ` class A_REMOVE { @@ -1553,33 +1761,40 @@ describe("bundler", () => { } `, }, - entryPoints: ["/entry.js"], + dce: true, }); itBundled("dce/DCEVarExports", { - // GENERATED files: { "/a.js": /* js */ ` - var foo = { bar: 123 } + var foo = { keep: 123 } module.exports = foo `, "/b.js": /* js */ ` - var exports = { bar: 123 } + var exports = { keep: 123 } module.exports = exports `, "/c.js": /* js */ ` - var module = { bar: 123 } + var module = { keep: 123 } exports.foo = module `, }, + dce: true, entryPoints: ["/a.js", "/b.js", "/c.js"], }); itBundled("dce/DCETemplateLiteral", { - // GENERATED - files: {}, - entryPoints: ["/entry.js"], + files: { + "/entry.js": + "var remove;\n" + + "var alsoKeep;\n" + + "let a = `${keep}`\n" + + "let remove2 = `${123}`\n" + + "let c = `${keep ? 1 : 2n}`\n" + + "let remove3 = `${remove ? 1 : 2n}`\n" + + "let e = `${alsoKeep}`\n", + }, + dce: true, }); itBundled("dce/TreeShakingLoweredClassStaticField", { - // GENERATED files: { "/entry.js": /* js */ ` class REMOVE_ME { @@ -1603,9 +1818,11 @@ describe("bundler", () => { new KeepMe2() `, }, + dce: true, + dceKeepMarkerCount: 9, + unsupportedJSFeatures: ["class-field"], }); itBundled("dce/TreeShakingLoweredClassStaticFieldMinified", { - // GENERATED files: { "/entry.js": /* js */ ` class REMOVE_ME { @@ -1629,10 +1846,12 @@ describe("bundler", () => { new KeepMe2() `, }, - unsupportedJSFeatures: "ClassField", + dce: true, + dceKeepMarkerCount: 9, + unsupportedJSFeatures: ["class-field"], + minifySyntax: true, }); itBundled("dce/TreeShakingLoweredClassStaticFieldAssignment", { - // GENERATED files: { "/entry.ts": /* ts */ ` class KeepMe1 { @@ -1642,7 +1861,7 @@ describe("bundler", () => { } class KeepMe2 { static x = 'x' - static y = sideEffects() + static y = sideEffects_keep() static z = 'z' } class KeepMe3 { @@ -1653,11 +1872,11 @@ describe("bundler", () => { new KeepMe3() `, }, - entryPoints: ["/entry.js"], - unsupportedJSFeatures: "ClassField", + unsupportedJSFeatures: ["class-field"], + dce: true, + dceKeepMarkerCount: 14, }); itBundled("dce/InlineIdentityFunctionCalls", { - // GENERATED files: { "/identity.js": /* js */ ` function DROP(x) { return x } @@ -1801,9 +2020,13 @@ describe("bundler", () => { "/not-identity-rest.js", "/not-identity-return.js", ], + dce: true, + minifySyntax: true, + dceKeepMarkerCount: { + "/out/identity-first.js": 4, + }, }); itBundled("dce/InlineEmptyFunctionCalls", { - // GENERATED files: { "/empty.js": /* js */ ` function DROP() {} @@ -1914,49 +2137,93 @@ describe("bundler", () => { "/reassign-array.js", "/reassign-object.js", ], + minifySyntax: true, + dce: true, + dceKeepMarkerCount: { + "/out/empty-first.js": 4, + }, }); itBundled("dce/InlineFunctionCallBehaviorChanges", { - // GENERATED files: { - "/entry.js": ` - function empty() {} - function id(x) { return x } - - export let shouldBeWrapped = [ - id(foo.bar)(), - id(foo[bar])(), - id(foo?.bar)(), - id(foo?.[bar])(), - - (empty(), foo.bar)(), - (empty(), foo[bar])(), - (empty(), foo?.bar)(), - (empty(), foo?.[bar])(), - - id(eval)(), - id(eval)?.(), - (empty(), eval)(), - (empty(), eval)?.(), - - id(foo.bar)\` + "\`\`" + - `, - }, - mode: "passthrough", + // At the time of writing, using a template string here triggered a bug in bun's transpiler + // making it impossible to run the test. + "/entry.js": + "function empty_REMOVE() { }\n" + + "function id_REMOVE(x) { return x }\n" + + "\n" + + "export let shouldBeWrapped = [\n" + + " id_REMOVE(foo.bar)(),\n" + + " id_REMOVE(foo[bar])(),\n" + + " id_REMOVE(foo?.bar)(),\n" + + " id_REMOVE(foo?.[bar])(),\n" + + "\n" + + " (empty_REMOVE(), foo.bar)(),\n" + + " (empty_REMOVE(), foo[bar])(),\n" + + " (empty_REMOVE(), foo?.bar)(),\n" + + " (empty_REMOVE(), foo?.[bar])(),\n" + + "\n" + + " id_REMOVE(eval)(),\n" + + " id_REMOVE(eval)?.(),\n" + + " (empty_REMOVE(), eval)(),\n" + + " (empty_REMOVE(), eval)?.(),\n" + + "\n" + + " id_REMOVE(foo.bar)``,\n" + + " id_REMOVE(foo[bar])``,\n" + + " id_REMOVE(foo?.bar)``,\n" + + " id_REMOVE(foo?.[bar])``,\n" + + "\n" + + " (empty_REMOVE(), foo.bar)``,\n" + + " (empty_REMOVE(), foo[bar])``,\n" + + " (empty_REMOVE(), foo?.bar)``,\n" + + " (empty_REMOVE(), foo?.[bar])``,\n" + + "\n" + + " delete id_REMOVE(foo),\n" + + " delete id_REMOVE(foo.bar),\n" + + " delete id_REMOVE(foo[bar]),\n" + + " delete id_REMOVE(foo?.bar),\n" + + " delete id_REMOVE(foo?.[bar]),\n" + + "\n" + + " delete (empty_REMOVE(), foo),\n" + + " delete (empty_REMOVE(), foo.bar),\n" + + " delete (empty_REMOVE(), foo[bar]),\n" + + " delete (empty_REMOVE(), foo?.bar),\n" + + " delete (empty_REMOVE(), foo?.[bar]),\n" + + "\n" + + " delete empty_REMOVE(),\n" + + "]\n" + + "\n" + + "export let shouldNotBeWrapped = [\n" + + " id_REMOVE(foo)(),\n" + + " (empty_REMOVE(), foo)(),\n" + + "\n" + + " id_REMOVE(foo)``,\n" + + " (empty_REMOVE(), foo)``,\n" + + "]\n" + + "\n" + + "export let shouldNotBeDoubleWrapped = [\n" + + " delete (empty_REMOVE(), foo(), foo()),\n" + + " delete id_REMOVE((foo(), bar())),\n" + + "]", + }, + mode: "transform", + minifySyntax: true, + treeShaking: true, + dce: true, }); itBundled("dce/InlineFunctionCallForInitDecl", { - // GENERATED files: { "/entry.js": /* js */ ` - function empty() {} - function id(x) { return x } + function empty_REMOVE() {} + function id_REMOVE(x) { return x } - for (var y = empty(); false; ) ; - for (var z = id(123); false; ) ; + for (var y = empty_REMOVE(); false; ) ; + for (var z = id_REMOVE(123); false; ) ; `, }, + minifySyntax: true, + dce: true, }); itBundled("dce/ConstValueInliningNoBundle", { - // GENERATED files: { "/top-level.js": /* js */ ` // These should be kept because they are top-level and tree shaking is not enabled @@ -2105,10 +2372,15 @@ describe("bundler", () => { "/backwards-reference-top-level.js", "/backwards-reference-nested-function.js", ], - mode: "passthrough", + mode: "transform", + minifySyntax: true, + dce: true, + dceKeepMarkerCount: { + "/out/top-level.js": 7, + "/out/namespace-export.js": 1, + }, }); itBundled("dce/ConstValueInliningBundle", { - // GENERATED files: { "/exported-entry.js": /* js */ ` const x_REMOVE = 1 @@ -2226,9 +2498,15 @@ describe("bundler", () => { ], format: "esm", minifySyntax: true, + dce: true, + dceKeepMarkerCount: { + "/out/re-exported-entry.js": 2, + "/out/re-exported-2-entry.js": 2, + "/out/re-exported-star-entry.js": 4, + "/out/re-exported-star-entry.js.": 4, + }, }); itBundled("dce/ConstValueInliningAssign", { - // GENERATED files: { "/const-assign.js": /* js */ ` const x = 1 @@ -2240,46 +2518,43 @@ describe("bundler", () => { `, }, entryPoints: ["/const-assign.js", "/const-update.js"], - mode: "passthrough", - /* TODO FIX expectedScanLog: `const-assign.js: ERROR: Cannot assign to "x" because it is a constant - const-assign.js: NOTE: The symbol "x" was declared a constant here: - const-update.js: ERROR: Cannot assign to "x" because it is a constant - const-update.js: NOTE: The symbol "x" was declared a constant here: - `, */ + bundleErrors: { + "/const-assign.js": ['Cannot assign to constant variable "x"'], + "/const-update.js": ['Cannot assign to constant variable "x"'], + }, }); itBundled("dce/ConstValueInliningDirectEval", { - // GENERATED files: { "/top-level-no-eval.js": /* js */ ` - const x = 1 - console.log(x, evil('x')) + const keep = 1 + console.log(keep, evil('x')) // inline the 1 here `, "/top-level-eval.js": /* js */ ` - const x = 1 - console.log(x, eval('x')) + const keep = 1 + console.log(keep, eval('x')) // inline the 1 but keep the const def `, "/nested-no-eval.js": /* js */ ` (() => { - const x = 1 - console.log(x, evil('x')) + const remove = 1 + console.log(remove, evil('x')) // inline the 1 here and remove the const def })() `, "/nested-eval.js": /* js */ ` (() => { - const x = 1 - console.log(x, eval('x')) + const keep = 1 + console.log(keep, eval('x')) // inline the 1 but keep the const def })() `, "/ts-namespace-no-eval.ts": /* ts */ ` namespace y { - export const x = 1 - console.log(x, evil('x')) + export const keep = 1 + console.log(keep, evil('x')) // inline the 1 here } `, "/ts-namespace-eval.ts": /* ts */ ` namespace z { - export const x = 1 - console.log(x, eval('x')) + export const keep = 1 + console.log(keep, eval('x')) } `, }, @@ -2291,19 +2566,28 @@ describe("bundler", () => { "/ts-namespace-no-eval.ts", "/ts-namespace-eval.ts", ], - mode: "passthrough", + mode: "transform", + minifySyntax: true, + dce: true, + dceKeepMarkerCount: { + "/out/top-level-no-eval.js": 1, + "/out/top-level-eval.js": 1, + "/out/nested-eval.js": 1, + "/out/ts-namespace-no-eval.js": 1, + "/out/ts-namespace-eval.js": 1, + }, }); itBundled("dce/CrossModuleConstantFolding", { // GENERATED files: { "/enum-constants.ts": /* ts */ ` - export enum x { + export enum remove { a = 3, b = 6, } `, "/enum-entry.ts": /* ts */ ` - import { x } from './enum-constants' + import { remove as x } from './enum-constants' console.log([ +x.b, -x.b, @@ -2386,14 +2670,14 @@ describe("bundler", () => { export const a = 2 export const b = 4 export const c = 8 - export enum x { + export enum remove { a = 16, b = 32, c = 64, } `, "/nested-entry.ts": /* ts */ ` - import { a, b, c, x } from './nested-constants' + import { a, b, c, remove as x } from './nested-constants' console.log({ 'should be 4': ~(~a & ~b) & (b | c), 'should be 32': ~(~x.a & ~x.b) & (x.b | x.c), @@ -2401,9 +2685,9 @@ describe("bundler", () => { `, }, entryPoints: ["/enum-entry.ts", "/const-entry.js", "/nested-entry.ts"], + dce: true, }); itBundled("dce/MultipleDeclarationTreeShaking", { - // GENERATED files: { "/var2.js": /* js */ ` var x = 1 @@ -2431,9 +2715,17 @@ describe("bundler", () => { `, }, entryPoints: ["/var2.js", "/var3.js", "/function2.js", "/function3.js"], + dce: true, + treeShaking: true, + minifySyntax: false, + run: [ + { file: "/out/var2.js", stdout: "1" }, + { file: "/out/var3.js", stdout: "1\n2" }, + { file: "/out/function2.js", stdout: "2" }, + { file: "/out/function3.js", stdout: "3\n3" }, + ], }); itBundled("dce/MultipleDeclarationTreeShakingMinifySyntax", { - // GENERATED files: { "/var2.js": /* js */ ` var x = 1 @@ -2448,79 +2740,118 @@ describe("bundler", () => { var x = 3 `, "/function2.js": /* js */ ` - function x() { return 1 } + function x() { return "REMOVE" } console.log(x()) function x() { return 2 } `, "/function3.js": /* js */ ` - function x() { return 1 } + function x() { return "REMOVE" } console.log(x()) - function x() { return 2 } + function x() { return "REMOVE" } console.log(x()) function x() { return 3 } `, }, entryPoints: ["/var2.js", "/var3.js", "/function2.js", "/function3.js"], + dce: true, + treeShaking: true, + minifySyntax: true, + run: [ + { file: "/out/var2.js", stdout: "1" }, + { file: "/out/var3.js", stdout: "1\n2" }, + { file: "/out/function2.js", stdout: "2" }, + { file: "/out/function3.js", stdout: "3\n3" }, + ], }); itBundled("dce/PureCallsWithSpread", { - // GENERATED files: { + // this changes to "[...args]" "/entry.js": /* js */ ` - /* @__PURE__ */ foo(...args); - /* @__PURE__ */ new foo(...args); + /* @__PURE__ */ REMOVE(...args); + /* @__PURE__ */ new REMOVE(...args); `, }, + minifySyntax: true, + dce: true, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + assert([...code.matchAll(/\[\.\.\.args\]/g)].length === 2, "spread should be preserved"); + }, }); itBundled("dce/TopLevelFunctionInliningWithSpread", { - // GENERATED files: { "/entry.js": /* js */ ` - function empty1() {} - function empty2() {} - function empty3() {} + function empty1_remove() {} + function empty2_remove() {} + function empty3_remove() {} function identity1(x) { return x } - function identity2(x) { return x } + function identity2_remove(x) { return x } function identity3(x) { return x } - empty1() - empty2(args) - empty3(...args) + empty1_remove() + empty2_remove(args) + empty3_remove(...args) identity1() - identity2(args) + identity2_remove(args) identity3(...args) `, "/inner.js": /* js */ ` - export function empty1() {} - export function empty2() {} - export function empty3() {} + export function empty1_remove() {} + export function empty2_remove() {} + export function empty3_remove() {} export function identity1(x) { return x } - export function identity2(x) { return x } + export function identity2_remove(x) { return x } export function identity3(x) { return x } `, "/entry-outer.js": /* js */ ` import { - empty1, - empty2, - empty3, + empty1_remove, + empty2_remove, + empty3_remove, identity1, - identity2, + identity2_remove, identity3, } from './inner.js' - empty1() - empty2(args) - empty3(...args) + empty1_remove() + empty2_remove(args) + empty3_remove(...args) identity1() - identity2(args) + identity2_remove(args) identity3(...args) `, }, + dce: true, entryPoints: ["/entry.js", "/entry-outer.js"], + minifySyntax: true, + + runtimeFiles: { + "/test.js": /* js */ ` + globalThis.args = { + [Symbol.iterator]() { + console.log('spread') + return { + next() { + return { done: true, value: undefined } + } + } + } + }; + + await import('./out/entry.js'); + console.log('---') + await import('./out/entry-outer.js'); + `, + }, + run: { + file: "/test.js", + stdout: "spread\nspread\n---\nspread\nspread", + }, }); itBundled("dce/NestedFunctionInliningWithSpread", { // GENERATED @@ -2577,8 +2908,8 @@ describe("bundler", () => { }, entryPoints: ["/entry.js", "/entry-outer.js"], }); + // im confused what this is testing. cross platform slash? there is none?? not even in the go source itBundled("dce/PackageJsonSideEffectsFalseCrossPlatformSlash", { - // GENERATED files: { "/Users/user/project/src/entry.js": /* js */ ` import "demo-pkg/foo" @@ -2595,155 +2926,159 @@ describe("bundler", () => { } `, }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSS", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg/button' - import { Menu } from 'pkg/menu' - render(<Button/>) - `, - "/project/node_modules/pkg/button.js": /* js */ ` - import './button.css' - export let Button - `, - "/project/node_modules/pkg/button.css": `button { color: red }`, - "/project/node_modules/pkg/menu.js": /* js */ ` - import './menu.css' - export let Menu - `, - "/project/node_modules/pkg/menu.css": `menu { color: red }`, - }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSSReExportSideEffectsFalse", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg' - render(<Button/>) - `, - "/project/node_modules/pkg/entry.js": `export { Button } from './components'`, - "/project/node_modules/pkg/package.json": /* json */ ` - { - "main": "./entry.js", - "sideEffects": false - } - `, - "/project/node_modules/pkg/components.jsx": /* jsx */ ` - require('./button.css') - export const Button = () => <button/> - `, - "/project/node_modules/pkg/button.css": `button { color: red }`, - }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSSReExportSideEffectsFalseOnlyJS", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg' - render(<Button/>) - `, - "/project/node_modules/pkg/entry.js": `export { Button } from './components'`, - "/project/node_modules/pkg/package.json": /* json */ ` - { - "main": "./entry.js", - "sideEffects": ["*.css"] - } - `, - "/project/node_modules/pkg/components.jsx": /* jsx */ ` - require('./button.css') - export const Button = () => <button/> - `, - "/project/node_modules/pkg/button.css": `button { color: red }`, - }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSSExportStarSideEffectsFalse", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg' - render(<Button/>) - `, - "/project/node_modules/pkg/entry.js": `export * from './components'`, - "/project/node_modules/pkg/package.json": /* json */ ` - { - "main": "./entry.js", - "sideEffects": false - } - `, - "/project/node_modules/pkg/components.jsx": /* jsx */ ` - require('./button.css') - export const Button = () => <button/> - `, - "/project/node_modules/pkg/button.css": `button { color: red }`, - }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSSExportStarSideEffectsFalseOnlyJS", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg' - render(<Button/>) - `, - "/project/node_modules/pkg/entry.js": `export * from './components'`, - "/project/node_modules/pkg/package.json": /* json */ ` - { - "main": "./entry.js", - "sideEffects": ["*.css"] - } - `, - "/project/node_modules/pkg/components.jsx": /* jsx */ ` - require('./button.css') - export const Button = () => <button/> - `, - "/project/node_modules/pkg/button.css": `button { color: red }`, - }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSSUnusedNestedImportSideEffectsFalse", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg/button' - render(<Button/>) - `, - "/project/node_modules/pkg/package.json": /* json */ ` - { - "sideEffects": false - } - `, - "/project/node_modules/pkg/button.jsx": /* jsx */ ` - import styles from './styles' - export const Button = () => <button/> - `, - "/project/node_modules/pkg/styles.js": /* js */ ` - import './styles.css' - export default {} - `, - "/project/node_modules/pkg/styles.css": `button { color: red }`, - }, - }); - itBundled("dce/TreeShakingJSWithAssociatedCSSUnusedNestedImportSideEffectsFalseOnlyJS", { - // GENERATED - files: { - "/project/test.jsx": /* jsx */ ` - import { Button } from 'pkg/button' - render(<Button/>) - `, - "/project/node_modules/pkg/package.json": /* json */ ` - { - "sideEffects": ["*.css"] - } - `, - "/project/node_modules/pkg/button.jsx": /* jsx */ ` - import styles from './styles' - export const Button = () => <button/> - `, - "/project/node_modules/pkg/styles.js": /* js */ ` - import './styles.css' - export default {} - `, - "/project/node_modules/pkg/styles.css": `button { color: red }`, - }, - }); + run: { + stdout: "foo\nbar", + }, + }); + // itBundled("dce/TreeShakingJSWithAssociatedCSS", { + // // TODO: css assertions. this should contain both button and menu + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg/button' + // import { Menu } from 'pkg/menu' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/button.js": /* js */ ` + // import './button.css' + // export let Button + // `, + // "/project/node_modules/pkg/button.css": `button { color: red }`, + // "/project/node_modules/pkg/menu.js": /* js */ ` + // import './menu.css' + // export let Menu + // `, + // "/project/node_modules/pkg/menu.css": `menu { color: green }`, + // }, + // external: ["react"], + // }); + // itBundled("dce/TreeShakingJSWithAssociatedCSSReExportSideEffectsFalse", { + // // GENERATED + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/entry.js": `export { Button } from './components'`, + // "/project/node_modules/pkg/package.json": /* json */ ` + // { + // "main": "./entry.js", + // "sideEffects": false + // } + // `, + // "/project/node_modules/pkg/components.jsx": /* jsx */ ` + // require('./button.css') + // export const Button = () => <button/> + // `, + // "/project/node_modules/pkg/button.css": `button { color: red }`, + // }, + // }); + // itBundled("dce/TreeShakingJSWithAssociatedCSSReExportSideEffectsFalseOnlyJS", { + // // GENERATED + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/entry.js": `export { Button } from './components'`, + // "/project/node_modules/pkg/package.json": /* json */ ` + // { + // "main": "./entry.js", + // "sideEffects": ["*.css"] + // } + // `, + // "/project/node_modules/pkg/components.jsx": /* jsx */ ` + // require('./button.css') + // export const Button = () => <button/> + // `, + // "/project/node_modules/pkg/button.css": `button { color: red }`, + // }, + // }); + // itBundled("dce/TreeShakingJSWithAssociatedCSSExportStarSideEffectsFalse", { + // // GENERATED + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/entry.js": `export * from './components'`, + // "/project/node_modules/pkg/package.json": /* json */ ` + // { + // "main": "./entry.js", + // "sideEffects": false + // } + // `, + // "/project/node_modules/pkg/components.jsx": /* jsx */ ` + // require('./button.css') + // export const Button = () => <button/> + // `, + // "/project/node_modules/pkg/button.css": `button { color: red }`, + // }, + // }); + // itBundled("dce/TreeShakingJSWithAssociatedCSSExportStarSideEffectsFalseOnlyJS", { + // // GENERATED + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/entry.js": `export * from './components'`, + // "/project/node_modules/pkg/package.json": /* json */ ` + // { + // "main": "./entry.js", + // "sideEffects": ["*.css"] + // } + // `, + // "/project/node_modules/pkg/components.jsx": /* jsx */ ` + // require('./button.css') + // export const Button = () => <button/> + // `, + // "/project/node_modules/pkg/button.css": `button { color: red }`, + // }, + // }); + // itBundled("dce/TreeShakingJSWithAssociatedCSSUnusedNestedImportSideEffectsFalse", { + // // GENERATED + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg/button' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/package.json": /* json */ ` + // { + // "sideEffects": false + // } + // `, + // "/project/node_modules/pkg/button.jsx": /* jsx */ ` + // import styles from './styles' + // export const Button = () => <button/> + // `, + // "/project/node_modules/pkg/styles.js": /* js */ ` + // import './styles.css' + // export default {} + // `, + // "/project/node_modules/pkg/styles.css": `button { color: red }`, + // }, + // }); + // itBundled("dce/TreeShakingJSWithAssociatedCSSUnusedNestedImportSideEffectsFalseOnlyJS", { + // // GENERATED + // files: { + // "/project/test.jsx": /* jsx */ ` + // import { Button } from 'pkg/button' + // render(<Button/>) + // `, + // "/project/node_modules/pkg/package.json": /* json */ ` + // { + // "sideEffects": ["*.css"] + // } + // `, + // "/project/node_modules/pkg/button.jsx": /* jsx */ ` + // import styles from './styles' + // export const Button = () => <button/> + // `, + // "/project/node_modules/pkg/styles.js": /* js */ ` + // import './styles.css' + // export default {} + // `, + // "/project/node_modules/pkg/styles.css": `button { color: red }`, + // }, + // }); }); diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts index 70630fc31..4d0c78c55 100644 --- a/test/bundler/esbuild/default.test.ts +++ b/test/bundler/esbuild/default.test.ts @@ -1,6 +1,6 @@ import assert from "assert"; import dedent from "dedent"; -import { bundlerTest, expectBundled, itBundled, testForFile } from "../expectBundled"; +import { ESBUILD_PATH, bundlerTest, expectBundled, itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); // Tests ported from: @@ -3643,9 +3643,8 @@ describe("bundler", () => { "/entry.js": ['Duplicate injected file "/inject.js"'], }, }); - return; + // TODO: runtime checks for these next two. i think esbuild is doing this one wrong. itBundled("default/Inject", { - // GENERATED files: { "/entry.js": /* js */ ` let sideEffects = console.log('this should be renamed') @@ -3664,12 +3663,12 @@ describe("bundler", () => { export let obj = {} export let sideEffects = console.log('side effects') export let noSideEffects = /* @__PURE__ */ console.log('side effects') - export let injectedAndDefined = 'should not be used' - let injected_and_defined = 'should not be used' + export let injectedAndDefined = 'should not be used FAILED' + let injected_and_defined = 'should not be used FAILED' export { injected_and_defined as 'injected.and.defined' } `, "/node_modules/unused/index.js": `console.log('This is unused but still has side effects')`, - "/node_modules/sideEffects-false/index.js": `console.log('This is unused and has no side effects')`, + "/node_modules/sideEffects-false/index.js": `console.log('This is unused and has no side effects. FAILED')`, "/node_modules/sideEffects-false/package.json": /* json */ ` { "sideEffects": false @@ -3684,13 +3683,13 @@ describe("bundler", () => { } export { replace2 as 'chain2.prop2' } `, - "/collision.js": `export let collide = 123`, + "/collision.js": `export let collide = "FAILED"`, "/re-export.js": /* js */ ` export {re_export} from 'external-pkg' export {'re.export'} from 'external-pkg2' `, }, - format: "cjs", + format: "esm", inject: [ "/inject.js", "/node_modules/unused/index.js", @@ -3705,12 +3704,33 @@ describe("bundler", () => { injectedAndDefined: JSON.stringify("should be used"), "injected.and.defined": JSON.stringify("should be used"), }, + external: ["external-pkg", "external-pkg2"], + runtimeFiles: { + "/node_modules/external-pkg/index.js": `export let re_export = '1'`, + "/node_modules/external-pkg2/index.js": `export let x = '2'; export { x as 're.export' }`, + }, + dce: true, + run: { + stdout: ` + side effects + This is unused but still has side effects + this should be renamed + undefined + defined + should be used + should be used + [Function: test] + [Function: test] + 123 + 1 + 2 + `, + }, }); itBundled("default/InjectNoBundle", { - // GENERATED files: { "/entry.js": /* js */ ` - let sideEffects = console.log('side effects') + let sideEffects = console.log('this should be renamed') let collide = 123 console.log(obj.prop) console.log(obj.defined) @@ -3720,18 +3740,18 @@ describe("bundler", () => { console.log(chain2.prop2.test) console.log(collide) console.log(re_export) - console.log(reexpo.rt) + console.log(re.export) `, "/inject.js": /* js */ ` export let obj = {} - export let sideEffects = console.log('this should be renamed') + export let sideEffects = console.log('side effects') export let noSideEffects = /* @__PURE__ */ console.log('side effects') - export let injectedAndDefined = 'should not be used' - let injected_and_defined = 'should not be used' + export let injectedAndDefined = 'should not be used FAILED' + let injected_and_defined = 'should not be used FAILED' export { injected_and_defined as 'injected.and.defined' } `, "/node_modules/unused/index.js": `console.log('This is unused but still has side effects')`, - "/node_modules/sideEffects-false/index.js": `console.log('This is unused and has no side effects')`, + "/node_modules/sideEffects-false/index.js": `console.log('This is unused and has no side effects. FAILED')`, "/node_modules/sideEffects-false/package.json": /* json */ ` { "sideEffects": false @@ -3741,151 +3761,184 @@ describe("bundler", () => { export let replace = { test() {} } - let replaceDot = { + let replace2 = { test() {} } - export { replaceDot as 'chain2.prop2' } + export { replace2 as 'chain2.prop2' } `, - "/collision.js": `export let collide = 123`, + "/collision.js": `export let collide = "FAILED"`, "/re-export.js": /* js */ ` export {re_export} from 'external-pkg' - export {'reexpo.rt'} from 'external-pkg2' + export {'re.export'} from 'external-pkg2' `, }, - treeShaking: true, - mode: "passthrough", + format: "esm", + inject: [ + "/inject.js", + "/node_modules/unused/index.js", + "/node_modules/sideEffects-false/index.js", + "/replacement.js", + "/collision.js", + "/re-export.js", + ], define: { "chain.prop": "replace", - "obj.defined": '"defined"', - injectedAndDefined: '"should be used"', - "injected.and.defined": '"should be used"', - }, - }); - itBundled("default/InjectJSX", { - // GENERATED - files: { - "/entry.jsx": `console.log(<><div/></>)`, - "/inject.js": /* js */ ` - export function el() {} - export function frag() {} - `, - }, - define: { - "React.createElement": "el", - "React.Fragment": "frag", - }, - }); - itBundled("default/InjectJSXDotNames", { - // GENERATED - files: { - "/entry.jsx": `console.log(<><div/></>)`, - "/inject.js": /* js */ ` - function el() {} - function frag() {} - export { - el as 'React.createElement', - frag as 'React.Fragment', - } - `, - }, - }); - itBundled("default/InjectImportTS", { - // GENERATED - files: { - "/entry.ts": `console.log('here')`, - "/inject.js": /* js */ ` - // Unused imports are automatically removed in TypeScript files (this - // is a mis-feature of the TypeScript language). However, injected - // imports are an esbuild feature so we get to decide what the - // semantics are. We do not want injected imports to disappear unless - // they have been explicitly marked as having no side effects. - console.log('must be present') - `, - }, - format: "esm", - mode: "convertformat", - }); - itBundled("default/InjectImportOrder", { - // GENERATED - files: { - "/entry.ts": /* ts */ ` - import 'third' - console.log('third') - `, - "/inject-1.js": /* js */ ` - import 'first' - console.log('first') - `, - "/inject-2.js": /* js */ ` - import 'second' - console.log('second') - `, - }, - inject: ["/inject-1.js", "/inject-2.js"], - }); - itBundled("default/InjectAssign", { - // GENERATED - files: { - "/entry.js": /* js */ ` - test = true - foo.bar = true - defined = true - `, - "/inject.js": /* js */ ` - export let test = 0 - let fooBar = 1 - let someDefine = 2 - export { fooBar as 'foo.bar' } - export { someDefine as 'some.define' } - `, + "obj.defined": JSON.stringify("defined"), + injectedAndDefined: JSON.stringify("should be used"), + "injected.and.defined": JSON.stringify("should be used"), }, - inject: ["/inject.js"], - define: { - defined: "some.define", + runtimeFiles: { + "/node_modules/external-pkg/index.js": `export let re_export = '1'`, + "/node_modules/external-pkg2/index.js": `export let x = '2'; export { x as 're.export' }`, }, - }); - itBundled("default/InjectWithDefine", { - files: { - "/entry.js": /* js */ ` - console.log( - // define wins over inject - both === 'define', - bo.th === 'defi.ne', - // define forwards to inject - first === 'success (identifier)', - fir.st === 'success (dot name)', - ) - `, - "/inject.js": /* js */ ` - export let both = 'inject' - export let first = 'TEST FAILED!' - export let second = 'success (identifier)' - - let both2 = 'inject' - let first2 = 'TEST FAILED!' - let second2 = 'success (dot name)' - export { - both2 as 'bo.th', - first2 as 'fir.st', - second2 as 'seco.nd', - } + dce: true, + treeShaking: true, + mode: "transform", + run: { + stdout: ` + side effects + This is unused but still has side effects + this should be renamed + undefined + defined + should be used + should be used + [Function: test] + [Function: test] + 123 + 1 + 2 `, }, - inject: ["/inject.js"], - define: { - "both": '"define"', - "bo.th": '"defi.ne"', - "first": "second", - "fir.st": "seco.nd", - }, }); + // itBundled("default/InjectJSX", { + // files: { + // "/entry.jsx": `console.log(<><div/></>)`, + // "/inject.js": /* js */ ` + // export function el() {} + // export function frag() {} + // `, + // }, + // define: { + // "React.createElement": "el", + // "React.Fragment": "frag", + // }, + // inject: ["/inject.js"], + // }); + // itBundled("default/InjectJSXDotNames", { + // // GENERATED + // files: { + // "/entry.jsx": `console.log(<><div/></>)`, + // "/inject.js": /* js */ ` + // function el() {} + // function frag() {} + // export { + // el as 'React.createElement', + // frag as 'React.Fragment', + // } + // `, + // }, + // }); + // itBundled("default/InjectImportTS", { + // // GENERATED + // files: { + // "/entry.ts": `console.log('here')`, + // "/inject.js": /* js */ ` + // // Unused imports are automatically removed in TypeScript files (this + // // is a mis-feature of the TypeScript language). However, injected + // // imports are an esbuild feature so we get to decide what the + // // semantics are. We do not want injected imports to disappear unless + // // they have been explicitly marked as having no side effects. + // console.log('must be present') + // `, + // }, + // format: "esm", + // }); + // itBundled("default/InjectImportOrder", { + // // GENERATED + // files: { + // "/entry.ts": /* ts */ ` + // import 'third' + // console.log('third') + // `, + // "/inject-1.js": /* js */ ` + // import 'first' + // console.log('first') + // `, + // "/inject-2.js": /* js */ ` + // import 'second' + // console.log('second') + // `, + // }, + // inject: ["/inject-1.js", "/inject-2.js"], + // }); + // itBundled("default/InjectAssign", { + // // GENERATED + // files: { + // "/entry.js": /* js */ ` + // test = true + // foo.bar = true + // defined = true + // `, + // "/inject.js": /* js */ ` + // export let test = 0 + // let fooBar = 1 + // let someDefine = 2 + // export { fooBar as 'foo.bar' } + // export { someDefine as 'some.define' } + // `, + // }, + // inject: ["/inject.js"], + // define: { + // defined: "some.define", + // }, + // }); + // itBundled("default/InjectWithDefine", { + // files: { + // "/entry.js": /* js */ ` + // console.log( + // // define wins over inject + // both === 'define', + // bo.th === 'defi.ne', + // // define forwards to inject + // first === 'success (identifier)', + // fir.st === 'success (dot name)', + // ) + // `, + // "/inject.js": /* js */ ` + // export let both = 'inject' + // export let first = 'TEST FAILED!' + // export let second = 'success (identifier)' + + // let both2 = 'inject' + // let first2 = 'TEST FAILED!' + // let second2 = 'success (dot name)' + // export { + // both2 as 'bo.th', + // first2 as 'fir.st', + // second2 as 'seco.nd', + // } + // `, + // }, + // inject: ["/inject.js"], + // define: { + // "both": '"define"', + // "bo.th": '"defi.ne"', + // "first": "second", + // "fir.st": "seco.nd", + // }, + // }); itBundled("default/Outbase", { - // GENERATED files: { "/a/b/c.js": `console.log('c')`, "/a/b/d.js": `console.log('d')`, }, entryPoints: ["/a/b/c.js", "/a/b/d.js"], + outbase: "/", + onAfterBundle(api) { + api.assertFileExists("/out/a/b/c.js"); + api.assertFileExists("/out/a/b/d.js"); + }, }); itBundled("default/AvoidTDZ", { // GENERATED @@ -3895,14 +3948,23 @@ describe("bundler", () => { static foo = new Foo } let foo = Foo.foo - console.log(foo) + console.log(JSON.stringify(foo)) export class Bar {} export let bar = 123 `, }, + runtimeFiles: { + "/test.js": /* js */ ` + import * as mod from './out'; + console.log(JSON.stringify(mod)); + `, + }, + run: { + file: "/test.js", + stdout: '{}\n{"bar":123}', + }, }); itBundled("default/AvoidTDZNoBundle", { - // GENERATED files: { "/entry.js": /* js */ ` class Foo { @@ -3914,7 +3976,17 @@ describe("bundler", () => { export let bar = 123 `, }, - mode: "passthrough", + mode: "transform", + runtimeFiles: { + "/test.js": /* js */ ` + import * as mod from './out'; + console.log(JSON.stringify(mod)); + `, + }, + run: { + file: "/test.js", + stdout: '{}\n{"bar":123}', + }, }); itBundled("default/DefineImportMeta", { files: { @@ -3943,7 +4015,6 @@ describe("bundler", () => { }, }); itBundled("default/DefineImportMetaES5", { - // GENERATED files: { "/replaced.js": `console.log(import.meta.x)`, "/kept.js": `console.log(import.meta.y)`, @@ -3953,41 +4024,45 @@ describe("bundler", () => { define: { "import.meta.x": 1, }, - /* TODO FIX expectedScanLog: `dead-code.js: WARNING: "import.meta" is not available in the configured target environment and will be empty - kept.js: WARNING: "import.meta" is not available in the configured target environment and will be empty - `, */ - }); - itBundled("default/InjectImportMeta", { - // GENERATED - files: { - "/entry.js": /* js */ ` - console.log( - // These should be fully substituted - import.meta, - import.meta.foo, - import.meta.foo.bar, - - // Should just substitute "import.meta.foo" - import.meta.foo.baz, - - // This should not be substituted - import.meta.bar, - ) - `, - "/inject.js": /* js */ ` - let foo = 1 - let bar = 2 - let baz = 3 - export { - foo as 'import.meta', - bar as 'import.meta.foo', - baz as 'import.meta.foo.bar', - } - `, + unsupportedJSFeatures: ["import-meta"], + run: [ + { file: "/out/replaced.js", stdout: "1" }, + { file: "/out/kept.js", stdout: "undefined" }, + ], + onAfterBundle(api) { + api.expectFile("/out/dead-code.js").toBe(""); }, }); + // itBundled("default/InjectImportMeta", { + // // GENERATED + // files: { + // "/entry.js": /* js */ ` + // console.log( + // // These should be fully substituted + // import.meta, + // import.meta.foo, + // import.meta.foo.bar, + + // // Should just substitute "import.meta.foo" + // import.meta.foo.baz, + + // // This should not be substituted + // import.meta.bar, + // ) + // `, + // "/inject.js": /* js */ ` + // let foo = 1 + // let bar = 2 + // let baz = 3 + // export { + // foo as 'import.meta', + // bar as 'import.meta.foo', + // baz as 'import.meta.foo.bar', + // } + // `, + // }, + // }); itBundled("default/DefineThis", { - // GENERATED files: { "/entry.js": /* js */ ` ok( @@ -4015,7 +4090,7 @@ describe("bundler", () => { })(); // Nothing should be substituted in this code - (function() { + export default function() { doNotSubstitute( this, this.foo, @@ -4023,20 +4098,52 @@ describe("bundler", () => { this.foo.baz, this.bar, ); - })(); + }; `, }, define: { - this: 1, - "this.foo": 2, - "this.foo.bar": 3, + this: "_replaced", + "this.foo": "_replaced_foo", + "this.foo.bar": "_replaced_foo_bar", + }, + onAfterBundle(api) { + const split = api.readFile("/out.js").split("doNotSubstitute"); + expect(split.length).toBe(2); + assert(!split[0].includes("this"), "this should not be substituted in the first two cases"); + assert([...split[1].matchAll(/this/g)].length === 5, "there should be 5 mentions of this in the third case"); + }, + runtimeFiles: { + "/test.js": /* js */ ` + globalThis.ok = (...args) => console.log(JSON.stringify(args)); + globalThis.doNotSubstitute = (...args) => console.log(JSON.stringify(args)); + globalThis._replaced = { foo: 1 }; + globalThis._replaced_foo = { baz: 2 }; + globalThis._replaced_foo_bar = 3; + const { default: fn } = await import('./out.js'); + + fn.call({ + foo: { + bar: 4, + baz: 5, + }, + bar: 6, + }); + `, + }, + run: { + file: "/test.js", + stdout: ` + [{"foo":1},{"baz":2},3,2,null] + [{"foo":1},{"baz":2},3,2,null] + [{"foo":{"bar":4,"baz":5},"bar":6},{"bar":4,"baz":5},4,5,6] + `, }, }); itBundled("default/DefineOptionalChain", { // GENERATED files: { "/entry.js": /* js */ ` - console.log([ + log([ a.b.c, a?.b.c, a.b?.c, @@ -4054,12 +4161,24 @@ describe("bundler", () => { define: { "a.b.c": 1, }, + runtimeFiles: { + "/test.js": /* js */ ` + globalThis.log = (...args) => console.log(JSON.stringify(args)); + globalThis.a = { B: { C: 2 } }; + globalThis.b = "B"; + globalThis.c = "C"; + await import('./out.js'); + `, + }, + run: { + file: "/test.js", + stdout: `[[1,1,1],[1,1,1],[2,2,2]]`, + }, }); itBundled("default/DefineOptionalChainLowered", { - // GENERATED files: { "/entry.js": /* js */ ` - console.log([ + log([ a.b.c, a?.b.c, a.b?.c, @@ -4071,15 +4190,36 @@ describe("bundler", () => { a[b][c], a?.[b][c], a[b]?.[c], + a?.[d][c], + a[d]?.[e], ]) `, }, + unsupportedJSFeatures: ["optional-chain"], define: { "a.b.c": 1, }, + runtimeFiles: { + "/test.js": /* js */ ` + globalThis.log = (...args) => console.log(JSON.stringify(args)); + globalThis.a = { B: { C: 2 }, D: { } }; + globalThis.b = "B"; + globalThis.c = "C"; + globalThis.d = "D"; + globalThis.e = "E"; + await import('./out.js'); + `, + }, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + assert(!code.includes("?."), "code should not contain optional chaining"); + }, + run: { + file: "/test.js", + stdout: `[[1,1,1],[1,1,1],[2,2,2,null,null]]`, + }, }); itBundled("default/DefineInfiniteLoopIssue2407", { - // GENERATED files: { "/entry.js": /* js */ ` a.b() @@ -4092,88 +4232,143 @@ describe("bundler", () => { "c.a": "a.b", "x.y": "y", }, - }); - itBundled("default/DefineAssignWarning", { - // GENERATED - files: { - "/read.js": /* js */ ` - console.log( - [a, b.c, b['c']], - [d, e.f, e['f']], - [g, h.i, h['i']], - ) - `, - "/write.js": /* js */ ` - console.log( - [a = 0, b.c = 0, b['c'] = 0], - [d = 0, e.f = 0, e['f'] = 0], - [g = 0, h.i = 0, h['i'] = 0], - ) + runtimeFiles: { + "/test.js": /* js */ ` + globalThis.b = { c: () => console.log('1') }; + globalThis.y = () => console.log('2'); + await import('./out.js'); `, }, - entryPoints: ["/read.js", "/write.js"], - define: { - a: "null", - "b.c": "null", - d: "ident", - "e.f": "ident", - g: "dot.chain", - "h.i": "dot.chain", + run: { + file: "/test.js", + stdout: "1\n2", }, }); + // TODO: this doesnt warn in esbuild ??? + // itBundled("default/DefineAssignWarning", { + // // GENERATED + // files: { + // "/read.js": /* js */ ` + // console.log( + // [a, b.c, b['c']], + // [d, e.f, e['f']], + // [g, h.i, h['i']], + // ) + // `, + // "/write.js": /* js */ ` + // console.log( + // [a = 0, b.c = 0, b['c'] = 0], + // [d = 0, e.f = 0, e['f'] = 0], + // [g = 0, h.i = 0, h['i'] = 0], + // ) + // `, + // }, + // entryPoints: ["/read.js", "/write.js"], + // define: { + // a: "null", + // "b.c": "null", + // d: "ident", + // "e.f": "ident", + // g: "dot.chain", + // "h.i": "dot.chain", + // }, + // }); itBundled("default/KeepNamesTreeShaking", { - // GENERATED files: { "/entry.js": /* js */ ` - function fnStmtRemove() {} - function fnStmtKeep() {} - x = fnStmtKeep - - let fnExprRemove = function remove() {} - let fnExprKeep = function keep() {} - x = fnExprKeep - - class clsStmtRemove {} - class clsStmtKeep {} - new clsStmtKeep() - - let clsExprRemove = class remove {} - let clsExprKeep = class keep {} - new clsExprKeep() + (function() { + function fnStmtRemove() {} + function fnStmtKeep() {} + x = fnStmtKeep + + let fnExprRemove = function remove() {} + let fnExprKeep = function keepFn() {} + x = fnExprKeep + + class clsStmtRemove {} + class clsStmtKeep {} + new clsStmtKeep() + + let clsExprRemove = class remove {} + let clsExprKeep = class keepClass {} + new clsExprKeep() + })(); `, }, keepNames: true, + dce: true, + onAfterBundle(api) { + // to properly check that keep names actually worked, we need to minify the + // file and THEN check for the names. we do this separatly just so that we know that + // the bundler's minifier doesn't mess anything up. + Bun.spawnSync([ESBUILD_PATH, "--minify-identifiers", "--outfile=out.min.js", "out.js"], { cwd: api.root }); + const code = api.readFile("/out.min.js"); + const checks = ["fnStmtKeep", "keepFn", "clsStmtKeep", "keepClass"]; + for (const check of checks) { + assert(code.includes(check), `code should contain ${check} past minifying`); + } + }, }); itBundled("default/KeepNamesClassStaticName", { - // GENERATED files: { "/entry.js": /* js */ ` - class A { static foo } - class B { static name } - class C { static name() {} } - class D { static get name() {} } - class E { static set name(x) {} } - class F { static ['name'] = 0 } + class ClassName1A { static foo = 1 } + class ClassName1B { static name = 2 } + class ClassName1C { static name() {} } + class ClassName1D { static get name() {} } + class ClassName1E { static set name(x) {} } + class ClassName1F { static ['name'] = 0 } - let a = class a { static foo } - let b = class b { static name } - let c = class c { static name() {} } - let d = class d { static get name() {} } - let e = class e { static set name(x) {} } - let f = class f { static ['name'] = 0 } + let a = class ClassName2a { static foo } + let b = class ClassName2b { static name } + let c = class ClassName2c { static name() {} } + let d = class ClassName2d { static get name() {} } + let e = class ClassName2e { static set name(x) {} } + let f = class ClassName2f { static ['name'] = 0 } - let a2 = class { static foo } - let b2 = class { static name } - let c2 = class { static name() {} } - let d2 = class { static get name() {} } - let e2 = class { static set name(x) {} } - let f2 = class { static ['name'] = 0 } + let ClassName_a2 = class { static foo } + let ClassName_b2 = class { static name } + let ClassName_c2 = class { static name() {} } + let ClassName_d2 = class { static get name() {} } + let ClassName_e2 = class { static set name(x) {} } + let ClassName_f2 = class { static ['name'] = 0 } + + export { ClassName1A, ClassName1B, ClassName1C, ClassName1D, ClassName1E, ClassName1F, a, b, c, d, e, f, ClassName_a2 as a2, ClassName_b2 as b2,ClassName_c2 as c2,ClassName_d2 as d2,ClassName_e2 as e2,ClassName_f2 as f2 } `, }, - mode: "passthrough", + keepNames: true, + onAfterBundle(api) { + // to properly check that keep names actually worked, we need to minify the + // file and THEN check for the names. we do this separatly just so that we know that + // the bundler's minifier doesn't mess anything up. + Bun.spawnSync([ESBUILD_PATH, "--minify-identifiers", "--outfile=out.min.js", "out.js"], { cwd: api.root }); + const code = api.readFile("/out.min.js"); + const checks = [ + "ClassName1A", + "ClassName1B", + "ClassName1C", + "ClassName1D", + "ClassName1E", + "ClassName1F", + "ClassName2a", + "ClassName2b", + "ClassName2c", + "ClassName2d", + "ClassName2e", + "ClassName2f", + "ClassName_a2", + "ClassName_b2", + "ClassName_c2", + "ClassName_d2", + "ClassName_e2", + "ClassName_f2", + ]; + for (const check of checks) { + assert(code.includes(check), `code should contain ${check} past minifying`); + } + }, }); itBundled("default/CharFreqIgnoreComments", { - // GENERATED files: { "/a.js": /* js */ ` export default function(one, two, three, four) { @@ -4193,32 +4388,39 @@ describe("bundler", () => { // LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL `, }, + minifyIdentifiers: true, entryPoints: ["/a.js", "/b.js"], + onAfterBundle(api) { + function capture(str: string) { + return str.match(/function.*?\(\s*(\w+),\s*(\w+),\s*(\w+),\s*(\w+)\)/)!.slice(1); + } + const a = capture(api.readFile("/out/a.js")); + const b = capture(api.readFile("/out/b.js")); + expect(a).toEqual(b); + expect(a).not.toEqual(["one", "two", "three", "four"]); + }, }); itBundled("default/ImportRelativeAsPackage", { - // GENERATED files: { "/Users/user/project/src/entry.js": `import 'some/other/file'`, "/Users/user/project/src/some/other/file.js": ``, }, - /* TODO FIX expectedScanLog: `Users/user/project/src/entry.js: ERROR: Could not resolve "some/other/file" - NOTE: Use the relative path "./some/other/file" to reference the file "Users/user/project/src/some/other/file.js". Without the leading "./", the path "some/other/file" is being interpreted as a package path instead. - `, */ + bundleErrors: { + "/Users/user/project/src/entry.js": [`Could not resolve: "some/other/file". Maybe you need to "bun install"?`], + }, }); itBundled("default/ForbidConstAssignWhenBundling", { - // GENERATED files: { "/entry.js": /* js */ ` const x = 1 x = 2 `, }, - /* TODO FIX expectedScanLog: `entry.js: ERROR: Cannot assign to "x" because it is a constant - entry.js: NOTE: The symbol "x" was declared a constant here: - `, */ + bundleErrors: { + "/entry.js": [`Cannot assign to "x" because it is a constant`], + }, }); itBundled("default/ConstWithLet", { - // GENERATED files: { "/entry.js": /* js */ ` const a = 1; console.log(a) @@ -4229,7 +4431,14 @@ describe("bundler", () => { for (const e of x) console.log(e) `, }, + minifySyntax: true, + onAfterBundle(api) { + const code = api.readFile("/out.js"); + expect(code).not.toContain("const"); + assert([...code.matchAll(/let/g)].length === 3, "should have 3 let statements"); + }, }); + // TODO: this fails on esbuild ??? itBundled("default/ConstWithLetNoBundle", { // GENERATED files: { @@ -4242,41 +4451,36 @@ describe("bundler", () => { for (const e of x) console.log(e) `, }, - mode: "passthrough", - }); - itBundled("default/ConstWithLetNoMangle", { - // GENERATED - files: { - "/entry.js": /* js */ ` - const a = 1; console.log(a) - if (true) { const b = 2; console.log(b) } - for (const c = x;;) console.log(c) - for (const d in x) console.log(d) - for (const e of x) console.log(e) - `, - }, - }); - itBundled("default/RequireMainCacheCommonJS", { - // GENERATED - files: { - "/entry.js": /* js */ ` - console.log('is main:', require.main === module) - console.log(require('./is-main')) - console.log('cache:', require.cache); - `, - "/is-main.js": `module.exports = require.main === module`, + minifySyntax: true, + mode: "transform", + onAfterBundle(api) { + const code = api.readFile("/out.js"); + expect(code).not.toContain("const"); + assert([...code.matchAll(/let/g)].length === 3, "should have 3 let statements"); }, - platform: "node", }); + // TODO: this is hard to test since bun runtime doesn't support require.main and require.cache + // i'm not even sure what we want our behavior to be for this case. + // itBundled("default/RequireMainCacheCommonJS", { + // files: { + // "/entry.js": /* js */ ` + // console.log('is main:', require.main === module) + // console.log(require('./is-main')) + // console.log('cache:', require.cache); + // `, + // "/is-main.js": `module.exports = require.main === module`, + // }, + // format: "cjs", + // platform: "node", + // }); itBundled("default/ExternalES6ConvertedToCommonJS", { - // GENERATED files: { "/entry.js": /* js */ ` - require('./a') - require('./b') - require('./c') - require('./d') - require('./e') + console.log(JSON.stringify(require('./a'))); + console.log(JSON.stringify(require('./b'))); + console.log(JSON.stringify(require('./c'))); + console.log(JSON.stringify(require('./d'))); + console.log(JSON.stringify(require('./e'))); `, "/a.js": /* js */ ` import * as ns from 'x' @@ -4290,88 +4494,87 @@ describe("bundler", () => { "/d.js": `export {ns} from 'x'`, "/e.js": `export * from 'x'`, }, + external: ["x"], format: "esm", + runtimeFiles: { + "/node_modules/x/index.js": /* js */ ` + export const ns = 123 + export const ns2 = 456 + `, + }, + run: { + stdout: ` + {"ns":{"ns":123,"ns2":456}} + {"ns":{"ns":123,"ns2":456}} + {"ns":{"ns":123,"ns2":456}} + {"ns":123} + {"ns":123,"ns2":456} + `, + }, }); - itBundled("default/CallImportNamespaceWarning", { - // GENERATED - files: { - "/js.js": /* js */ ` - import * as a from "a" - import {b} from "b" - import c from "c" - a() - b() - c() - new a() - new b() - new c() - `, - "/ts.ts": /* ts */ ` - import * as a from "a" - import {b} from "b" - import c from "c" - a() - b() - c() - new a() - new b() - new c() - `, - "/jsx-components.jsx": /* jsx */ ` - import * as A from "a" - import {B} from "b" - import C from "c" - <A/>; - <B/>; - <C/>; - `, - "/jsx-a.jsx": /* jsx */ ` - // @jsx a - import * as a from "a" - <div/> - `, - "/jsx-b.jsx": /* jsx */ ` - // @jsx b - import {b} from "b" - <div/> - `, - "/jsx-c.jsx": /* jsx */ ` - // @jsx c - import c from "c" - <div/> - `, - }, - entryPoints: ["/js.js", "/ts.ts", "/jsx-components.jsx", "/jsx-a.jsx", "/jsx-b.jsx", "/jsx-c.jsx"], - mode: "convertformat", - /* TODO FIX expectedScanLog: `js.js: WARNING: Calling "a" will crash at run-time because it's an import namespace object, not a function - js.js: NOTE: Consider changing "a" to a default import instead: - js.js: WARNING: Constructing "a" will crash at run-time because it's an import namespace object, not a constructor - js.js: NOTE: Consider changing "a" to a default import instead: - jsx-a.jsx: WARNING: Calling "a" will crash at run-time because it's an import namespace object, not a function - jsx-a.jsx: NOTE: Consider changing "a" to a default import instead: - jsx-components.jsx: WARNING: Using "A" in a JSX expression will crash at run-time because it's an import namespace object, not a component - jsx-components.jsx: NOTE: Consider changing "A" to a default import instead: - ts.ts: WARNING: Calling "a" will crash at run-time because it's an import namespace object, not a function - ts.ts: NOTE: Consider changing "a" to a default import instead: - NOTE: Make sure to enable TypeScript's "esModuleInterop" setting so that TypeScript's type checker generates an error when you try to do this. You can read more about this setting here: https://www.typescriptlang.org/tsconfig#esModuleInterop - ts.ts: WARNING: Constructing "a" will crash at run-time because it's an import namespace object, not a constructor - ts.ts: NOTE: Consider changing "a" to a default import instead: - NOTE: Make sure to enable TypeScript's "esModuleInterop" setting so that TypeScript's type checker generates an error when you try to do this. You can read more about this setting here: https://www.typescriptlang.org/tsconfig#esModuleInterop - `, */ - }); + // TODO: + // itBundled("default/CallImportNamespaceWarning", { + // files: { + // "/js.js": /* js */ ` + // import * as a from "a" + // import {b} from "b" + // import c from "c" + // a() + // b() + // c() + // new a() + // new b() + // new c() + // `, + // "/ts.ts": /* ts */ ` + // import * as a from "a" + // import {b} from "b" + // import c from "c" + // a() + // b() + // c() + // new a() + // new b() + // new c() + // `, + // "/jsx-components.jsx": /* jsx */ ` + // import * as A from "a" + // import {B} from "b" + // import C from "c" + // <A/>; + // <B/>; + // <C/>; + // `, + // "/jsx-a.jsx": /* jsx */ ` + // // @jsx a + // import * as a from "a" + // <div/> + // `, + // "/jsx-b.jsx": /* jsx */ ` + // // @jsx b + // import {b} from "b" + // <div/> + // `, + // "/jsx-c.jsx": /* jsx */ ` + // // @jsx c + // import c from "c" + // <div/> + // `, + // }, + // entryPoints: ["/js.js", "/ts.ts", "/jsx-components.jsx", "/jsx-a.jsx", "/jsx-b.jsx", "/jsx-c.jsx"], + // mode: "transform", + // external: ["a", "b", "c", "react/jsx-dev-runtime"], + // }); + return; + // I cant get bun to use `this` as the JSX runtime. It's a pretty silly idea anyways. itBundled("default/JSXThisValueCommonJS", { - // GENERATED files: { "/factory.jsx": /* jsx */ ` - console.log([ - <x />, - /* @__PURE__ */ this('x', null), - ]) + CHECK1(<x />); + CHECK1(/* @__PURE__ */ this('x', null)); f = function() { - console.log([ - <y />, - /* @__PURE__ */ this('y', null), - ]) + CHECK2(<y />); + CHECK2(/* @__PURE__ */ this('y', null)); } `, "/fragment.jsx": /* jsx */ ` @@ -4388,7 +4591,10 @@ describe("bundler", () => { `, }, entryPoints: ["/factory.jsx", "/fragment.jsx"], + external: ["react/jsx-dev-runtime", "react"], jsx: { + development: false, + automaticRuntime: false, factory: "this", fragment: "this", }, diff --git a/test/bundler/expectBundled.md b/test/bundler/expectBundled.md index f8883459d..4e944543c 100644 --- a/test/bundler/expectBundled.md +++ b/test/bundler/expectBundled.md @@ -182,3 +182,9 @@ itBundled("importstar/ReExportStarExternalIIFE", { ## dce: true This parameter checks the bundle for strings like `DROP`, `REMOVE`, and `FAIL` within the bundle, and will throw an error. This is handy for dead code elimination tests where you can just name variables that should be removed with one of those trigger words. In addition, `KEEP`, `PRESERVE`, and `KEEPME` is scanned in the source code and will throw an error if the count of those strings is not equal to the count of the corresponding trigger strings. + +Places that are not required to be dce'd contain `POSSIBLE_REMOVAL` and do not trigger an error if not removed. These might be able to be optimized in the future. + +## keepNames tricks + +In `esbuild/default.test.ts`, test `default/KeepNamesTreeShaking`, we call the esbuild cli to minify identifiers, and then check the code for expected class names to survive the minification (keep names forcibily sets functions `.name`). diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 87e31b447..cd62ce515 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -42,6 +42,8 @@ if (ESBUILD) { console.warn("NOTE: using esbuild for bun build tests"); } +export const ESBUILD_PATH = import.meta.resolveSync("esbuild/bin/esbuild"); + export interface BundlerTestInput { // file options files: Record<string, string>; @@ -131,6 +133,11 @@ export interface BundlerTestInput { */ dce?: boolean; /** + * Override the number of keep markers, which is auto detected by default. + * Does nothing if dce is false. + */ + dceKeepMarkerCount?: number | Record<string, number> | false; + /** * Shorthand for testing splitting cases. Given a list of files, checks that each file doesn't * contain the specified strings. This lets us test that certain values are not bundled. */ @@ -192,6 +199,7 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole bundleErrors, bundleWarnings, dce, + dceKeepMarkerCount, define, entryNames, entryPoints, @@ -202,13 +210,17 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole globalName, inject, jsx = {}, + keepNames, legalComments, + loader, + mainFields, metafile, minifyIdentifiers, minifySyntax, minifyWhitespace, mode, onAfterBundle, + outbase, outdir, outfile, outputPaths, @@ -218,6 +230,7 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole skipOnEsbuild, sourceMap, splitting, + treeShaking, unsupportedCSSFeatures, unsupportedJSFeatures, ...unknownProps @@ -244,9 +257,6 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole if (bundleWarnings === true) bundleWarnings = {}; const useOutFile = outfile ? true : outdir ? false : entryPoints.length === 1; - if (!ESBUILD && jsx.automaticRuntime) { - throw new Error("jsx.automaticRuntime not implemented in bun build"); - } if (!ESBUILD && format !== "esm") { throw new Error("formats besides esm not implemented in bun build"); } @@ -262,6 +272,21 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole if (!ESBUILD && unsupportedCSSFeatures && unsupportedCSSFeatures.length) { throw new Error("unsupportedCSSFeatures not implemented in bun build"); } + if (!ESBUILD && outbase) { + throw new Error("outbase not implemented in bun build"); + } + if (!ESBUILD && keepNames) { + throw new Error("keepNames not implemented in bun build"); + } + if (!ESBUILD && minifyIdentifiers) { + throw new Error("minifyIdentifiers not implemented in bun build"); + } + if (!ESBUILD && mainFields) { + throw new Error("mainFields not implemented in bun build"); + } + if (!ESBUILD && loader) { + throw new Error("loader not implemented in bun build"); + } if (ESBUILD && skipOnEsbuild) { return; } @@ -281,9 +306,11 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole outfile = useOutFile ? path.join(root, outfile ?? "/out.js") : undefined; outdir = !useOutFile ? path.join(root, outdir ?? "/out") : undefined; metafile = metafile ? path.join(root, metafile) : undefined; - outputPaths = outputPaths - ? outputPaths.map(file => path.join(root, file)) - : entryPaths.map(file => path.join(outdir!, path.basename(file))); + outputPaths = ( + outputPaths + ? outputPaths.map(file => path.join(root, file)) + : entryPaths.map(file => path.join(outdir!, path.basename(file))) + ).map(x => x.replace(/\.ts$/, ".js")); if (outdir) { entryNames ??= "[name].[ext]"; @@ -316,25 +343,30 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`, define && Object.entries(define).map(([k, v]) => ["--define", `${k}=${v}`]), `--platform=${platform}`, + external && external.map(x => ["--external", x]), minifyIdentifiers && `--minify-identifiers`, minifySyntax && `--minify-syntax`, minifyWhitespace && `--minify-whitespace`, globalName && `--global-name=${globalName}`, - external && external.map(x => ["--external", x]), - inject && inject.map(x => ["--inject", path.join(root, x)]), - jsx.automaticRuntime && "--jsx=automatic", + // inject && inject.map(x => ["--inject", path.join(root, x)]), + jsx.automaticRuntime === false && "--jsx=classic", jsx.factory && `--jsx-factory=${jsx.factory}`, jsx.fragment && `--jsx-fragment=${jsx.fragment}`, - jsx.development && `--jsx-dev`, + jsx.development === false && `--jsx-production`, // metafile && `--metafile=${metafile}`, // sourceMap && `--sourcemap${sourceMap !== true ? `=${sourceMap}` : ""}`, entryNames && entryNames !== "[name].[ext]" && [`--entry-names`, entryNames], // `--format=${format}`, // legalComments && `--legal-comments=${legalComments}`, splitting && `--splitting`, + // treeShaking && `--tree-shaking`, + // outbase && `--outbase=${outbase}`, + // keepNames && `--keep-names`, + // mainFields && `--main-fields=${mainFields}`, + // loader && Object.entries(loader).map(([k, v]) => ["--loader", `${k}=${v}`]), ] : [ - Bun.which("esbuild"), + ESBUILD_PATH, mode === "bundle" && "--bundle", outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`, `--format=${format}`, @@ -356,6 +388,11 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole banner && `--banner:js=${banner}`, legalComments && `--legal-comments=${legalComments}`, splitting && `--splitting`, + treeShaking && `--tree-shaking`, + outbase && `--outbase=${path.join(root, outbase)}`, + keepNames && `--keep-names`, + mainFields && `--main-fields=${mainFields.join(",")}`, + loader && Object.entries(loader).map(([k, v]) => `--loader:${k}=${v}`), [...(unsupportedJSFeatures ?? []), ...(unsupportedCSSFeatures ?? [])].map(x => `--supported:${x}=false`), ...entryPaths, ...(entryPointsRaw ?? []), @@ -572,17 +609,48 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole options: opts, } satisfies BundlerTestBundleAPI; + // DCE keep scan + let keepMarkers: Record<string, number> = typeof dceKeepMarkerCount === "object" ? dceKeepMarkerCount : {}; + let keepMarkersFound = 0; + if (dce && typeof dceKeepMarkerCount !== "number" && dceKeepMarkerCount !== false) { + for (const file of Object.entries(files)) { + keepMarkers[outfile ? outfile : path.join(outdir!, file[0]).slice(root.length).replace(/\.ts$/, ".js")] ??= [ + ...file[1].matchAll(/KEEP/gi), + ].length; + } + } + // Check that the bundle failed with status code 0 by verifying all files exist. + // TODO: clean up this entire bit into one main loop if (outfile) { if (!existsSync(outfile)) { throw new Error("Bundle was not written to disk: " + outfile); } else { const content = readFileSync(outfile).toString(); if (dce) { - const dceFails = [...content.matchAll(/FAIL|FAILED|DROP|REMOVE/g)]; + const dceFails = [...content.matchAll(/FAIL|FAILED|DROP|REMOVE/gi)]; if (dceFails.length) { throw new Error("DCE test did not remove all expected code in " + outfile + "."); } + if (dceKeepMarkerCount !== false) { + const keepMarkersThisFile = [...content.matchAll(/KEEP/gi)].length; + keepMarkersFound += keepMarkersThisFile; + if ( + (typeof dceKeepMarkerCount === "number" + ? dceKeepMarkerCount + : Object.values(keepMarkers).reduce((a, b) => a + b, 0)) !== keepMarkersThisFile + ) { + throw new Error( + "DCE keep markers were not preserved in " + + outfile + + ". Expected " + + keepMarkers[outfile] + + " but found " + + keepMarkersThisFile + + ".", + ); + } + } } if (!ESBUILD) { // expect(readFileSync(outfile).toString()).toMatchSnapshot(outfile.slice(root.length)); @@ -590,18 +658,53 @@ export function expectBundled(id: string, opts: BundlerTestInput, dryRun?: boole } } else { // entryNames makes it so we cannot predict the output file - if (!entryNames) { + if (!entryNames || entryNames === "[name].[ext]") { for (const fullpath of outputPaths) { if (!existsSync(fullpath)) { throw new Error("Bundle was not written to disk: " + fullpath); - } else if (!ESBUILD) { - // expect(readFileSync(fullpath).toString()).toMatchSnapshot(fullpath.slice(root.length)); + } else { + if (!ESBUILD) { + // expect(readFileSync(fullpath).toString()).toMatchSnapshot(fullpath.slice(root.length)); + } + if (dce) { + const content = readFileSync(fullpath, "utf8"); + const dceFails = [...content.matchAll(/FAIL|FAILED|DROP|REMOVE/gi)]; + const key = fullpath.slice(root.length); + if (dceFails.length) { + throw new Error("DCE test did not remove all expected code in " + key + "."); + } + if (dceKeepMarkerCount !== false) { + const keepMarkersThisFile = [...content.matchAll(/KEEP/gi)].length; + keepMarkersFound += keepMarkersThisFile; + if (keepMarkers[key] !== keepMarkersThisFile) { + throw new Error( + "DCE keep markers were not preserved in " + + key + + ". Expected " + + keepMarkers[key] + + " but found " + + keepMarkersThisFile + + ".", + ); + } + } + } } } } else if (!ESBUILD) { // TODO: snapshot these test cases } } + if ( + dce && + dceKeepMarkerCount !== false && + typeof dceKeepMarkerCount === "number" && + keepMarkersFound !== dceKeepMarkerCount + ) { + throw new Error( + `DCE keep markers were not preserved. Expected ${dceKeepMarkerCount} KEEP markers, but found ${keepMarkersFound}.`, + ); + } if (assertNotPresent) { for (const [key, value] of Object.entries(assertNotPresent)) { diff --git a/test/bundler/report-bundler-test-progress.sh b/test/bundler/report-bundler-test-progress.sh new file mode 100644 index 000000000..1ee0fec66 --- /dev/null +++ b/test/bundler/report-bundler-test-progress.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +tests="$(echo esbuild/* bundler*.test.ts)" + +printf "%40s %7s %7s | %5s %5s %5s | %5s\n" "TEST" "defined" "refined" "pass" "fail" "skip" "%pass" + +total_defined=0 +total_total=0 +total_pass=0 +total_fail=0 +total_skip=0 + +for test in $tests; do + defined=$(grep "^import" $test -v | grep -v expectBundled.md | grep -Ec "expectBundled|itBundled") + output=$(bun test $test 2>&1 | tail -n 5) + pass=$(echo "$output" | grep "pass" | cut -d " " -f 2) + fail=$(echo "$output" | grep "fail" | cut -d " " -f 2) + skip=$(echo "$output" | grep "skip" | cut -d " " -f 2) + if [ -z "$skip" ]; then skip=0; fi + if [ -z "$fail" ]; then fail=0; fi + if [ -z "$pass" ]; then pass=0; fi + total=$((pass + fail + skip)) + percent_pass=$(echo "scale=1; ($pass * 100) / ($pass + $fail) " | bc 2>/dev/null || echo "-") + printf "%40s %7s %7s | %5s %5s %5s | %5s%%\n" "$test" "$defined" "$total" "$pass" "$fail" "$skip" "$percent_pass" + + total_defined=$((total_defined + defined)) + total_total=$((total_total + total)) + total_pass=$((total_pass + pass)) + total_fail=$((total_fail + fail)) + total_skip=$((total_skip + skip)) +done + +total_pass_percent=$(echo "scale=1; ($total_pass * 100) / ($total_pass + $total_fail)") + +printf -- "\n" +printf "%40s %7s %7s | %5s %5s %5s | %5s\n" "TOTAL" "$total_defined" "$total_total" "$total_pass" "$total_fail" "$total_skip" "$total_pass_percent" +printf "\n" +printf "\n" +printf " %s%% Refined\n" $(echo "scale=1; $total_total / $total_defined * 100" | bc) +printf " %s%% Passing\n" $(echo "scale=1; $total_pass / $total_total * 100" | bc) +printf " %s%% Passing including what we skip\n" $(echo "scale=1; $total_pass / $total_total * 100" | bc) +printf "\n" +printf "dave's status: %s/%s tests\n" $total_total $total_defined diff --git a/test/bundler/run-single-bundler-test.sh b/test/bundler/run-single-bundler-test.sh index 5e9484dae..0b3cf96b2 100755 --- a/test/bundler/run-single-bundler-test.sh +++ b/test/bundler/run-single-bundler-test.sh @@ -6,7 +6,7 @@ if [ -z "$1" ]; then exit 1 fi -__dirname="$(dirname "$0")" +__dirname="$(dirname $(realpath "$0"))" cd "$__dirname" clear diff --git a/test/bundler/transpiler.test.js b/test/bundler/transpiler.test.js index 0272fb7c6..da7a7d00d 100644 --- a/test/bundler/transpiler.test.js +++ b/test/bundler/transpiler.test.js @@ -2944,4 +2944,14 @@ console.log(foo, array); expect(exports).toHaveLength(3); }); }); + + describe("edge cases", () => { + it("import statement with quoted specifier", () => { + expectPrinted_(`import { "x.y" as xy } from "bar";`, `import {"x.y" as xy} from "bar"`); + }); + + it('`str` + "``"', () => { + expectPrinted_('const x = `str` + "``";', "const x = `str\\`\\``"); + }); + }); }); diff --git a/test/package.json b/test/package.json index 7df5401be..f3ef067bd 100644 --- a/test/package.json +++ b/test/package.json @@ -10,7 +10,7 @@ "bktree-fast": "^0.0.7", "body-parser": "^1.20.2", "dedent": "^0.7.0", - "esbuild": "^0.17.11", + "esbuild": "^0.17.16", "express": "^4.18.2", "iconv-lite": "^0.6.3", "lodash": "^4.17.21", |
