aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar dave caruso <me@paperdave.net> 2023-04-25 22:13:39 -0400
committerGravatar GitHub <noreply@github.com> 2023-04-25 19:13:39 -0700
commit0846a4fa809430a77f1284a7a526a946007484e0 (patch)
tree799fcc8893c1993beba1962bcdfa5065222006a5
parent2256d43a32164433431f387ebc7c40597bb10638 (diff)
downloadbun-0846a4fa809430a77f1284a7a526a946007484e0.tar.gz
bun-0846a4fa809430a77f1284a7a526a946007484e0.tar.zst
bun-0846a4fa809430a77f1284a7a526a946007484e0.zip
bundler tests, testing plugins (#2740)
* add cjs2esm stuff * tests * plugin testing
-rw-r--r--test/bundler/bundler_browser.test.ts5
-rw-r--r--test/bundler/bundler_cjs2esm.test.ts4
-rw-r--r--test/bundler/bundler_edgecase.test.ts2
-rw-r--r--test/bundler/bundler_minify.test.ts3
-rw-r--r--test/bundler/bundler_plugin.test.ts393
-rw-r--r--test/bundler/esbuild/css.test.ts2
-rw-r--r--test/bundler/esbuild/dce.test.ts3
-rw-r--r--test/bundler/esbuild/default.test.ts9
-rw-r--r--test/bundler/esbuild/importstar.test.ts2
-rw-r--r--test/bundler/esbuild/importstar_ts.test.ts2
-rw-r--r--test/bundler/esbuild/loader.test.ts2
-rw-r--r--test/bundler/esbuild/lower.test.ts2
-rw-r--r--test/bundler/esbuild/packagejson.test.ts2
-rw-r--r--test/bundler/esbuild/ts.test.ts2
-rw-r--r--test/bundler/esbuild/tsconfig.test.ts2
-rw-r--r--test/bundler/expectBundled.md2
-rw-r--r--test/bundler/expectBundled.ts1276
-rwxr-xr-xtest/bundler/run-single-bundler-test.sh17
18 files changed, 1117 insertions, 613 deletions
diff --git a/test/bundler/bundler_browser.test.ts b/test/bundler/bundler_browser.test.ts
index 89570a445..422e860b5 100644
--- a/test/bundler/bundler_browser.test.ts
+++ b/test/bundler/bundler_browser.test.ts
@@ -1,6 +1,6 @@
import assert from "assert";
import dedent from "dedent";
-import { bundlerTest, expectBundled, itBundled, testForFile } from "./expectBundled";
+import { itBundled, testForFile } from "./expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
describe("bundler", () => {
@@ -265,7 +265,8 @@ describe("bundler", () => {
},
});
- itBundled("browser/BunPolyfillExternal", {
+ // not implemented right now
+ itBundled.skip("browser/BunPolyfillExternal", {
skipOnEsbuild: true,
files: ImportBunError.options.files,
platform: "browser",
diff --git a/test/bundler/bundler_cjs2esm.test.ts b/test/bundler/bundler_cjs2esm.test.ts
index 3b3e01802..f88700372 100644
--- a/test/bundler/bundler_cjs2esm.test.ts
+++ b/test/bundler/bundler_cjs2esm.test.ts
@@ -1,6 +1,6 @@
import assert from "assert";
import dedent from "dedent";
-import { bundlerTest, expectBundled, itBundled, testForFile } from "./expectBundled";
+import { itBundled, testForFile } from "./expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
describe("bundler", () => {
@@ -161,7 +161,7 @@ describe("bundler", () => {
`,
},
cjs2esm: {
- exclude: ["/node_modules/lib/index.js"],
+ unhandled: ["/node_modules/lib/index.js"],
},
run: {
stdout: "development",
diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts
index 66c7f1b1d..d7b994200 100644
--- a/test/bundler/bundler_edgecase.test.ts
+++ b/test/bundler/bundler_edgecase.test.ts
@@ -1,6 +1,6 @@
import assert from "assert";
import dedent from "dedent";
-import { bundlerTest, expectBundled, itBundled, testForFile } from "./expectBundled";
+import { itBundled, testForFile } from "./expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
describe("bundler", () => {
diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts
index d52252af0..cad991f2a 100644
--- a/test/bundler/bundler_minify.test.ts
+++ b/test/bundler/bundler_minify.test.ts
@@ -1,6 +1,6 @@
import assert from "assert";
import dedent from "dedent";
-import { bundlerTest, expectBundled, itBundled, testForFile } from "./expectBundled";
+import { itBundled, testForFile } from "./expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
describe("bundler", () => {
@@ -53,7 +53,6 @@ describe("bundler", () => {
],
minifySyntax: true,
platform: "bun",
- minifySyntax: true,
});
itBundled("minify/FunctionExpressionRemoveName", {
notImplemented: true,
diff --git a/test/bundler/bundler_plugin.test.ts b/test/bundler/bundler_plugin.test.ts
new file mode 100644
index 000000000..69bd221ef
--- /dev/null
+++ b/test/bundler/bundler_plugin.test.ts
@@ -0,0 +1,393 @@
+import assert from "assert";
+import dedent from "dedent";
+import path from "path";
+import { itBundled, testForFile } from "./expectBundled";
+var { describe, test, expect } = testForFile(import.meta.path);
+
+describe("bundler", () => {
+ const loadFixture = {
+ "index.ts": /* ts */ `
+ import { foo } from "./foo.magic";
+ console.log(foo);
+ `,
+ "foo.magic": `
+ hello world
+ `,
+ "another_file.ts": `
+ export const foo = "foo";
+ `,
+ };
+ const resolveFixture = {
+ "index.ts": /* ts */ `
+ import { foo } from "./foo.magic";
+ console.log(foo);
+ `,
+ "foo.ts": /* ts */ `
+ export const foo = "foo";
+ `,
+ };
+
+ itBundled("plugin/Resolve", {
+ files: resolveFixture,
+ // The bundler testing api has a shorthand where the plugins array can be
+ // the `setup` function of one plugin.
+ plugins(builder) {
+ builder.onResolve({ filter: /\.magic$/ }, args => {
+ return {
+ path: path.resolve(path.dirname(args.importer), args.path.replace(/\.magic$/, ".ts")),
+ };
+ });
+ },
+ run: {
+ stdout: "foo",
+ },
+ });
+ itBundled("plugin/Load", {
+ files: loadFixture,
+ plugins(builder) {
+ builder.onLoad({ filter: /\.magic$/ }, async args => {
+ const text = await Bun.file(args.path).text();
+ return {
+ contents: `export const foo = ${JSON.stringify(text.toUpperCase())};`,
+ loader: "ts",
+ };
+ });
+ },
+ run: {
+ stdout: "foo",
+ },
+ });
+
+ // Load Plugin Errors
+ itBundled("plugin/LoadThrow", {
+ files: loadFixture,
+ plugins(builder) {
+ builder.onLoad({ filter: /\.magic$/ }, args => {
+ throw new Error("error here");
+ });
+ },
+ });
+ itBundled("plugin/LoadThrowPrimative", {
+ files: loadFixture,
+ plugins(builder) {
+ builder.onLoad({ filter: /\.magic$/ }, args => {
+ throw "123";
+ });
+ },
+ });
+ itBundled("plugin/LoadThrowAsync", {
+ files: loadFixture,
+ plugins(builder) {
+ builder.onLoad({ filter: /\.magic$/ }, async args => {
+ throw new Error("error here");
+ });
+ },
+ });
+ itBundled("plugin/LoadThrowPrimativeAsync", {
+ files: loadFixture,
+ plugins(builder) {
+ builder.onLoad({ filter: /\.magic$/ }, async args => {
+ throw 123;
+ });
+ },
+ });
+
+ // Load Plugin Errors
+ itBundled("plugin/ResolveThrow", {
+ files: resolveFixture,
+ plugins(builder) {
+ builder.onResolve({ filter: /\.magic$/ }, args => {
+ throw new Error("error here");
+ });
+ },
+ });
+ itBundled("plugin/ResolveThrowPrimative", {
+ files: resolveFixture,
+ plugins(builder) {
+ builder.onResolve({ filter: /\.magic$/ }, args => {
+ throw "123";
+ });
+ },
+ });
+
+ //
+ itBundled("plugin/ResolvePrefix", ({ root }) => {
+ let onResolveCount = 0;
+
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "magic:some_string";
+ import * as bar from "./other_file.ts";
+ console.log(foo.foo, bar.bar);
+ `,
+ "foo.ts": /* ts */ `
+ export const foo = "foo";
+ `,
+ "other_file.ts": /* ts */ `
+ export const bar = "bar";
+ `,
+ },
+ plugins(builder) {
+ builder.onResolve({ filter: /.*/, namespace: "magic" }, args => {
+ throw new Error("should not be called. magic: does not make this a namespace");
+ });
+ builder.onResolve({ filter: /^magic:.*/ }, args => {
+ expect(args.path).toBe("magic:some_string");
+ expect(args.importer).toBe(root + "/index.ts");
+ expect(args.namespace).toBe("file");
+ expect(args.kind).toBe("import-statement");
+ onResolveCount++;
+
+ return {
+ path: path.resolve(path.dirname(args.importer), "foo.ts"),
+ };
+ });
+ },
+ run: {
+ stdout: "foo bar",
+ },
+ onAfterBundle(api) {
+ expect(onResolveCount).toBe(1);
+ },
+ };
+ });
+ itBundled("plugin/ResolveNamespaceFilterIgnored", ({ root }) => {
+ let onResolveCountBad = 0;
+
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "magic:some_string";
+ import * as bar from "./other_file.ts";
+ console.log(foo.foo, bar.bar);
+ `,
+ "foo.ts": /* ts */ `
+ export const foo = "foo";
+ `,
+ "other_file.ts": /* ts */ `
+ export const bar = "bar";
+ `,
+ },
+ plugins(builder) {
+ // this was being called when it shouldnt
+ builder.onResolve({ filter: /.*/, namespace: "magic" }, args => {
+ onResolveCountBad++;
+ });
+ builder.onResolve({ filter: /magic:some_string/, namespace: "magic" }, args => {
+ onResolveCountBad++;
+ });
+ builder.onResolve({ filter: /magic:some_string/ }, args => {
+ return {
+ path: path.resolve(path.dirname(args.importer), "foo.ts"),
+ };
+ });
+ },
+ run: {
+ stdout: "foo bar",
+ },
+ onAfterBundle(api) {
+ try {
+ expect(onResolveCountBad).toBe(0);
+ } catch (error) {
+ console.error(
+ "resolve plugins with namespace constraint should not be called when the namespace is not matched, even if prefix like `magic:`",
+ );
+ throw error;
+ }
+ },
+ };
+ });
+ itBundled("plugin/ResolveAndLoadNamespace", ({ root }) => {
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "magic:some_string";
+ console.log(foo.foo);
+ `,
+ },
+ plugins(builder) {
+ builder.onResolve({ filter: /magic:some_string/ }, args => {
+ return {
+ path: "namespace_path",
+ namespace: "my_namespace",
+ };
+ });
+ // the path given is already resolved, so it should not re-resolve
+ builder.onResolve({ filter: /namespace_path/, namespace: "my_namespace" }, args => {
+ throw new Error("SHOULD NOT BE CALLED");
+ });
+ builder.onResolve({ filter: /namespace_path/ }, args => {
+ throw new Error("SHOULD NOT BE CALLED");
+ });
+ builder.onLoad({ filter: /namespace_path/, namespace: "my_namespace" }, args => {
+ expect(args.path).toBe("namespace_path");
+ expect(args.namespace).toBe("my_namespace");
+ expect(args.suffix).toBeFalsy();
+
+ return {
+ contents: "export const foo = 'foo';",
+ loader: "js",
+ };
+ });
+ builder.onLoad({ filter: /.*/, namespace: "my_namespace" }, args => {
+ throw new Error("SHOULD NOT BE CALLED");
+ });
+ },
+ run: {
+ stdout: "foo",
+ },
+ };
+ });
+ itBundled("plugin/ResolveAndLoadNamespaceNested", ({ root }) => {
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "magic:some_string";
+ console.log(foo.foo);
+ `,
+ "foo.ts": /* ts */ `
+ export const foo = "foo";
+ `,
+ },
+ plugins(builder) {
+ builder.onResolve({ filter: /magic:some_string/ }, args => {
+ return {
+ path: "namespace_path",
+ namespace: "my_namespace",
+ };
+ });
+ // the path given is already resolved, so it should not re-resolve
+ builder.onResolve({ filter: /namespace_path/, namespace: "my_namespace" }, args => {
+ throw new Error("SHOULD NOT BE CALLED");
+ });
+ builder.onResolve({ filter: /namespace_path/ }, args => {
+ throw new Error("SHOULD NOT BE CALLED");
+ });
+ builder.onLoad({ filter: /namespace_path/, namespace: "my_namespace" }, args => {
+ expect(args.path).toBe("namespace_path");
+ expect(args.namespace).toBe("my_namespace");
+ expect(args.suffix).toBeFalsy();
+
+ return {
+ contents: "import 'nested_import';export const foo = 'foo';",
+ loader: "js",
+ };
+ });
+ builder.onResolve({ filter: /nested_import/ }, args => {
+ expect(args.path).toBe("nested_import");
+ expect(args.namespace).toBe("my_namespace");
+ return {
+ path: root + "/foo.ts",
+ namespace: "file",
+ };
+ });
+ },
+ run: {
+ stdout: "foo",
+ },
+ };
+ });
+ itBundled("plugin/ResolveOverrideFile", ({ root }) => {
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "./foo.ts";
+ console.log(foo.foo);
+ `,
+ "foo.ts": /* ts */ `
+ export const foo = "FAILED";
+ `,
+ "bar.ts": /* ts */ `
+ export const foo = "foo";
+ `,
+ },
+ plugins(builder) {
+ builder.onResolve({ filter: /foo.ts$/ }, args => {
+ return {
+ path: root + "/bar.ts",
+ };
+ });
+ },
+ run: {
+ stdout: "foo",
+ },
+ };
+ });
+ itBundled("plugin/ResolveTwoImportsOnce", ({ root }) => {
+ let onResolveCount = 0;
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "./foo.ts";
+ import * as foo2 from "./foo.ts";
+ console.log(foo.foo, foo2.foo);
+ `,
+ "foo.ts": /* ts */ `
+ export const foo = "FAILED";
+ `,
+ "bar.ts": /* ts */ `
+ export const foo = "this string should exist once";
+ `,
+ },
+ plugins(builder) {
+ builder.onResolve({ filter: /foo.ts$/ }, args => {
+ onResolveCount++;
+ return {
+ path: root + "/bar.ts",
+ };
+ });
+ },
+ run: {
+ stdout: "this string should exist once this string should exist once",
+ },
+ onAfterBundle(api) {
+ expect(onResolveCount).toBe(1);
+ const contents = api.readFile("/out.js");
+ expect([...contents.matchAll(/this string should exist once/g)].length).toBe(1);
+ },
+ };
+ });
+ itBundled("plugin/ResolveTwoImportsSeparateFiles", ({ root }) => {
+ let onResolveCount = 0;
+ let importers: string[] = [];
+ return {
+ files: {
+ "index.ts": /* ts */ `
+ import * as foo from "./one.ts";
+ import * as bar from "./two.ts";
+ console.log(foo.foo, bar.bar);
+ `,
+ "one.ts": /* ts */ `
+ import * as imported from "./foo.ts";
+ export const foo = imported.foo;
+ `,
+ "two.ts": /* ts */ `
+ import * as imported from "./foo.ts";
+ export const bar = imported.foo;
+ `,
+ "bar.ts": /* ts */ `
+ export const foo = "this string should exist once";
+ `,
+ },
+ plugins(builder) {
+ builder.onResolve({ filter: /foo.ts$/ }, args => {
+ importers.push(args.importer);
+ onResolveCount++;
+ return {
+ path: root + "/bar.ts",
+ };
+ });
+ },
+ run: {
+ stdout: "this string should exist once this string should exist once",
+ },
+ onAfterBundle(api) {
+ expect(importers).toEqual([root + "/one.ts", root + "/two.ts"]);
+ expect(onResolveCount).toBe(2);
+ const contents = api.readFile("/out.js");
+ expect([...contents.matchAll(/this string should exist once/g)].length).toBe(1);
+ },
+ };
+ });
+});
diff --git a/test/bundler/esbuild/css.test.ts b/test/bundler/esbuild/css.test.ts
index 83203cb6c..75eeb21c5 100644
--- a/test/bundler/esbuild/css.test.ts
+++ b/test/bundler/esbuild/css.test.ts
@@ -1,4 +1,4 @@
-import { expectBundled, itBundled, testForFile } from "../expectBundled";
+import { itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/dce.test.ts b/test/bundler/esbuild/dce.test.ts
index 6e65244cf..9d4a0c27d 100644
--- a/test/bundler/esbuild/dce.test.ts
+++ b/test/bundler/esbuild/dce.test.ts
@@ -1,6 +1,6 @@
import assert from "assert";
import dedent from "dedent";
-import { expectBundled, itBundled, testForFile } from "../expectBundled";
+import { itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
@@ -1272,6 +1272,7 @@ describe("bundler", () => {
dce: true,
});
itBundled("dce/TreeShakingClassProperty", {
+ notImplemented: true,
files: {
"/entry.js": /* js */ `
let remove1 = class { x }
diff --git a/test/bundler/esbuild/default.test.ts b/test/bundler/esbuild/default.test.ts
index 421b1fd9f..e178637aa 100644
--- a/test/bundler/esbuild/default.test.ts
+++ b/test/bundler/esbuild/default.test.ts
@@ -1,13 +1,6 @@
import assert from "assert";
import dedent from "dedent";
-import {
- ESBUILD_PATH,
- RUN_UNCHECKED_TESTS,
- bundlerTest,
- expectBundled,
- itBundled,
- testForFile,
-} from "../expectBundled";
+import { ESBUILD_PATH, RUN_UNCHECKED_TESTS, itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/importstar.test.ts b/test/bundler/esbuild/importstar.test.ts
index 0b7daa532..f65e0f8f4 100644
--- a/test/bundler/esbuild/importstar.test.ts
+++ b/test/bundler/esbuild/importstar.test.ts
@@ -1,5 +1,5 @@
import assert from "assert";
-import { expectBundled, itBundled, testForFile } from "../expectBundled";
+import { itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/importstar_ts.test.ts b/test/bundler/esbuild/importstar_ts.test.ts
index 76afe6d7e..5bbb0567e 100644
--- a/test/bundler/esbuild/importstar_ts.test.ts
+++ b/test/bundler/esbuild/importstar_ts.test.ts
@@ -1,5 +1,5 @@
import { test, describe } from "bun:test";
-import { RUN_UNCHECKED_TESTS, expectBundled, itBundled } from "../expectBundled";
+import { RUN_UNCHECKED_TESTS, itBundled } from "../expectBundled";
// Tests ported from:
// https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_importstar_ts_test.go
diff --git a/test/bundler/esbuild/loader.test.ts b/test/bundler/esbuild/loader.test.ts
index 54b5a04e5..93a4e5fff 100644
--- a/test/bundler/esbuild/loader.test.ts
+++ b/test/bundler/esbuild/loader.test.ts
@@ -1,4 +1,4 @@
-import { RUN_UNCHECKED_TESTS, expectBundled, itBundled, testForFile } from "../expectBundled";
+import { RUN_UNCHECKED_TESTS, itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/lower.test.ts b/test/bundler/esbuild/lower.test.ts
index 23b18397d..d03c4a27f 100644
--- a/test/bundler/esbuild/lower.test.ts
+++ b/test/bundler/esbuild/lower.test.ts
@@ -1,4 +1,4 @@
-import { RUN_UNCHECKED_TESTS, expectBundled, itBundled, testForFile } from "../expectBundled";
+import { RUN_UNCHECKED_TESTS, itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/packagejson.test.ts b/test/bundler/esbuild/packagejson.test.ts
index 7f1a31762..c3b241561 100644
--- a/test/bundler/esbuild/packagejson.test.ts
+++ b/test/bundler/esbuild/packagejson.test.ts
@@ -1,4 +1,4 @@
-import { RUN_UNCHECKED_TESTS, expectBundled, itBundled, testForFile } from "../expectBundled";
+import { itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/ts.test.ts b/test/bundler/esbuild/ts.test.ts
index 9843a1cbe..a3b3ce925 100644
--- a/test/bundler/esbuild/ts.test.ts
+++ b/test/bundler/esbuild/ts.test.ts
@@ -1,5 +1,5 @@
import assert from "assert";
-import { RUN_UNCHECKED_TESTS, itBundled, testForFile } from "../expectBundled";
+import { itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/esbuild/tsconfig.test.ts b/test/bundler/esbuild/tsconfig.test.ts
index 8c83fee7d..accd0ca7e 100644
--- a/test/bundler/esbuild/tsconfig.test.ts
+++ b/test/bundler/esbuild/tsconfig.test.ts
@@ -1,4 +1,4 @@
-import { bundlerTest, expectBundled, itBundled, testForFile } from "../expectBundled";
+import { itBundled, testForFile } from "../expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
// Tests ported from:
diff --git a/test/bundler/expectBundled.md b/test/bundler/expectBundled.md
index b088f3b65..299c31cac 100644
--- a/test/bundler/expectBundled.md
+++ b/test/bundler/expectBundled.md
@@ -39,7 +39,7 @@ Passing the second argument at all will use `esbuild` instead of `bun build`. It
At the start of test files, use `testForFile` instead of importing from `bun:test`:
```ts
-import { bundlerTest, expectBundled, itBundled, testForFile } from "./expectBundled";
+import { itBundled, testForFile } from "./expectBundled";
var { describe, test, expect } = testForFile(import.meta.path);
```
diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts
index 2dda32cb0..e625cb500 100644
--- a/test/bundler/expectBundled.ts
+++ b/test/bundler/expectBundled.ts
@@ -7,8 +7,10 @@ import dedent from "dedent";
import { bunEnv, bunExe } from "harness";
import { tmpdir } from "os";
import { callerSourceOrigin } from "bun:jsc";
-import { fileURLToPath } from "bun";
+import { BuildConfig, BunPlugin, fileURLToPath } from "bun";
import type { Expect } from "bun:test";
+import { PluginBuilder } from "bun";
+import * as esbuild from "esbuild";
type BunTestExports = typeof import("bun:test");
export function testForFile(file: string): BunTestExports {
@@ -68,6 +70,9 @@ export interface BundlerTestInput {
/** Use when doing something weird with entryPoints and you need to check other output paths. */
outputPaths?: string[];
+ /** force using cli or js api. defaults to api if possible, then cli otherwise */
+ backend?: "cli" | "api";
+
// bundler options
alias?: Record<string, string>;
assetNames?: string;
@@ -117,6 +122,7 @@ export interface BundlerTestInput {
/** if set to true or false, create or edit tsconfig.json to set compilerOptions.useDefineForClassFields */
useDefineForClassFields?: boolean;
sourceMap?: boolean | "inline" | "external";
+ plugins?: BunPlugin[] | ((builder: PluginBuilder) => void | Promise<void>);
// pass subprocess.env
env?: Record<string, any>;
@@ -149,9 +155,10 @@ export interface BundlerTestInput {
* Shorthand for testing CJS->ESM cases.
* Checks source code for the commonjs helper.
*
- * Set to true means all cjs files should be converted. You can pass `exclude` to expect them to stay commonjs.
+ * Set to true means all cjs files should be converted. You can pass `unhandled` to expect them
+ * to stay commonjs (will throw if esm)
*/
- cjs2esm?: boolean | { exclude: string[] };
+ cjs2esm?: boolean | { unhandled: string[] };
/**
* Override the number of keep markers, which is auto detected by default.
* Does nothing if dce is false.
@@ -222,6 +229,11 @@ export interface BundlerTestRunOptions {
runtime?: "bun" | "node";
}
+/** given when you do itBundled('id', (this object) => BundlerTestInput) */
+export interface BundlerTestWrappedAPI {
+ root: string;
+}
+
export interface BundlerTestRef {
id: string;
options: BundlerTestInput;
@@ -233,22 +245,24 @@ function testRef(id: string, options: BundlerTestInput): BundlerTestRef {
return { id, options };
}
-export function expectBundled(
+function expectBundled(
id: string,
opts: BundlerTestInput,
dryRun = false,
ignoreFilter = false,
-): BundlerTestRef {
+): Promise<BundlerTestRef> | BundlerTestRef {
var { expect, it, test } = testForFile(callerSourceOrigin());
if (!ignoreFilter && FILTER && id !== FILTER) return testRef(id, opts);
let {
- assertNotPresent,
- banner,
- bundleErrors,
bundleWarnings,
+ bundleErrors,
+ banner,
+ backend,
+ assertNotPresent,
capture,
chunkNames,
+ cjs2esm,
dce,
dceKeepMarkerCount,
define,
@@ -278,6 +292,7 @@ export function expectBundled(
outfile,
outputPaths,
platform,
+ plugins,
publicPath,
run,
runtimeFiles,
@@ -288,6 +303,8 @@ export function expectBundled(
unsupportedCSSFeatures,
unsupportedJSFeatures,
useDefineForClassFields,
+ // @ts-expect-error
+ _referenceFn,
...unknownProps
} = opts;
@@ -364,642 +381,755 @@ export function expectBundled(
return testRef(id, opts);
}
- const root = path.join(outBase, id.replaceAll("/", path.sep));
- if (DEBUG) console.log("root:", root);
+ return (async () => {
+ if (!backend) {
+ backend = plugins !== undefined ? "api" : "cli";
+ }
- const entryPaths = entryPoints.map(file => path.join(root, file));
+ const root = path.join(outBase, id.replaceAll("/", path.sep));
+ if (DEBUG) console.log("root:", root);
- if (external) {
- external = external.map(x => x.replace(/\{\{root\}\}/g, root));
- }
+ const entryPaths = entryPoints.map(file => path.join(root, file));
- 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)))
- ).map(x => x.replace(/\.ts$/, ".js"));
-
- if (mode === "transform" && !outfile) {
- throw new Error("transform mode requires one single outfile");
- }
+ if (external) {
+ external = external.map(x => x.replace(/\{\{root\}\}/g, root));
+ }
- if (outdir) {
- entryNames ??= "[name].[ext]";
- chunkNames ??= "[name]-[hash].[ext]";
- }
+ 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)))
+ ).map(x => x.replace(/\.ts$/, ".js"));
+
+ if (mode === "transform" && !outfile) {
+ throw new Error("transform mode requires one single outfile");
+ }
+ if (cjs2esm && !outfile && !minifySyntax && !minifyWhitespace) {
+ throw new Error("cjs2esm=true requires one output file, minifyWhitespace=false, and minifySyntax=false");
+ }
- // Option validation
- if (entryPaths.length !== 1 && outfile && !entryPointsRaw) {
- throw new Error("Test cannot specify `outfile` when more than one entry path.");
- }
+ if (outdir) {
+ entryNames ??= "[name].[ext]";
+ chunkNames ??= "[name]-[hash].[ext]";
+ }
- // Prepare source folder
- if (existsSync(root)) {
- rmSync(root, { recursive: true });
- }
- for (const [file, contents] of Object.entries(files)) {
- const filename = path.join(root, file);
- mkdirSync(path.dirname(filename), { recursive: true });
- writeFileSync(filename, dedent(contents).replace(/\{\{root\}\}/g, root));
- }
+ // Option validation
+ if (entryPaths.length !== 1 && outfile && !entryPointsRaw) {
+ throw new Error("Test cannot specify `outfile` when more than one entry path.");
+ }
- if (useDefineForClassFields !== undefined) {
- if (existsSync(path.join(root, "tsconfig.json"))) {
- try {
- const tsconfig = JSON.parse(readFileSync(path.join(root, "tsconfig.json"), "utf8"));
- tsconfig.compilerOptions = tsconfig.compilerOptions ?? {};
- tsconfig.compilerOptions.useDefineForClassFields = useDefineForClassFields;
- writeFileSync(path.join(root, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
- } catch (error) {
- console.log(
- "DEBUG NOTE: specifying useDefineForClassFields causes tsconfig.json to be parsed as JSON and not JSONC.",
+ // Prepare source folder
+ if (existsSync(root)) {
+ rmSync(root, { recursive: true });
+ }
+ for (const [file, contents] of Object.entries(files)) {
+ const filename = path.join(root, file);
+ mkdirSync(path.dirname(filename), { recursive: true });
+ writeFileSync(filename, dedent(contents).replace(/\{\{root\}\}/g, root));
+ }
+
+ if (useDefineForClassFields !== undefined) {
+ if (existsSync(path.join(root, "tsconfig.json"))) {
+ try {
+ const tsconfig = JSON.parse(readFileSync(path.join(root, "tsconfig.json"), "utf8"));
+ tsconfig.compilerOptions = tsconfig.compilerOptions ?? {};
+ tsconfig.compilerOptions.useDefineForClassFields = useDefineForClassFields;
+ writeFileSync(path.join(root, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
+ } catch (error) {
+ console.log(
+ "DEBUG NOTE: specifying useDefineForClassFields causes tsconfig.json to be parsed as JSON and not JSONC.",
+ );
+ }
+ } else {
+ writeFileSync(
+ path.join(root, "tsconfig.json"),
+ JSON.stringify({ compilerOptions: { useDefineForClassFields } }, null, 2),
);
}
- } else {
- writeFileSync(
- path.join(root, "tsconfig.json"),
- JSON.stringify({ compilerOptions: { useDefineForClassFields } }, null, 2),
- );
}
- }
- // Run bun build cli. In the future we can move to using `Bun.Bundler`
- const cmd = (
- !ESBUILD
- ? [
- ...(process.env.BUN_DEBUGGER ? ["lldb-server", "g:1234", "--"] : []),
- BUN_EXE,
- "build",
- ...entryPaths,
- ...(entryPointsRaw ?? []),
- mode === "bundle" ? [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}`,
- // 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 === false && `--jsx-production`,
- // metafile && `--metafile=${metafile}`,
- // sourceMap && `--sourcemap${sourceMap !== true ? `=${sourceMap}` : ""}`,
- entryNames && entryNames !== "[name].[ext]" && [`--entry-names`, entryNames],
- // chunkNames && chunkNames !== "[name]-[hash].[ext]" && [`--chunk-names`, chunkNames],
- // `--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}`]),
- // publicPath && `--public-path=${publicPath}`,
- mode === "transform" && "--transform",
- ]
- : [
- ESBUILD_PATH,
- mode === "bundle" && "--bundle",
- outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`,
- `--format=${format}`,
- `--platform=${platform === "bun" ? "node" : platform}`,
- 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)}`),
- define && Object.entries(define).map(([k, v]) => `--define:${k}=${v}`),
- jsx.automaticRuntime && "--jsx=automatic",
- jsx.factory && `--jsx-factory=${jsx.factory}`,
- jsx.fragment && `--jsx-fragment=${jsx.fragment}`,
- jsx.development && `--jsx-dev`,
- entryNames && entryNames !== "[name].[ext]" && `--entry-names=${entryNames.replace(/\.\[ext]$/, "")}`,
- chunkNames && chunkNames !== "[name]-[hash].[ext]" && `--chunk-names=${chunkNames.replace(/\.\[ext]$/, "")}`,
- metafile && `--metafile=${metafile}`,
- sourceMap && `--sourcemap${sourceMap !== true ? `=${sourceMap}` : ""}`,
- 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}`),
- publicPath && `--public-path=${publicPath}`,
- [...(unsupportedJSFeatures ?? []), ...(unsupportedCSSFeatures ?? [])].map(x => `--supported:${x}=false`),
- ...entryPaths,
- ...(entryPointsRaw ?? []),
- ]
- )
- .flat(Infinity)
- .filter(Boolean)
- .map(x => String(x)) as [string, ...string[]];
-
- if (DEBUG) {
- writeFileSync(
- path.join(root, "run.sh"),
- "#!/bin/sh\n" + cmd.map(x => (x.match(/^[a-z0-9_:=\./\\-]+$/i) ? x : `"${x.replace(/"/g, '\\"')}"`)).join(" "),
- );
- try {
- mkdirSync(path.join(root, ".vscode"), { recursive: true });
- } catch (e) {}
-
- writeFileSync(
- path.join(root, ".vscode", "launch.json"),
- JSON.stringify(
- {
- "version": "0.2.0",
- "configurations": [
+ // Run bun build cli. In the future we can move to using `Bun.Bundler`
+ let warningReference: Record<string, { file: string; error: string; line?: string; col?: string }[]> = {};
+ if (backend === "cli") {
+ if (plugins) {
+ throw new Error("plugins not possible in backend=CLI");
+ }
+
+ const cmd = (
+ !ESBUILD
+ ? [
+ ...(process.env.BUN_DEBUGGER ? ["lldb-server", "g:1234", "--"] : []),
+ BUN_EXE,
+ "build",
+ ...entryPaths,
+ ...(entryPointsRaw ?? []),
+ mode === "bundle" ? [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}`,
+ // 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 === false && `--jsx-production`,
+ // metafile && `--metafile=${metafile}`,
+ // sourceMap && `--sourcemap${sourceMap !== true ? `=${sourceMap}` : ""}`,
+ entryNames && entryNames !== "[name].[ext]" && [`--entry-names`, entryNames],
+ // chunkNames && chunkNames !== "[name]-[hash].[ext]" && [`--chunk-names`, chunkNames],
+ // `--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}`]),
+ // publicPath && `--public-path=${publicPath}`,
+ mode === "transform" && "--transform",
+ ]
+ : [
+ ESBUILD_PATH,
+ mode === "bundle" && "--bundle",
+ outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`,
+ `--format=${format}`,
+ `--platform=${platform === "bun" ? "node" : platform}`,
+ 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)}`),
+ define && Object.entries(define).map(([k, v]) => `--define:${k}=${v}`),
+ jsx.automaticRuntime && "--jsx=automatic",
+ jsx.factory && `--jsx-factory=${jsx.factory}`,
+ jsx.fragment && `--jsx-fragment=${jsx.fragment}`,
+ jsx.development && `--jsx-dev`,
+ entryNames && entryNames !== "[name].[ext]" && `--entry-names=${entryNames.replace(/\.\[ext]$/, "")}`,
+ chunkNames &&
+ chunkNames !== "[name]-[hash].[ext]" &&
+ `--chunk-names=${chunkNames.replace(/\.\[ext]$/, "")}`,
+ metafile && `--metafile=${metafile}`,
+ sourceMap && `--sourcemap${sourceMap !== true ? `=${sourceMap}` : ""}`,
+ 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}`),
+ publicPath && `--public-path=${publicPath}`,
+ [...(unsupportedJSFeatures ?? []), ...(unsupportedCSSFeatures ?? [])].map(x => `--supported:${x}=false`),
+ ...entryPaths,
+ ...(entryPointsRaw ?? []),
+ ]
+ )
+ .flat(Infinity)
+ .filter(Boolean)
+ .map(x => String(x)) as [string, ...string[]];
+
+ if (DEBUG) {
+ writeFileSync(
+ path.join(root, "run.sh"),
+ "#!/bin/sh\n" +
+ cmd.map(x => (x.match(/^[a-z0-9_:=\./\\-]+$/i) ? x : `"${x.replace(/"/g, '\\"')}"`)).join(" "),
+ );
+ try {
+ mkdirSync(path.join(root, ".vscode"), { recursive: true });
+ } catch (e) {}
+
+ writeFileSync(
+ path.join(root, ".vscode", "launch.json"),
+ JSON.stringify(
{
- "type": "lldb",
- "request": "launch",
- "name": "bun test",
- "program": cmd[0],
- "args": cmd.slice(1),
- "cwd": root,
- "env": {
- "FORCE_COLOR": "1",
- },
- "console": "internalConsole",
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "bun test",
+ "program": cmd[0],
+ "args": cmd.slice(1),
+ "cwd": root,
+ "env": {
+ "FORCE_COLOR": "1",
+ },
+ "console": "internalConsole",
+ },
+ ],
},
- ],
- },
- null,
- 2,
- ),
- );
- }
+ null,
+ 2,
+ ),
+ );
+ }
- const bundlerEnv = { ...bunEnv, ...env };
- // remove undefined keys instead of passing "undefined"
- for (const key in bundlerEnv) {
- if (bundlerEnv[key] === undefined) {
- delete bundlerEnv[key];
- }
- }
+ const bundlerEnv = { ...bunEnv, ...env };
+ // remove undefined keys instead of passing "undefined"
+ for (const key in bundlerEnv) {
+ if (bundlerEnv[key] === undefined) {
+ delete bundlerEnv[key];
+ }
+ }
+
+ const { stdout, stderr, success } = Bun.spawnSync({
+ cmd,
+ cwd: root,
+ stdio: ["ignore", "pipe", "pipe"],
+ env: bundlerEnv,
+ });
+
+ // Check for errors
+ const expectedErrors = bundleErrors
+ ? Object.entries(bundleErrors).flatMap(([file, v]) => v.map(error => ({ file, error })))
+ : null;
+
+ if (!success) {
+ if (!ESBUILD) {
+ const errorText = stderr.toString("utf-8");
+ const errorRegex = /^error: (.*?)\n(?:.*?\n\s*\^\s*\n(.*?)\n)?/gms;
+ const allErrors = [...errorText.matchAll(errorRegex)]
+ .map(([_str1, error, source]) => {
+ if (!source) {
+ if (error === "FileNotFound") {
+ return null;
+ }
+ return { error, file: "<bun>" };
+ }
+ const [_str2, fullFilename, line, col] = source.match(/bun-build-tests\/(.*):(\d+):(\d+)/)!;
+ const file = fullFilename.slice(id.length + path.basename(outBase).length + 1);
+ return { error, file, line, col };
+ })
+ .filter(Boolean) as any[];
+
+ if (allErrors.length === 0) {
+ console.log(errorText);
+ }
- const { stdout, stderr, success } = Bun.spawnSync({
- cmd,
- cwd: root,
- stdio: ["ignore", "pipe", "pipe"],
- env: bundlerEnv,
- });
-
- // Check for errors
- const expectedErrors = bundleErrors
- ? Object.entries(bundleErrors).flatMap(([file, v]) => v.map(error => ({ file, error })))
- : null;
-
- if (!success) {
- if (!ESBUILD) {
- const errorText = stderr.toString("utf-8");
- const errorRegex = /^error: (.*?)\n(?:.*?\n\s*\^\s*\n(.*?)\n)?/gms;
- const allErrors = [...errorText.matchAll(errorRegex)]
- .map(([_str1, error, source]) => {
- if (!source) {
- if (error === "FileNotFound") {
- return null;
+ if (
+ errorText.includes("Crash report saved to:") ||
+ errorText.includes("panic: reached unreachable code") ||
+ errorText.includes("Panic: reached unreachable code") ||
+ errorText.includes("Segmentation fault") ||
+ errorText.includes("bun has crashed")
+ ) {
+ throw new Error("Bun crashed during build");
+ }
+
+ if (DEBUG && allErrors.length) {
+ console.log("REFERENCE ERRORS OBJECT");
+ console.log("bundleErrors: {");
+ const files: any = {};
+ for (const err of allErrors) {
+ files[err.file] ??= [];
+ files[err.file].push(err);
}
- return { error, file: "<bun>" };
+ for (const [file, errs] of Object.entries(files)) {
+ console.log(' "' + file + '": [');
+ for (const err of errs as any) {
+ console.log(" `" + err.error + "`,");
+ }
+ console.log(" ],");
+ }
+ console.log("},");
}
- const [_str2, fullFilename, line, col] = source.match(/bun-build-tests\/(.*):(\d+):(\d+)/)!;
- const file = fullFilename.slice(id.length + path.basename(outBase).length + 1);
- return { error, file, line, col };
- })
- .filter(Boolean) as any[];
- if (allErrors.length === 0) {
- console.log(errorText);
+ if (expectedErrors) {
+ const errorsLeft = [...expectedErrors];
+ let unexpectedErrors = [];
+
+ for (const error of allErrors) {
+ const i = errorsLeft.findIndex(item => error.file === item.file && error.error.includes(item.error));
+ if (i === -1) {
+ unexpectedErrors.push(error);
+ } else {
+ errorsLeft.splice(i, 1);
+ }
+ }
+
+ if (unexpectedErrors.length) {
+ throw new Error(
+ "Unexpected errors reported while bundling:\n" +
+ [...unexpectedErrors].map(formatError).join("\n") +
+ "\n\nExpected errors:\n" +
+ expectedErrors.map(formatError).join("\n"),
+ );
+ }
+
+ if (errorsLeft.length) {
+ throw new Error("Errors were expected while bundling:\n" + errorsLeft.map(formatError).join("\n"));
+ }
+
+ return testRef(id, opts);
+ }
+ throw new Error("Bundle Failed\n" + [...allErrors].map(formatError).join("\n"));
+ } else if (!expectedErrors) {
+ throw new Error("Bundle Failed\n" + stderr?.toString("utf-8"));
+ }
+ return testRef(id, opts);
+ } else if (expectedErrors) {
+ throw new Error("Errors were expected while bundling:\n" + expectedErrors.map(formatError).join("\n"));
}
- if (
- errorText.includes("Crash report saved to:") ||
- errorText.includes("panic: reached unreachable code") ||
- errorText.includes("Panic: reached unreachable code") ||
- errorText.includes("Segmentation fault") ||
- errorText.includes("bun has crashed")
- ) {
- throw new Error("Bun crashed during build");
+ if (mode === "transform" && !ESBUILD) {
+ mkdirSync(path.dirname(outfile!), { recursive: true });
+ Bun.write(outfile!, stdout);
}
- if (DEBUG && allErrors.length) {
- console.log("REFERENCE ERRORS OBJECT");
- console.log("bundleErrors: {");
- const files: any = {};
- for (const err of allErrors) {
- files[err.file] ??= [];
- files[err.file].push(err);
+ // Check for warnings
+ if (!ESBUILD) {
+ const warningRegex = /^warn: (.*?)\n.*?\n\s*\^\s*\n(.*?)\n/gms;
+ const allWarnings = [...stderr!.toString("utf-8").matchAll(warningRegex)].map(([_str1, error, source]) => {
+ const [_str2, fullFilename, line, col] = source.match(/bun-build-tests\/(.*):(\d+):(\d+)/)!;
+ const file = fullFilename.slice(id.length + path.basename(outBase).length + 1);
+ return { error, file, line, col };
+ });
+ const expectedWarnings = bundleWarnings
+ ? Object.entries(bundleWarnings).flatMap(([file, v]) => v.map(error => ({ file, error })))
+ : null;
+
+ for (const err of allWarnings) {
+ warningReference[err.file] ??= [];
+ warningReference[err.file].push(err);
}
- for (const [file, errs] of Object.entries(files)) {
- console.log(' "' + file + '": [');
- for (const err of errs as any) {
- console.log(" `" + err.error + "`,");
+ if (DEBUG && allWarnings.length) {
+ console.log("REFERENCE WARNINGS OBJECT");
+ console.log("bundleWarnings: {");
+ for (const [file, errs] of Object.entries(warningReference)) {
+ console.log(' "' + file + '": [');
+ for (const err of errs as any) {
+ console.log(" `" + err.error + "`,");
+ }
+ console.log(" ],");
}
- console.log(" ],");
+ console.log("},");
}
- console.log("},");
- }
- if (expectedErrors) {
- const errorsLeft = [...expectedErrors];
- let unexpectedErrors = [];
+ if (allWarnings.length > 0 && !expectedWarnings) {
+ throw new Error("Warnings were thrown while bundling:\n" + allWarnings.map(formatError).join("\n"));
+ } else if (expectedWarnings) {
+ const warningsLeft = [...expectedWarnings];
+ let unexpectedWarnings = [];
- for (const error of allErrors) {
- const i = errorsLeft.findIndex(item => error.file === item.file && error.error.includes(item.error));
- if (i === -1) {
- unexpectedErrors.push(error);
- } else {
- errorsLeft.splice(i, 1);
+ for (const error of allWarnings) {
+ const i = warningsLeft.findIndex(item => error.file === item.file && error.error.includes(item.error));
+ if (i === -1) {
+ unexpectedWarnings.push(error);
+ } else {
+ warningsLeft.splice(i, 1);
+ }
}
- }
- if (unexpectedErrors.length) {
- throw new Error(
- "Unexpected errors reported while bundling:\n" +
- [...unexpectedErrors].map(formatError).join("\n") +
- "\n\nExpected errors:\n" +
- expectedErrors.map(formatError).join("\n"),
- );
- }
+ if (unexpectedWarnings.length) {
+ throw new Error(
+ "Unexpected warnings reported while bundling:\n" +
+ [...unexpectedWarnings].map(formatError).join("\n") +
+ "\n\nExpected warnings:\n" +
+ expectedWarnings.map(formatError).join("\n"),
+ );
+ }
- if (errorsLeft.length) {
- throw new Error("Errors were expected while bundling:\n" + errorsLeft.map(formatError).join("\n"));
+ if (warningsLeft.length) {
+ throw new Error("Warnings were expected while bundling:\n" + warningsLeft.map(formatError).join("\n"));
+ }
}
-
- return testRef(id, opts);
}
- throw new Error("Bundle Failed\n" + [...allErrors].map(formatError).join("\n"));
- } else if (!expectedErrors) {
- throw new Error("Bundle Failed\n" + stderr?.toString("utf-8"));
- }
- return testRef(id, opts);
- } else if (expectedErrors) {
- throw new Error("Errors were expected while bundling:\n" + expectedErrors.map(formatError).join("\n"));
+ } else {
+ const pluginArray = typeof plugins === "function" ? [{ name: "plugin", setup: plugins }] : plugins;
+ if (!ESBUILD) {
+ const buildOutDir = useOutFile ? path.dirname(outfile!) : outdir!;
+
+ const buildConfig = {
+ entrypoints: [...entryPaths, ...(entryPointsRaw ?? [])],
+ external,
+ minify: {
+ whitespace: minifyWhitespace,
+ identifiers: minifyIdentifiers,
+ syntax: minifySyntax,
+ },
+ naming: {
+ entrypoint: useOutFile ? path.basename(outfile!) : entryNames,
+ chunk: chunkNames,
+ },
+ plugins: pluginArray,
+ treeShaking,
+ outdir: buildOutDir,
+ sourcemap: sourceMap === true ? "external" : sourceMap || "none",
+ splitting,
+ target: platform === "neutral" ? "browser" : platform,
+ } as BuildConfig;
+
+ if (DEBUG) {
+ if (_referenceFn) {
+ const x = _referenceFn.toString().replace(/^\s*expect\(.*$/gm, "// $&");
+ const debugFile = `import path from 'path';
+ import assert from 'assert';
+ const {plugins} = (${x})({ root: ${JSON.stringify(root)} });
+ const options = ${JSON.stringify({ ...buildConfig, plugins: undefined }, null, 2)};
+ options.plugins = typeof plugins === "function" ? [{ name: "plugin", setup: plugins }] : plugins;
+ const build = await Bun.build(options);
+ if (build.logs) {
+ throw build.logs;
}
-
- if (mode === "transform" && !ESBUILD) {
- mkdirSync(path.dirname(outfile!), { recursive: true });
- Bun.write(outfile!, stdout);
+ for (const blob of build.outputs) {
+ await Bun.write(path.join(options.outdir, blob.path), blob.result);
}
-
- // Check for warnings
- let warningReference: Record<string, { file: string; error: string; line?: string; col?: string }[]> = {};
- if (!ESBUILD) {
- const warningRegex = /^warn: (.*?)\n.*?\n\s*\^\s*\n(.*?)\n/gms;
- const allWarnings = [...stderr!.toString("utf-8").matchAll(warningRegex)].map(([_str1, error, source]) => {
- const [_str2, fullFilename, line, col] = source.match(/bun-build-tests\/(.*):(\d+):(\d+)/)!;
- const file = fullFilename.slice(id.length + path.basename(outBase).length + 1);
- return { error, file, line, col };
- });
- const expectedWarnings = bundleWarnings
- ? Object.entries(bundleWarnings).flatMap(([file, v]) => v.map(error => ({ file, error })))
- : null;
-
- for (const err of allWarnings) {
- warningReference[err.file] ??= [];
- warningReference[err.file].push(err);
- }
- if (DEBUG && allWarnings.length) {
- console.log("REFERENCE WARNINGS OBJECT");
- console.log("bundleWarnings: {");
- for (const [file, errs] of Object.entries(warningReference)) {
- console.log(' "' + file + '": [');
- for (const err of errs as any) {
- console.log(" `" + err.error + "`,");
+ `;
+ writeFileSync(path.join(root, "run.js"), debugFile);
+ } else {
+ console.log("TODO: generate run.js, currently only works if options are wrapped in a function");
+ }
}
- console.log(" ],");
- }
- console.log("},");
- }
- if (allWarnings.length > 0 && !expectedWarnings) {
- throw new Error("Warnings were thrown while bundling:\n" + allWarnings.map(formatError).join("\n"));
- } else if (expectedWarnings) {
- const warningsLeft = [...expectedWarnings];
- let unexpectedWarnings = [];
+ const build = await Bun.build(buildConfig);
+ Bun.gc(true);
- for (const error of allWarnings) {
- const i = warningsLeft.findIndex(item => error.file === item.file && error.error.includes(item.error));
- if (i === -1) {
- unexpectedWarnings.push(error);
- } else {
- warningsLeft.splice(i, 1);
+ if (build.logs) {
+ console.log(build.logs);
+ throw new Error("TODO: handle build logs, but we should make this api nicer");
}
- }
- if (unexpectedWarnings.length) {
- throw new Error(
- "Unexpected warnings reported while bundling:\n" +
- [...unexpectedWarnings].map(formatError).join("\n") +
- "\n\nExpected warnings:\n" +
- expectedWarnings.map(formatError).join("\n"),
- );
- }
-
- if (warningsLeft.length) {
- throw new Error("Warnings were expected while bundling:\n" + warningsLeft.map(formatError).join("\n"));
+ for (const blob of build.outputs) {
+ await Bun.write(path.join(buildOutDir, blob.path), blob.result);
+ }
+ } else {
+ await esbuild.build({
+ bundle: true,
+ entryPoints: [...entryPaths, ...(entryPointsRaw ?? [])],
+ ...(useOutFile ? { outfile: outfile! } : { outdir: outdir! }),
+ plugins: pluginArray as any,
+ });
}
}
- }
- const readCache: Record<string, string> = {};
- const readFile = (file: string) =>
- readCache[file] || (readCache[file] = readFileSync(path.join(root, file), "utf-8"));
- const writeFile = (file: string, contents: string) => {
- readCache[file] = contents;
- writeFileSync(path.join(root, file), contents);
- };
- const api = {
- root,
- outfile: outfile!,
- outdir: outdir!,
- readFile,
- writeFile,
- expectFile: file => expect(readFile(file)),
- prependFile: (file, contents) => writeFile(file, dedent(contents) + "\n" + readFile(file)),
- appendFile: (file, contents) => writeFile(file, readFile(file) + "\n" + dedent(contents)),
- assertFileExists: file => {
- if (!existsSync(path.join(root, file))) {
- throw new Error("Expected file to be written: " + file);
- }
- },
- warnings: warningReference,
- options: opts,
- captureFile: (file, fnName = "capture") => {
- const fileContents = readFile(file);
- const regex = new RegExp(`\\b${fnName}\\s*\\(((?:\\(\\))?.*?)\\)`, "g");
- const matches = [...fileContents.matchAll(regex)];
- if (matches.length === 0) {
- throw new Error(`No ${fnName} calls found in ${file}`);
+ const readCache: Record<string, string> = {};
+ const readFile = (file: string) =>
+ readCache[file] || (readCache[file] = readFileSync(path.join(root, file), "utf-8"));
+ const writeFile = (file: string, contents: string) => {
+ readCache[file] = contents;
+ writeFileSync(path.join(root, file), contents);
+ };
+ const api = {
+ root,
+ outfile: outfile!,
+ outdir: outdir!,
+ readFile,
+ writeFile,
+ expectFile: file => expect(readFile(file)),
+ prependFile: (file, contents) => writeFile(file, dedent(contents) + "\n" + readFile(file)),
+ appendFile: (file, contents) => writeFile(file, readFile(file) + "\n" + dedent(contents)),
+ assertFileExists: file => {
+ if (!existsSync(path.join(root, file))) {
+ throw new Error("Expected file to be written: " + file);
+ }
+ },
+ warnings: warningReference,
+ options: opts,
+ captureFile: (file, fnName = "capture") => {
+ const fileContents = readFile(file);
+ const regex = new RegExp(`\\b${fnName}\\s*\\(((?:\\(\\))?.*?)\\)`, "g");
+ const matches = [...fileContents.matchAll(regex)];
+ if (matches.length === 0) {
+ throw new Error(`No ${fnName} calls found in ${file}`);
+ }
+ return matches.map(match => match[1]);
+ },
+ } 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;
}
- return matches.map(match => match[1]);
- },
- } 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/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 +
- ".",
- );
+ // 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/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));
+ }
}
- if (!ESBUILD) {
- // expect(readFileSync(outfile).toString()).toMatchSnapshot(outfile.slice(root.length));
- }
- }
- } else {
- // entryNames makes it so we cannot predict the output file
- 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));
- }
- 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 + ".");
+ } else {
+ // entryNames makes it so we cannot predict the output file
+ 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));
}
- 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 +
- ".",
- );
+ 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
}
- } 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 (
+ 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)) {
- const filepath = path.join(root, key);
- if (existsSync(filepath)) {
- const strings = Array.isArray(value) ? value : [value];
- for (const str of strings) {
- if (api.readFile(key).includes(str)) throw new Error(`Expected ${key} to not contain "${str}"`);
+ if (assertNotPresent) {
+ for (const [key, value] of Object.entries(assertNotPresent)) {
+ const filepath = path.join(root, key);
+ if (existsSync(filepath)) {
+ const strings = Array.isArray(value) ? value : [value];
+ for (const str of strings) {
+ if (api.readFile(key).includes(str)) throw new Error(`Expected ${key} to not contain "${str}"`);
+ }
}
}
}
- }
-
- if (capture) {
- const captures = api.captureFile(path.relative(root, outfile ?? outputPaths[0]));
- expect(captures).toEqual(capture);
- }
-
- // Write runtime files to disk as well as run the post bundle hook.
- for (const [file, contents] of Object.entries(runtimeFiles ?? {})) {
- mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
- writeFileSync(path.join(root, file), dedent(contents).replace(/\{\{root\}\}/g, root));
- }
-
- if (onAfterBundle) {
- onAfterBundle(api);
- }
- // check reference
- if (matchesReference) {
- const { ref } = matchesReference;
- const theirRoot = path.join(outBase, ref.id.replaceAll("/", path.sep));
- if (!existsSync(theirRoot)) {
- expectBundled(ref.id, ref.options, false, true);
- if (!existsSync(theirRoot)) {
- console.log("Expected " + theirRoot + " to exist after running reference test");
- throw new Error('Reference test "' + ref.id + '" did not succeed');
- }
+ if (capture) {
+ const captures = api.captureFile(path.relative(root, outfile ?? outputPaths[0]));
+ expect(captures).toEqual(capture);
}
- for (const file of matchesReference.files) {
- const ours = path.join(root, file);
- const theirs = path.join(theirRoot, file);
- if (!existsSync(theirs)) throw new Error(`Reference test "${ref.id}" did not write ${file}`);
- if (!existsSync(ours)) throw new Error(`Test did not write ${file}`);
+
+ // cjs2esm checks
+ if (cjs2esm) {
+ const outfiletext = api.readFile(path.relative(root, outfile ?? outputPaths[0]));
+ const regex = /\/\/\s+(.+?)\nvar\s+([a-zA-Z0-9_$]+)\s+=\s+__commonJS/g;
+ const matches = [...outfiletext.matchAll(regex)].map(match => "/" + match[1]);
+ const expectedMatches = cjs2esm === true ? [] : cjs2esm.unhandled ?? [];
try {
- expect(readFileSync(ours).toString()).toBe(readFileSync(theirs).toString());
+ expect(matches).toEqual(expectedMatches);
} catch (error) {
- console.log("Expected reference test " + ref.id + "'s " + file + " to match ours");
+ if (matches.length === expectedMatches.length) {
+ console.error(`cjs2esm check failed.`);
+ } else {
+ console.error(
+ `cjs2esm check failed. expected ${expectedMatches.length} __commonJS helpers but found ${matches.length}.`,
+ );
+ }
throw error;
}
}
- }
- // Runtime checks!
- if (run) {
- const runs = Array.isArray(run) ? run : [run];
- let i = 0;
- for (const run of runs) {
- let prefix = runs.length === 1 ? "" : `[run ${i++}] `;
-
- let file = run.file;
- if (file) {
- file = path.join(root, file);
- } else if (entryPaths.length === 1) {
- file = outfile;
- } else {
- throw new Error(prefix + "run.file is required when there is more than one entrypoint.");
- }
+ // Write runtime files to disk as well as run the post bundle hook.
+ for (const [file, contents] of Object.entries(runtimeFiles ?? {})) {
+ mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
+ writeFileSync(path.join(root, file), dedent(contents).replace(/\{\{root\}\}/g, root));
+ }
- const { success, stdout, stderr } = Bun.spawnSync({
- cmd: [
- (run.runtime ?? "bun") === "bun" ? bunExe() : "node",
- ...(run.bunArgs ?? []),
- file,
- ...(run.args ?? []),
- ] as [string, ...string[]],
- env: {
- ...bunEnv,
- FORCE_COLOR: "0",
- },
- stdio: ["ignore", "pipe", "pipe"],
- });
+ if (onAfterBundle) {
+ onAfterBundle(api);
+ }
- if (run.error) {
- if (success) {
- throw new Error(
- prefix +
- "Bundle should have thrown at runtime\n" +
- stdout!.toString("utf-8") +
- "\n" +
- stderr!.toString("utf-8"),
- );
+ // check reference
+ if (matchesReference) {
+ const { ref } = matchesReference;
+ const theirRoot = path.join(outBase, ref.id.replaceAll("/", path.sep));
+ if (!existsSync(theirRoot)) {
+ expectBundled(ref.id, ref.options, false, true);
+ if (!existsSync(theirRoot)) {
+ console.log("Expected " + theirRoot + " to exist after running reference test");
+ throw new Error('Reference test "' + ref.id + '" did not succeed');
}
+ }
+ for (const file of matchesReference.files) {
+ const ours = path.join(root, file);
+ const theirs = path.join(theirRoot, file);
+ if (!existsSync(theirs)) throw new Error(`Reference test "${ref.id}" did not write ${file}`);
+ if (!existsSync(ours)) throw new Error(`Test did not write ${file}`);
+ try {
+ expect(readFileSync(ours).toString()).toBe(readFileSync(theirs).toString());
+ } catch (error) {
+ console.log("Expected reference test " + ref.id + "'s " + file + " to match ours");
+ throw error;
+ }
+ }
+ }
- if (run.errorLineMatch) {
- // in order to properly analyze the error, we have to look backwards on stderr. this approach
- // most definetly can be improved but it works fine here.
- const stack = [];
- let error;
- const lines = stderr!
- .toString("utf-8")
- .split("\n")
- .filter(Boolean)
- .map(x => x.trim())
- .reverse();
- for (const line of lines) {
- if (line.startsWith("at")) {
- stack.push(line);
- } else {
- error = line;
- break;
- }
- }
- if (!error) {
- throw new Error(`${prefix}Runtime failed with no error. Expecting "${run.error}"`);
+ // Runtime checks!
+ if (run) {
+ const runs = Array.isArray(run) ? run : [run];
+ let i = 0;
+ for (const run of runs) {
+ let prefix = runs.length === 1 ? "" : `[run ${i++}] `;
+
+ let file = run.file;
+ if (file) {
+ file = path.join(root, file);
+ } else if (entryPaths.length === 1) {
+ file = outfile;
+ } else {
+ throw new Error(prefix + "run.file is required when there is more than one entrypoint.");
+ }
+
+ const { success, stdout, stderr } = Bun.spawnSync({
+ cmd: [
+ (run.runtime ?? "bun") === "bun" ? bunExe() : "node",
+ ...(run.bunArgs ?? []),
+ file,
+ ...(run.args ?? []),
+ ] as [string, ...string[]],
+ env: {
+ ...bunEnv,
+ FORCE_COLOR: "0",
+ },
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ if (run.error) {
+ if (success) {
+ throw new Error(
+ prefix +
+ "Bundle should have thrown at runtime\n" +
+ stdout!.toString("utf-8") +
+ "\n" +
+ stderr!.toString("utf-8"),
+ );
}
- expect(error).toBe(run.error);
if (run.errorLineMatch) {
- const stackTraceLine = stack.pop()!;
- const match = /at (.*):(\d+):(\d+)$/.exec(stackTraceLine);
- if (match) {
- const line = readFileSync(match[1], "utf-8").split("\n")[+match[2] - 1];
- if (!run.errorLineMatch.test(line)) {
- throw new Error(`${prefix}Source code "${line}" does not match expression ${run.errorLineMatch}`);
+ // in order to properly analyze the error, we have to look backwards on stderr. this approach
+ // most definetly can be improved but it works fine here.
+ const stack = [];
+ let error;
+ const lines = stderr!
+ .toString("utf-8")
+ .split("\n")
+ .filter(Boolean)
+ .map(x => x.trim())
+ .reverse();
+ for (const line of lines) {
+ if (line.startsWith("at")) {
+ stack.push(line);
+ } else {
+ error = line;
+ break;
+ }
+ }
+ if (!error) {
+ throw new Error(`${prefix}Runtime failed with no error. Expecting "${run.error}"`);
+ }
+ expect(error).toBe(run.error);
+
+ if (run.errorLineMatch) {
+ const stackTraceLine = stack.pop()!;
+ const match = /at (.*):(\d+):(\d+)$/.exec(stackTraceLine);
+ if (match) {
+ const line = readFileSync(match[1], "utf-8").split("\n")[+match[2] - 1];
+ if (!run.errorLineMatch.test(line)) {
+ throw new Error(`${prefix}Source code "${line}" does not match expression ${run.errorLineMatch}`);
+ }
+ } else {
+ throw new Error(prefix + "Could not trace error.");
}
- } else {
- throw new Error(prefix + "Could not trace error.");
}
}
+ } else if (!success) {
+ throw new Error(prefix + "Runtime failed\n" + stdout!.toString("utf-8") + "\n" + stderr!.toString("utf-8"));
}
- } else if (!success) {
- throw new Error(prefix + "Runtime failed\n" + stdout!.toString("utf-8") + "\n" + stderr!.toString("utf-8"));
- }
- if (run.stdout !== undefined) {
- const result = stdout!.toString("utf-8").trim();
- const expected = dedent(run.stdout).trim();
- if (expected !== result) {
- console.log({ file });
+ if (run.stdout !== undefined) {
+ const result = stdout!.toString("utf-8").trim();
+ const expected = dedent(run.stdout).trim();
+ if (expected !== result) {
+ console.log({ file });
+ }
+ expect(result).toBe(expected);
}
- expect(result).toBe(expected);
- }
- if (run.partialStdout !== undefined) {
- const result = stdout!.toString("utf-8").trim();
- const expected = dedent(run.partialStdout).trim();
- if (!result.includes(expected)) {
- console.log({ file });
+ if (run.partialStdout !== undefined) {
+ const result = stdout!.toString("utf-8").trim();
+ const expected = dedent(run.partialStdout).trim();
+ if (!result.includes(expected)) {
+ console.log({ file });
+ }
+ expect(result).toContain(expected);
}
- expect(result).toContain(expected);
}
}
- }
- return testRef(id, opts);
+ return testRef(id, opts);
+ })();
}
/** Shorthand for test and expectBundled. See `expectBundled` for what this does.
*/
-export function itBundled(id: string, opts: BundlerTestInput): BundlerTestRef {
+export function itBundled(
+ id: string,
+ opts: BundlerTestInput | ((metadata: BundlerTestWrappedAPI) => BundlerTestInput),
+): BundlerTestRef {
+ if (typeof opts === "function") {
+ const fn = opts;
+ opts = opts({ root: path.join(outBase, id.replaceAll("/", path.sep)) });
+ // @ts-expect-error
+ opts._referenceFn = fn;
+ }
const ref = testRef(id, opts);
const { it } = testForFile(callerSourceOrigin());
@@ -1015,16 +1145,16 @@ export function itBundled(id: string, opts: BundlerTestInput): BundlerTestRef {
}
if (opts.notImplemented) {
- try {
- expectBundled(id, opts);
- it(id, () => {
- throw new Error(
- `Test ${id} passes but was marked as "notImplemented"\nPlease remove "notImplemented: true" from this test.`,
- );
- });
- } catch (error: any) {
- if (!HIDE_SKIP) it.skip(id, () => {});
- }
+ if (!HIDE_SKIP) it.skip(id, () => {});
+ // commented out because expectBundled was made async and this is no longer possible in a sense.
+ // try {
+ // expectBundled(id, opts);
+ // it(id, () => {
+ // throw new Error(
+ // `Test ${id} passes but was marked as "notImplemented"\nPlease remove "notImplemented: true" from this test.`,
+ // );
+ // });
+ // } catch (error: any) {}
} else {
it(id, () => expectBundled(id, opts));
}
@@ -1039,22 +1169,6 @@ itBundled.skip = (id: string, opts: BundlerTestInput) => {
return testRef(id, opts);
};
-/** version of test that applies filtering */
-export function bundlerTest(id: string, cb: () => void) {
- if (FILTER && id !== FILTER) {
- return;
- }
- const { it } = testForFile(callerSourceOrigin());
- it(id, cb);
-}
-bundlerTest.skip = (id: string, cb: any) => {
- if (FILTER && id !== FILTER) {
- return;
- }
- const { it } = testForFile(callerSourceOrigin());
- if (!HIDE_SKIP) it.skip(id, cb);
-};
-
function formatError(err: { file: string; error: string; line?: string; col?: string }) {
return `${err.file}${err.line ? " :" + err.line : ""}${err.col ? ":" + err.col : ""}: ${err.error}`;
}
diff --git a/test/bundler/run-single-bundler-test.sh b/test/bundler/run-single-bundler-test.sh
index a22aee77d..389e2a399 100755
--- a/test/bundler/run-single-bundler-test.sh
+++ b/test/bundler/run-single-bundler-test.sh
@@ -6,8 +6,11 @@ if [ -z "$1" ]; then
exit 1
fi
-if [ -z "$BUN_EXE"]; then
- BUN_EXE=$(which bd 2>/dev/null || which bun 2>/dev/null)
+# using esbuild within bun-debug is extremely slow
+if [ -z "$2" ]; then
+ BUN=$(which bd 2>/dev/null || which bun-debug 2>/dev/null || which bun 2>/dev/null)
+else
+ BUN=$(which bun 2>/dev/null)
fi
__dirname="$(dirname $(realpath "$0"))"
@@ -25,14 +28,14 @@ if [ -n "$2" ]; then
fi
export FORCE_COLOR=1
-bun test bundler_ esbuild/ 2>&1 \
+$BUN test bundler_ esbuild/ 2>&1 \
| perl -ne 'print unless /^\e\[0m$/' \
- | grep -v -P '\x1b\[0m\x1b\[33m-\x1b\[2m \x1b\[0m\x1b\[2mbundler' \
- | grep -v ".test.ts:$" \
+ | grep -v -P '\x1b\[0m\x1b\[33m-\x1b\[2m \x1b\[0m\x1b\[2mbundler' --text \
+ | grep -v ".test.ts:$" --text \
| tee /tmp/run-single-bundler-test.txt \
- | grep "root:" -v
+ | grep "root:" -v --text
-symlinkDir=$(cat /tmp/run-single-bundler-test.txt | grep "root:" | cut -d " " -f 2)
+symlinkDir=$(cat /tmp/run-single-bundler-test.txt | grep "root:" --text | cut -d " " -f 2)
rm /tmp/run-single-bundler-test.txt
rm $__dirname/out -rf
if [ -e "$symlinkDir" ]; then