From 0846a4fa809430a77f1284a7a526a946007484e0 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Tue, 25 Apr 2023 22:13:39 -0400 Subject: bundler tests, testing plugins (#2740) * add cjs2esm stuff * tests * plugin testing --- test/bundler/bundler_browser.test.ts | 5 +- test/bundler/bundler_cjs2esm.test.ts | 4 +- test/bundler/bundler_edgecase.test.ts | 2 +- test/bundler/bundler_minify.test.ts | 3 +- test/bundler/bundler_plugin.test.ts | 393 +++++++++ test/bundler/esbuild/css.test.ts | 2 +- test/bundler/esbuild/dce.test.ts | 3 +- test/bundler/esbuild/default.test.ts | 9 +- test/bundler/esbuild/importstar.test.ts | 2 +- test/bundler/esbuild/importstar_ts.test.ts | 2 +- test/bundler/esbuild/loader.test.ts | 2 +- test/bundler/esbuild/lower.test.ts | 2 +- test/bundler/esbuild/packagejson.test.ts | 2 +- test/bundler/esbuild/ts.test.ts | 2 +- test/bundler/esbuild/tsconfig.test.ts | 2 +- test/bundler/expectBundled.md | 2 +- test/bundler/expectBundled.ts | 1276 +++++++++++++++------------- test/bundler/run-single-bundler-test.sh | 17 +- 18 files changed, 1117 insertions(+), 613 deletions(-) create mode 100644 test/bundler/bundler_plugin.test.ts 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; 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); // pass subprocess.env env?: Record; @@ -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 { 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 = {}; + 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: "" }; + } + 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: "" }; + 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 = {}; - 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 = {}; - 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 = {}; + 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 = 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 = 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 -- cgit v1.2.3