import assert from "assert"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); // Tests ported from: // https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_ts_test.go // For debug, all files are written to $TEMP/bun-bundle-tests/ describe("bundler", () => { itBundled("ts/DeclareConst", { files: { "/entry.ts": /* ts */ ` declare const require: any declare const exports: any; declare const module: any declare const foo: any let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); assert(!api.readFile("/out.js").includes("const foo"), 'does not include "const foo"'); }, }); itBundled("ts/DeclareLet", { files: { "/entry.ts": /* ts */ ` declare let require: any declare let exports: any; declare let module: any declare let foo: any let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); }, }); itBundled("ts/DeclareVar", { files: { "/entry.ts": /* ts */ ` declare var require: any declare var exports: any; declare var module: any declare var foo: any let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); }, }); itBundled("ts/DeclareClass", { files: { "/entry.ts": /* ts */ ` declare class require {} declare class exports {}; declare class module {} declare class foo {} let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); assert(!api.readFile("/out.js").includes("class"), 'does not include "class"'); }, }); itBundled("ts/DeclareClassFields", { files: { "/entry.ts": /* ts */ ` import './setup' import './define-false' import './define-true' `, "./setup.js": /* js */ ` globalThis.A = "global.A" globalThis.a = "global.a" globalThis.B = "global.B" globalThis.b = "global.b" globalThis.C = "global.C" globalThis.c = "global.c" globalThis.D = "global.D" globalThis.d = "global.d" `, "/define-false/index.ts": /* ts */ ` class Foo { a = 1 declare b: number [(() => null, c)] = 3 declare [(() => null, d)]: number static A = 5 static declare B: number static [(() => null, C)] = 7 static declare [(() => null, D)]: number } const props = x => JSON.stringify({ ...Object.getOwnPropertyDescriptors(x), length: undefined, prototype: undefined }) console.log('Foo ', props(Foo)) console.log('new Foo', props(new Foo())) `, "/define-true/index.ts": /* ts */ ` class Bar { a declare b [(() => null, c)] declare [(() => null, d)] static A static declare B static [(() => null, C)] static declare [(() => null, D)] } const props = x => JSON.stringify({ ...Object.getOwnPropertyDescriptors(x), length: undefined, prototype: undefined }) console.log('Bar ', props(Bar)) console.log('new Bar', props(new Bar())) `, "/define-true/tsconfig.json": /* json */ ` { "compilerOptions": { "useDefineForClassFields": true } } `, }, run: { stdout: ` Foo {"name":{"value":"Foo","writable":false,"enumerable":false,"configurable":true},"A":{"value":5,"writable":true,"enumerable":true,"configurable":true},"global.C":{"value":7,"writable":true,"enumerable":true,"configurable":true}} new Foo {"a":{"value":1,"writable":true,"enumerable":true,"configurable":true},"global.c":{"value":3,"writable":true,"enumerable":true,"configurable":true}} Bar {"name":{"value":"Bar","writable":false,"enumerable":false,"configurable":true},"A":{"writable":true,"enumerable":true,"configurable":true},"global.C":{"writable":true,"enumerable":true,"configurable":true}} new Bar {"a":{"writable":true,"enumerable":true,"configurable":true},"global.c":{"writable":true,"enumerable":true,"configurable":true}} `, }, }); itBundled("ts/DeclareFunction", { files: { "/entry.ts": /* ts */ ` declare function require(): void declare function exports(): void; declare function module(): void declare function foo() {} let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); assert(!api.readFile("/out.js").includes("function"), 'does not include "function"'); }, }); itBundled("ts/DeclareNamespace", { files: { "/entry.ts": /* ts */ ` declare namespace require {} declare namespace exports {}; declare namespace module {} declare namespace foo {} let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); assert(!api.readFile("/out.js").includes("namespace"), 'does not include "namespace"'); }, }); itBundled("ts/DeclareEnum", { files: { "/entry.ts": /* ts */ ` declare enum require {} declare enum exports {}; declare enum module {} declare enum foo {} let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); assert(!api.readFile("/out.js").includes("enum"), 'does not include "enum"'); }, }); itBundled("ts/DeclareConstEnum", { files: { "/entry.ts": /* ts */ ` declare const enum require {} declare const enum exports {}; declare const enum module {} declare const enum foo {} let foo = bar() `, }, onAfterBundle(api) { assert(api.readFile("/out.js").includes("foo = bar()"), 'includes "foo = bar()"'); assert(!api.readFile("/out.js").includes("require"), 'does not include "require"'); assert(!api.readFile("/out.js").includes("exports"), 'does not include "exports"'); assert(!api.readFile("/out.js").includes("module"), 'does not include "module"'); assert(!api.readFile("/out.js").includes("enum"), 'does not include "enum"'); assert(!api.readFile("/out.js").includes("const"), 'does not include "const"'); }, }); itBundled("ts/ConstEnumComments", { // When it comes time to implement this inlining, we may decide we do NOT // want to insert helper comments. todo: true, files: { "/bar.ts": /* ts */ ` export const enum Foo { "%/*" = 1, "*/%" = 2, } `, "/foo.ts": /* ts */ ` import { Foo } from "./bar"; const enum Bar { "%/*" = 1, "*/%" = 2, } console.log(JSON.stringify({ 'should have comments': [ Foo["%/*"], Bar["%/*"], ], 'should not have comments': [ Foo["*/%"], Bar["*/%"], ], })); `, }, entryPoints: ["/foo.ts"], onAfterBundle(api) { assert(!api.readFile("/out.js").match(/var|let|const/), "should have inlined all enum constants"); assert(!api.readFile("/out.js").match(/\*\/%/), "should not include '*/%' anywhere"); assert( [...api.readFile("/out.js").matchAll(/1\s*\/\* %\/\* \*\//g)].length === 2, "should have 2 comments for '1'", ); assert( [...api.readFile("/out.js").matchAll(/2\s*\/\*/g)].length === 0, "should have 0 comments for '2' since */ will break the comment syntax", ); }, run: { stdout: `{"should have comments":[1,1],"should not have comments":[2,2]}`, }, }); itBundled("ts/ImportEmptyNamespace", { files: { "/entry.ts": /* ts */ ` import {REMOVE} from './ns.ts' function foo(): REMOVE.type {} foo(); `, "/ns.ts": `export namespace REMOVE { type type = number }`, }, dce: true, run: true, }); itBundled("ts/ImportMissingES6", { files: { "/entry.ts": /* ts */ ` import fn, {x as a, y as b} from './foo' console.log(fn(a, b)) `, "/foo.js": `export const x = 123;`, }, bundleErrors: { "/entry.ts": [ `No matching export in "foo.js" for import "default"`, `No matching export in "foo.js" for import "y"`, ], }, }); itBundled("ts/ImportMissingUnusedES6", { files: { "/entry.ts": `import fn, {x as a, y as b} from './foo'`, "/foo.js": `export const x = 123`, }, // goal for this test is there is no error. we dont really care about the output }); itBundled("ts/ExportMissingES6", { todo: true, files: { "/entry.js": /* js */ ` import * as ns from './foo' console.log(JSON.stringify(ns)) `, // the reason this doesnt error in TS is because `nope` can be a type "/foo.ts": `export {nope} from './bar'`, "/bar.js": `export const yep = 123`, }, run: { stdout: `{}`, }, }); itBundled("ts/ImportMissingFile", { files: { "/entry.ts": /* ts */ ` import {Something} from './doesNotExist.ts' let foo = new Something `, }, bundleErrors: { "/entry.ts": [`Could not resolve: "./doesNotExist.ts"`], }, }); itBundled("ts/ImportTypeOnlyFile", { files: { "/entry.ts": /* ts */ ` import {SomeType1} from './doesNotExist1.ts' import {SomeType2} from './doesNotExist2.ts' function bar() { return 2; } let foo: SomeType1 = bar() console.log(foo); `, }, run: { stdout: "2", }, }); itBundled("ts/ExportEquals", { files: { "/a.ts": /* ts */ ` import b from './b.ts' console.log(JSON.stringify(b)) `, "/b.ts": /* ts */ ` export = [123, foo] function foo() {} `, }, run: { stdout: `[123,null]`, }, }); itBundled("ts/ExportNamespace", { files: { "/a.ts": /* ts */ ` import {Foo} from './b.ts' console.log(JSON.stringify(new Foo)) console.log(Foo.foo) console.log(Foo.bar) `, "/b.ts": /* ts */ ` export class Foo {} export namespace Foo { export let foo = 1 } export namespace Foo { export let bar = 2 } `, }, run: { stdout: `{}\n1\n2`, }, }); itBundled("ts/MinifyEnum", { todo: true, files: { "/a.ts": `enum Foo { A, B, C = Foo }\ncapture(Foo)`, // "/b.ts": `export enum Foo { X, Y, Z = Foo }`, }, entryPoints: ["/a.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, bundling: false, onAfterBundle(api) { const a = api.readFile("/out.js"); api.writeFile("/out.edited.js", a.replace(/capture\((.*?)\)/, `export const Foo = $1`)); // const b = api.readFile("/out/b.js"); // make sure the minification trick "enum[enum.K=V]=K" is used, but `enum` assert(a.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.A=0]=["']A["']/), "should be using enum minification trick (1)"); assert(a.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.B=1]=["']B["']/), "should be using enum minification trick (2)"); assert(a.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.C=[a-zA-Z$]]=["']C["']/), "should be using enum minification trick (3)"); // assert(b.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.X=0]=["']X["']/), "should be using enum minification trick (4)"); // assert(b.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.Y=1]=["']Y["']/), "should be using enum minification trick (5)"); // assert(b.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.Z=[a-zA-Z$]]=["']Z["']/), "should be using enum minification trick (6)"); }, runtimeFiles: { "/test.js": /* js */ ` import {Foo as FooA} from './out/a.edited.js' // import {Foo as FooB} from './out/b.js' import assert from 'assert'; assert.strictEqual(FooA.A, 0, 'a.ts Foo.A') assert.strictEqual(FooA.B, 1, 'a.ts Foo.B') assert.strictEqual(FooA.C, Foo, 'a.ts Foo.C') assert.strictEqual(FooA[0], 'A', 'a.ts Foo[0]') assert.strictEqual(FooA[1], 'B', 'a.ts Foo[1]') assert.strictEqual(FooA[FooA], 'C', 'a.ts Foo[Foo]') // assert.strictEqual(FooB.X, 0, 'b.ts Foo.X') // assert.strictEqual(FooB.Y, 1, 'b.ts Foo.Y') // assert.strictEqual(FooB.Z, FooB, 'b.ts Foo.Z') // assert.strictEqual(FooB[0], 'X', 'b.ts Foo[0]') // assert.strictEqual(FooB[1], 'Y', 'b.ts Foo[1]') // assert.strictEqual(FooB[FooB], 'Z', 'b.ts Foo[Foo]') `, }, }); itBundled("ts/MinifyEnumExported", { todo: true, files: { "/b.ts": `export enum Foo { X, Y, Z = Foo }`, }, entryPoints: ["/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, bundling: false, onAfterBundle(api) { const b = api.readFile("/out.js"); // make sure the minification trick "enum[enum.K=V]=K" is used, but `enum` assert(b.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.X=0]=["']X["']/), "should be using enum minification trick (4)"); assert(b.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.Y=1]=["']Y["']/), "should be using enum minification trick (5)"); assert(b.match(/\b[a-zA-Z$]\[[a-zA-Z$]\.Z=[a-zA-Z$]]=["']Z["']/), "should be using enum minification trick (6)"); }, runtimeFiles: { "/test.js": /* js */ ` import {Foo as FooB} from './out.js' import assert from 'assert'; assert.strictEqual(FooB.X, 0, 'b.ts Foo.X') assert.strictEqual(FooB.Y, 1, 'b.ts Foo.Y') assert.strictEqual(FooB.Z, FooB, 'b.ts Foo.Z') assert.strictEqual(FooB[0], 'X', 'b.ts Foo[0]') assert.strictEqual(FooB[1], 'Y', 'b.ts Foo[1]') assert.strictEqual(FooB[FooB], 'Z', 'b.ts Foo[Foo]') `, }, }); itBundled("ts/MinifyNestedEnum", { files: { "/a.ts": `function foo(arg) { enum Foo { A, B, C = Foo, D = arg } return Foo }\ncapture(foo)`, "/b.ts": `export function foo(arg) { enum Foo { X, Y, Z = Foo, W = arg } return Foo }`, }, entryPoints: ["/a.ts", "/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, onAfterBundle(api) { const a = api.readFile("/out/a.js"); api.writeFile("/out/a.edited.js", a.replace(/capture\((.*?)\)/, `export const Foo = $1`)); }, runtimeFiles: { "/test.js": /* js */ ` import {foo as fooA} from './out/a.edited.js' import {foo as fooB} from './out/b.js' import assert from 'assert'; const S = Symbol('S') const FooA = fooA(S) const FooB = fooB(S) assert.strictEqual(FooA.A, 0, 'a.ts Foo.A') assert.strictEqual(FooA.B, 1, 'a.ts Foo.B') assert.strictEqual(FooA.C, Foo, 'a.ts Foo.C') assert.strictEqual(FooA.D, S, 'a.ts Foo.D') assert.strictEqual(FooA[0], 'A', 'a.ts Foo[0]') assert.strictEqual(FooA[1], 'B', 'a.ts Foo[1]') assert.strictEqual(FooA[FooA], 'C', 'a.ts Foo[Foo]') assert.strictEqual(FooA[S], 'D', 'a.ts Foo[S]') assert.strictEqual(FooB.X, 0, 'b.ts Foo.X') assert.strictEqual(FooB.Y, 1, 'b.ts Foo.Y') assert.strictEqual(FooB.Z, FooB, 'b.ts Foo.Z') assert.strictEqual(FooB.W, S, 'b.ts Foo.W') assert.strictEqual(FooB[0], 'X', 'b.ts Foo[0]') assert.strictEqual(FooB[1], 'Y', 'b.ts Foo[1]') assert.strictEqual(FooB[FooB], 'Z', 'b.ts Foo[Foo]') assert.strictEqual(FooB[S], 'W', 'b.ts Foo[S]') `, }, }); itBundled("ts/MinifyNestedEnumNoLogicalAssignment", { files: { "/a.ts": `function foo(arg) { enum Foo { A, B, C = Foo, D = arg } return Foo }\ncapture(foo)`, "/b.ts": `export function foo(arg) { enum Foo { X, Y, Z = Foo, W = arg } return Foo }`, }, entryPoints: ["/a.ts", "/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, bundling: false, unsupportedJSFeatures: ["logical-assignment"], onAfterBundle(api) { const a = api.readFile("/out/a.js"); assert(a.includes("A"), "a should not be empty"); assert(!a.includes("||="), "a should not use logical assignment"); const b = api.readFile("/out/b.js"); assert(b.includes("X"), "b should not be empty"); assert(!b.includes("||="), "b should not use logical assignment"); }, }); itBundled("ts/MinifyNestedEnumNoArrow", { files: { "/a.ts": `function foo() { enum Foo { A, B, C = Foo } return Foo }`, "/b.ts": `export function foo() { enum Foo { X, Y, Z = Foo } return Foo }`, }, entryPoints: ["/a.ts", "/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, outdir: "/", bundling: false, unsupportedJSFeatures: ["arrow"], onAfterBundle(api) { const a = api.readFile("/a.js"); assert(a.includes("A"), "a should not be empty"); assert(!a.includes("=>"), "a should not use arrow"); const b = api.readFile("/b.js"); assert(b.includes("X"), "b should not be empty"); assert(!b.includes("=>"), "b should not use arrow"); }, }); itBundled("ts/MinifyNamespace", { files: { "/a.ts": /* ts */ ` namespace Foo { export namespace Bar { foo(Foo, Bar) } } capture(Foo) `, "/b.ts": /* ts */ ` export namespace Foo { export namespace Bar { foo(Foo, Bar) } } `, }, entryPoints: ["/a.ts", "/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, onAfterBundle(api) { api.writeFile("/out/a.edited.js", api.readFile("/out/a.js").replace(/capture\((.*?)\)/, `export const Foo = $1`)); }, runtimeFiles: { "/test.js": /* js */ ` let called = false; globalThis.foo = (a, b) => called = true; await import('./out/a.edited.js'); assert(called, 'foo should be called from a.ts'); called = false; await import('./out/b.js'); assert(called, 'foo should be called from b.ts'); `, }, }); itBundled("ts/MinifyNamespaceNoLogicalAssignment", { files: { "/a.ts": /* ts */ ` namespace Foo { export namespace Bar { foo(Foo, Bar) } } `, "/b.ts": /* ts */ ` export namespace Foo { export namespace Bar { foo(Foo, Bar) } } `, }, entryPoints: ["/a.ts", "/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, outdir: "/", bundling: false, unsupportedJSFeatures: ["logical-assignment"], onAfterBundle(api) { const a = api.readFile("/a.js"); assert(a.includes("Bar"), "a should not be empty"); assert(!a.includes("||="), "a should not use logical assignment"); const b = api.readFile("/b.js"); assert(b.includes("Bar"), "b should not be empty"); assert(!b.includes("||="), "b should not use logical assignment"); }, }); itBundled("ts/MinifyNamespaceNoArrow", { files: { "/a.ts": /* ts */ ` namespace Foo { export namespace Bar { foo(Foo, Bar) } } `, "/b.ts": /* ts */ ` export namespace Foo { export namespace Bar { foo(Foo, Bar) } } `, }, entryPoints: ["/a.ts", "/b.ts"], minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, outdir: "/", bundling: false, unsupportedJSFeatures: ["arrow"], onAfterBundle(api) { const a = api.readFile("/a.js"); assert(a.includes("foo"), "a should not be empty"); assert(!a.includes("=>"), "a should not use arrow"); const b = api.readFile("/b.js"); assert(b.includes("foo"), "b should not be empty"); assert(!b.includes("=>"), "b should not use arrow"); }, }); itBundled("ts/MinifyDerivedClass", { files: { "/entry.ts": /* ts */ ` class Foo extends Bar { foo = 1; bar = 2; constructor() { super(); foo(); bar(); } } export {Foo} `, }, minifySyntax: true, runtimeFiles: { "/test.js": /* js */ ` let calledFoo = false; let calledBar = false; globalThis.foo = () => calledFoo = true; globalThis.bar = () => calledBar = true; globalThis.Bar = class Bar { constructor() { console.log('super') this.hello = 3; } }; const {Foo} = await import('./entry.js'); import assert from 'assert'; const instance = new Foo(); console.log(instance.foo, instance.bar, instance.hello); assert(calledFoo, 'foo should be called'); assert(calledBar, 'bar should be called'); `, }, run: { file: "/test.js", stdout: "super\n1 2 3", }, }); itBundled("ts/ImportVsLocalCollisionAllTypes", { files: { "/entry.ts": /* ts */ ` import {a, b, c, d, e} from './other.ts' let a const b = 0 var c function d() { return 5; } class e { constructor() { this.prop = 2; }} console.log(JSON.stringify([a, b, c, d(), new e])) `, "/other.ts": ``, }, run: { stdout: '[null,0,null,5,{"prop":2}]', }, }); itBundled("ts/ImportVsLocalCollisionMixed", { files: { "/entry.ts": /* ts */ ` import {a, b, c, d, e, real} from './other.ts' let a const b = 0 var c function d() { return 5; } class e { constructor() { this.prop = 2; }} console.log(JSON.stringify([a, b, c, d(), new e, real])) `, "/other.ts": `export let real = 123`, }, run: { stdout: '[null,0,null,5,{"prop":2},123]', }, }); itBundled("ts/ImportEqualsEliminationTest", { todo: true, files: { "/entry.ts": /* ts */ ` import a = foo.a import b = a.b import c = b.c import x = foo.x import y = x.y import z = y.z export let bar = c `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.foo = { a: { b: { c: 123 } }, get x() { throw new Error('should not be called') } }; const {bar} = await import('./out.js'); console.log(bar); `, }, run: { file: "/test.js", stdout: "123", }, }); itBundled("ts/ImportEqualsTreeShakingFalse", { files: { "/entry.ts": /* ts */ ` import { foo } from 'pkg' import used = foo.used import unused_keep = foo.unused export { used } `, }, treeShaking: false, dce: true, bundling: false, external: ["pkg"], }); itBundled("ts/ImportEqualsTreeShakingTrue", { todo: true, files: { "/entry.ts": /* ts */ ` import { foo } from 'pkg' import used = foo.used import unused_drop = foo.unused export { used } `, }, dce: true, treeShaking: true, external: ["pkg"], bundling: false, }); itBundled("ts/ImportEqualsBundle", { todo: true, files: { "/entry.ts": /* ts */ ` import { foo } from 'pkg' import used = foo.used import unused_drop = foo.unused export { used } `, }, dce: true, treeShaking: true, external: ["pkg"], }); itBundled("ts/MinifiedBundleES6", { files: { "/entry.ts": /* ts */ ` import {foo} from './a' console.log(foo()) `, "/a.ts": /* ts */ ` export function foo() { return 123 } `, }, minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, run: { stdout: "123", }, }); itBundled("ts/MinifiedBundleCommonJS", { files: { "/entry.ts": /* ts */ ` const {foo} = require('./a') console.log(JSON.stringify([foo(), require('./j.json')])) `, "/a.ts": /* ts */ ` exports.foo = function() { return 123 } `, "/j.json": `{"test": true}`, }, minifySyntax: true, minifyWhitespace: true, minifyIdentifiers: true, run: { stdout: '[123,{"test":true}]', }, }); // TODO: all situations with decorators are currently not runtime-checked. as of writing bun crashes when hitting them at all. itBundled("ts/TypeScriptDecoratorsSimpleCase", { files: { "/entry.ts": /* ts */ ` function decorator(...args) { console.log('decorator called', JSON.stringify(args)) } @decorator class Foo { @decorator bar() { console.log('bar called') } } new Foo().bar() `, }, run: { stdout: ` decorator called [{},"bar",{"writable":true,"enumerable":false,"configurable":true}] decorator called [null] bar called `, }, }); itBundled("ts/TypeScriptDecorators", { // We still need to handle decorators with computed properties in method names todo: true, files: { "/entry.js": /* js */ ` import all from './all' import all_computed from './all_computed' import {a} from './a' import {b} from './b' import {c} from './c' import {d} from './d' import e from './e' import f from './f' import g from './g' import h from './h' import {i} from './i' import {j} from './j' import k from './k' import {fn} from './arguments' console.log(all, all_computed, a, b, c, d, e, f, g, h, i, j, k, fn) `, "/all.ts": /* ts */ ` @x.y() @new y.x() export default class Foo { @x @y mUndef @x @y mDef = 1 @x @y method(@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } @x @y declare mDecl constructor(@x0 @y0 arg0, @x1 @y1 arg1) {} @x @y static sUndef @x @y static sDef = new Foo @x @y static sMethod(@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } @x @y static declare mDecl } `, "/all_computed.ts": /* ts */ ` @x?.[_ + 'y']() @new y?.[_ + 'x']() export default class Foo { @x @y [mUndef()] @x @y [mDef()] = 1 @x @y [method()](@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } @x @y declare [mDecl()] // Side effect order must be preserved even for fields without decorators [xUndef()] [xDef()] = 2 static [yUndef()] static [yDef()] = 3 @x @y static [sUndef()] @x @y static [sDef()] = new Foo @x @y static [sMethod()](@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } @x @y static declare [mDecl()] } `, "/a.ts": /* ts */ ` @x(() => 0) @y(() => 1) class a_class { fn() { return new a_class } static z = new a_class } export let a = a_class `, "/b.ts": /* ts */ ` @x(() => 0) @y(() => 1) abstract class b_class { fn() { return new b_class } static z = new b_class } export let b = b_class `, "/c.ts": /* ts */ ` @x(() => 0) @y(() => 1) export class c { fn() { return new c } static z = new c } `, "/d.ts": /* ts */ ` @x(() => 0) @y(() => 1) export abstract class d { fn() { return new d } static z = new d } `, "/e.ts": /* ts */ ` @x(() => 0) @y(() => 1) export default class {} `, "/f.ts": /* ts */ ` @x(() => 0) @y(() => 1) export default class f { fn() { return new f } static z = new f } `, "/g.ts": /* ts */ ` @x(() => 0) @y(() => 1) export default abstract class {} `, "/h.ts": /* ts */ ` @x(() => 0) @y(() => 1) export default abstract class h { fn() { return new h } static z = new h } `, "/i.ts": /* ts */ ` class i_class { @x(() => 0) @y(() => 1) foo } export let i = i_class `, "/j.ts": /* ts */ ` export class j { @x(() => 0) @y(() => 1) foo() {} } `, "/k.ts": /* ts */ ` export default class { foo(@x(() => 0) @y(() => 1) x) {} } `, "/arguments.ts": /* ts */ ` function dec(x: any): any {} export function fn(x: string): any { class Foo { @dec(arguments[0]) [arguments[0]]() {} } return Foo; } `, }, }); itBundled("ts/TypeScriptDecoratorsKeepNames", { files: { "/entry.ts": /* ts */ ` @decoratorMustComeAfterName class Foo {} `, }, keepNames: true, }); itBundled("ts/TypeScriptDecoratorScopeESBuildIssue2147", { files: { "/entry.ts": /* ts */ ` class Foo { method1(@dec(foo) foo = 2) {} method2(@dec(() => foo) foo = 3) {} } class Bar { static x = class { static y = () => { @dec(bar) @dec(() => bar) class Baz { @dec(bar) method1() {} @dec(() => bar) method2() {} method3(@dec(() => bar) bar) {} method4(@dec(() => bar) bar) {} } return Baz } } } console.log(Foo, Bar) `, }, bundling: false, onAfterBundle(api) { const capturedCalls = api.captureFile("/out.js", "dec"); expect(capturedCalls).toEqual([ "foo", "() => foo", "bar", "() => bar", "() => bar", "() => bar", "bar", "() => bar", ]); }, }); itBundled("ts/ExportType*", { files: { "/entry.ts": /* ts */ ` export type * as Foo from "foo"; export type * from "foo"; console.log("hi"); `, }, run: { stdout: "hi\n", }, }); itBundled("ts/ExportDefaultTypeESBuildIssue316", { files: { "/entry.ts": /* ts */ ` import dc_def, { bar as dc } from './keep/declare-class' import dl_def, { bar as dl } from './keep/declare-let' import im_def, { bar as im } from './keep/interface-merged' import in_def, { bar as _in } from './keep/interface-nested' import tn_def, { bar as tn } from './keep/type-nested' import vn_def, { bar as vn } from './keep/value-namespace' import vnm_def, { bar as vnm } from './keep/value-namespace-merged' import i_def, { bar as i } from './remove/interface' import ie_def, { bar as ie } from './remove/interface-exported' import t_def, { bar as t } from './remove/type' import te_def, { bar as te } from './remove/type-exported' import ton_def, { bar as ton } from './remove/type-only-namespace' import tone_def, { bar as tone } from './remove/type-only-namespace-exported' export default [ dc_def, dc, dl_def, dl, im_def, im, in_def, _in, tn_def, tn, vn_def, vn, vnm_def, vnm, i, ie, t, te, ton, tone, ] `, "/keep/declare-class.ts": /* ts */ ` declare class foo {} export default foo export let bar = 123 `, "/keep/declare-let.ts": /* ts */ ` declare let foo: number export default foo export let bar = 123 `, "/keep/interface-merged.ts": /* ts */ ` class foo { static x = new foo } interface foo {} export default foo export let bar = 123 `, "/keep/interface-nested.ts": /* ts */ ` if (true) { interface foo {} } export default foo export let bar = 123 `, "/keep/type-nested.ts": /* ts */ ` if (true) { type foo = number } export default foo export let bar = 123 `, "/keep/value-namespace.ts": /* ts */ ` namespace foo { export let num = 0 } export default foo export let bar = 123 `, "/keep/value-namespace-merged.ts": /* ts */ ` namespace foo { export type num = number } namespace foo { export let num = 0 } export default foo export let bar = 123 `, "/remove/interface.ts": /* ts */ ` interface foo { } export default foo export let bar = 123 `, "/remove/interface-exported.ts": /* ts */ ` export interface foo { } export default foo export let bar = 123 `, "/remove/type.ts": /* ts */ ` type foo = number export default foo export let bar = 123 `, "/remove/type-exported.ts": /* ts */ ` export type foo = number export default foo export let bar = 123 `, "/remove/type-only-namespace.ts": /* ts */ ` namespace foo { export type num = number } export default foo export let bar = 123 `, "/remove/type-only-namespace-exported.ts": /* ts */ ` export namespace foo { export type num = number } export default foo export let bar = 123 `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.foo = 123456; const mod = (await import('./out.js')).default; console.log(JSON.stringify(mod)) console.log(JSON.stringify(mod.map(x => typeof x))) `, }, run: { file: "/test.js", stdout: ` [123456,123,123456,123,null,123,123456,123,123456,123,{"num":0},123,{"num":0},123,123,123,123,123,123,123] ["number","number","number","number","function","number","number","number","number","number","object","number","object","number","number","number","number","number","number","number"] `, }, }); itBundled("ts/ImplicitExtensions", { files: { "/entry.ts": /* ts */ ` import './pick-js.js' import './pick-ts.js' import './pick-jsx.jsx' import './pick-tsx.jsx' import './order-js.js' import './order-jsx.jsx' `, "/pick-js.js": `console.log("correct")`, "/pick-js.ts": `console.log("FAILED")`, "/pick-ts.jsx": `console.log("FAILED")`, "/pick-ts.ts": `console.log("correct")`, "/pick-jsx.jsx": `console.log("correct")`, "/pick-jsx.tsx": `console.log("FAILED")`, "/pick-tsx.js": `console.log("FAILED")`, "/pick-tsx.tsx": `console.log("correct")`, "/order-js.ts": `console.log("correct")`, "/order-js.tsx": `console.log("FAILED")`, "/order-jsx.ts": `console.log("correct")`, "/order-jsx.tsx": `console.log("FAILED")`, }, run: { stdout: "correct\n".repeat(6), }, }); itBundled("ts/ImplicitExtensionsMissing", { files: { "/entry.ts": /* ts */ ` import './mjs.mjs' import './cjs.cjs' import './js.js' import './jsx.jsx' `, "/mjs.ts": ``, "/mjs.tsx": ``, "/cjs.ts": ``, "/cjs.tsx": ``, "/js.ts.js": ``, "/jsx.tsx.jsx": ``, }, bundleErrors: { "/entry.ts": [ `Could not resolve: "./mjs.mjs"`, `Could not resolve: "./cjs.cjs"`, `Could not resolve: "./js.js"`, `Could not resolve: "./jsx.jsx"`, ], }, }); itBundled("ts/ExportTypeESBuildIssue379", { files: { "/entry.ts": /* ts */ ` import * as A from './a' import * as B from './b' import * as C from './c' import * as D from './d' console.log(JSON.stringify([A, B, C, D])) `, "/a.ts": /* ts */ ` type Test = Element let foo = 123 export { Test, foo } `, "/b.ts": /* ts */ ` export type Test = Element export let foo = 123 `, "/c.ts": /* ts */ ` import { Test } from './test' let foo = 123 export { Test } export { foo } `, "/d.ts": /* ts */ ` export { Test } export { foo } import { Test } from './test' let foo = 123 `, "/test.ts": `export type Test = Element`, }, run: { stdout: '[{"foo":123},{"foo":123},{"foo":123},{"foo":123}]', }, useDefineForClassFields: false, }); itBundled("ts/ThisInsideFunctionTS", { files: { "/entry.ts": /* ts */ ` function foo(x = this) { return [x, this]; } const objFoo = { foo(x = this) { return [x, this]; } } class Foo { x = this static z = 456; static y = this.z; foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(foo('bun'), ['bun', undefined]); assert.deepEqual(foo.call('this'), ['this', 'this']); assert.deepEqual(foo.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objFoo.foo('bun'), ['bun', objFoo]); assert.deepEqual(objFoo.foo(), [objFoo, objFoo]); const fooInstance = new Foo(); assert(fooInstance.x === fooInstance, 'Foo#x'); assert(Foo.y === 456, 'Foo.y'); assert.deepEqual(Foo.bar('bun'), ['bun', Foo]); assert.deepEqual(Foo.bar(), [Foo, Foo]); assert.deepEqual(fooInstance.foo(), [fooInstance, fooInstance]); assert.deepEqual(fooInstance.foo('bun'), ['bun', fooInstance]); if (nested) { function bar(x = this) { return [x, this]; } const objBar = { foo(x = this) { return [x, this]; } } class Bar { x = this static z = 456; static y = this.z foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(bar('bun'), ['bun', undefined]); assert.deepEqual(bar.call('this'), ['this', 'this']); assert.deepEqual(bar.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objBar.foo('bun'), ['bun', objBar]); assert.deepEqual(objBar.foo(), [objBar, objBar]); const barInstance = new Bar(); assert(barInstance.x === barInstance, 'Bar#x'); assert(Bar.y === 456, 'Bar.y'); assert.deepEqual(Bar.bar('bun'), ['bun', Bar]); assert.deepEqual(Bar.bar(), [Bar, Bar]); assert.deepEqual(barInstance.foo(), [barInstance, barInstance]); assert.deepEqual(barInstance.foo('bun'), ['bun', barInstance]); } `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.nested = true; globalThis.assert = (await import('assert')).default; import('./out') `, }, run: { file: "/test.js", }, }); itBundled("ts/ThisInsideFunctionTSUseDefineForClassFields", { files: { "/entry.ts": /* ts */ ` function foo(x = this) { return [x, this]; } const objFoo = { foo(x = this) { return [x, this]; } } class Foo { x = this static z = 456; static y = this.z; foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(foo('bun'), ['bun', undefined]); assert.deepEqual(foo.call('this'), ['this', 'this']); assert.deepEqual(foo.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objFoo.foo('bun'), ['bun', objFoo]); assert.deepEqual(objFoo.foo(), [objFoo, objFoo]); const fooInstance = new Foo(); assert(fooInstance.x === fooInstance, 'Foo#x'); assert(Foo.y === 456, 'Foo.y'); assert.deepEqual(Foo.bar('bun'), ['bun', Foo]); assert.deepEqual(Foo.bar(), [Foo, Foo]); assert.deepEqual(fooInstance.foo(), [fooInstance, fooInstance]); assert.deepEqual(fooInstance.foo('bun'), ['bun', fooInstance]); if (nested) { function bar(x = this) { return [x, this]; } const objBar = { foo(x = this) { return [x, this]; } } class Bar { x = this static z = 456; static y = this.z foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(bar('bun'), ['bun', undefined]); assert.deepEqual(bar.call('this'), ['this', 'this']); assert.deepEqual(bar.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objBar.foo('bun'), ['bun', objBar]); assert.deepEqual(objBar.foo(), [objBar, objBar]); const barInstance = new Bar(); assert(barInstance.x === barInstance, 'Bar#x'); assert(Bar.y === 456, 'Bar.y'); assert.deepEqual(Bar.bar('bun'), ['bun', Bar]); assert.deepEqual(Bar.bar(), [Bar, Bar]); assert.deepEqual(barInstance.foo(), [barInstance, barInstance]); assert.deepEqual(barInstance.foo('bun'), ['bun', barInstance]); } `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.nested = true; globalThis.assert = (await import('assert')).default; import('./out') `, }, run: { file: "/test.js", }, useDefineForClassFields: true, }); itBundled("ts/ThisInsideFunctionTSNoBundle", { files: { "/entry.ts": /* ts */ ` function foo(x = this) { return [x, this]; } const objFoo = { foo(x = this) { return [x, this]; } } class Foo { x = this static z = 456; static y = this.z; foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(foo('bun'), ['bun', undefined]); assert.deepEqual(foo.call('this'), ['this', 'this']); assert.deepEqual(foo.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objFoo.foo('bun'), ['bun', objFoo]); assert.deepEqual(objFoo.foo(), [objFoo, objFoo]); const fooInstance = new Foo(); assert(fooInstance.x === fooInstance, 'Foo#x'); assert(Foo.y === 456, 'Foo.y'); assert.deepEqual(Foo.bar('bun'), ['bun', Foo]); assert.deepEqual(Foo.bar(), [Foo, Foo]); assert.deepEqual(fooInstance.foo(), [fooInstance, fooInstance]); assert.deepEqual(fooInstance.foo('bun'), ['bun', fooInstance]); if (nested) { function bar(x = this) { return [x, this]; } const objBar = { foo(x = this) { return [x, this]; } } class Bar { x = this static z = 456; static y = this.z foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(bar('bun'), ['bun', undefined]); assert.deepEqual(bar.call('this'), ['this', 'this']); assert.deepEqual(bar.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objBar.foo('bun'), ['bun', objBar]); assert.deepEqual(objBar.foo(), [objBar, objBar]); const barInstance = new Bar(); assert(barInstance.x === barInstance, 'Bar#x'); assert(Bar.y === 456, 'Bar.y'); assert.deepEqual(Bar.bar('bun'), ['bun', Bar]); assert.deepEqual(Bar.bar(), [Bar, Bar]); assert.deepEqual(barInstance.foo(), [barInstance, barInstance]); assert.deepEqual(barInstance.foo('bun'), ['bun', barInstance]); } `, }, bundling: false, runtimeFiles: { "/test.js": /* js */ ` globalThis.nested = true; globalThis.assert = (await import('assert')).default; import('./out') `, }, run: { file: "/test.js", }, }); itBundled("ts/ThisInsideFunctionTSNoBundleUseDefineForClassFields", { files: { "/entry.ts": /* ts */ ` function foo(x = this) { return [x, this]; } const objFoo = { foo(x = this) { return [x, this]; } } class Foo { x = this static z = 456; static y = this.z; foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(foo('bun'), ['bun', undefined]); assert.deepEqual(foo.call('this'), ['this', 'this']); assert.deepEqual(foo.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objFoo.foo('bun'), ['bun', objFoo]); assert.deepEqual(objFoo.foo(), [objFoo, objFoo]); const fooInstance = new Foo(); assert(fooInstance.x === fooInstance, 'Foo#x'); assert(Foo.y === 456, 'Foo.y'); assert.deepEqual(Foo.bar('bun'), ['bun', Foo]); assert.deepEqual(Foo.bar(), [Foo, Foo]); assert.deepEqual(fooInstance.foo(), [fooInstance, fooInstance]); assert.deepEqual(fooInstance.foo('bun'), ['bun', fooInstance]); if (nested) { function bar(x = this) { return [x, this]; } const objBar = { foo(x = this) { return [x, this]; } } class Bar { x = this static z = 456; static y = this.z foo(x = this) { return [x, this]; } static bar(x = this) { return [x, this]; } } assert.deepEqual(bar('bun'), ['bun', undefined]); assert.deepEqual(bar.call('this'), ['this', 'this']); assert.deepEqual(bar.call('this', 'bun'), ['bun', 'this']); assert.deepEqual(objBar.foo('bun'), ['bun', objBar]); assert.deepEqual(objBar.foo(), [objBar, objBar]); const barInstance = new Bar(); assert(barInstance.x === barInstance, 'Bar#x'); assert(Bar.y === 456, 'Bar.y'); assert.deepEqual(Bar.bar('bun'), ['bun', Bar]); assert.deepEqual(Bar.bar(), [Bar, Bar]); assert.deepEqual(barInstance.foo(), [barInstance, barInstance]); assert.deepEqual(barInstance.foo('bun'), ['bun', barInstance]); } `, }, bundling: false, runtimeFiles: { "/test.js": /* js */ ` globalThis.nested = true; globalThis.assert = (await import('assert')).default; import('./out') `, }, run: { file: "/test.js", }, }); itBundled("ts/ComputedClassFieldUseDefineFalse", { todo: true, files: { "/entry.ts": /* ts */ ` class Foo { [q]; [r] = s; @dec [x]; @dec [y] = z; } export default Foo; `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.q = 'q1'; globalThis.r = 'r1'; globalThis.s = 's1'; globalThis.x = 'x1'; globalThis.y = 'y1'; globalThis.z = 'z1'; globalThis.dec = function(...args) { console.log(JSON.stringify([this, ...args])); }; const Foo = (await import('./out')).default; globalThis.q = 'q2'; globalThis.r = 'r2'; globalThis.s = 's2'; globalThis.x = 'x2'; globalThis.y = 'y2'; globalThis.z = 'z2'; const y = new Foo(); console.log(JSON.stringify(y)); `, }, useDefineForClassFields: false, bundling: false, run: { stdout: ` [null,{},"x1",null] [null,{},"y1",null] {"r1":"s2","y1":"z2"} `, file: "/test.js", }, }); itBundled("ts/ComputedClassFieldUseDefineTrue", { todo: true, files: { "/entry.ts": /* ts */ ` class Foo { [q]; [r] = s; @dec [x]; @dec [y] = z; } export default Foo; `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.q = 'q1'; globalThis.r = 'r1'; globalThis.s = 's1'; globalThis.x = 'x1'; globalThis.y = 'y1'; globalThis.z = 'z1'; globalThis.dec = function(...args) { console.log(JSON.stringify([this, ...args])); }; const Foo = (await import('./out')).default; globalThis.q = 'q2'; globalThis.r = 'r2'; globalThis.s = 's2'; globalThis.x = 'x2'; globalThis.y = 'y2'; globalThis.z = 'z2'; const y = new Foo(); console.log(JSON.stringify(y)); `, }, useDefineForClassFields: true, bundling: false, run: { stdout: ` [null,{},"x1",null] [null,{},"y1",null] {"r1":"s2","y1":"z2"} `, file: "/test.js", }, }); itBundled("ts/ComputedClassFieldUseDefineTrueLower", { files: { "/entry.ts": /* ts */ ` class Foo { [q]; [r] = s; @dec [x]; @dec [y] = z; } export default Foo; `, }, runtimeFiles: { "/test.js": /* js */ ` globalThis.q = 'q1'; globalThis.r = 'r1'; globalThis.s = 's1'; globalThis.x = 'x1'; globalThis.y = 'y1'; globalThis.z = 'z1'; globalThis.dec = function(...args) { console.log(JSON.stringify([this, ...args])); }; const Foo = (await import('./out')).default; globalThis.q = 'q2'; globalThis.r = 'r2'; globalThis.s = 's2'; globalThis.x = 'x2'; globalThis.y = 'y2'; globalThis.z = 'z2'; const y = new Foo(); console.log(JSON.stringify(y)); `, }, useDefineForClassFields: true, bundling: false, run: { stdout: ` [null,{},"x1",null] [null,{},"y1",null] {"r1":"s2","y1":"z2"} `, file: "/test.js", }, unsupportedJSFeatures: ["class-field"], }); itBundled("ts/AbstractClassFieldUseAssign", { todo: true, files: { "/entry.ts": /* ts */ ` const keepThis = Symbol('keepThis') declare const AND_REMOVE_THIS: unique symbol abstract class Foo { REMOVE_THIS: any [keepThis]: any abstract REMOVE_THIS_TOO: any abstract [AND_REMOVE_THIS]: any abstract [(x => y => x + y)('nested')('scopes_REMOVE')]: any } (() => new Foo())() `, }, dce: true, useDefineForClassFields: false, }); itBundled("ts/AbstractClassFieldUseDefine", { files: { "/entry.ts": /* ts */ ` const keepThisToo = Symbol('keepThisToo') declare const REMOVE_THIS_TOO: unique symbol abstract class Foo { keepThis: any [keepThisToo]: any abstract REMOVE_THIS: any abstract [REMOVE_THIS_TOO]: any abstract [(x => y => x + y)('nested')('scopes')]: any } (() => new Foo())() `, }, bundling: false, useDefineForClassFields: true, }); itBundled("ts/ImportMTS", { todo: true, files: { "/entry.ts": `import './imported.mjs'`, "/imported.mts": `console.log('works')`, }, run: { stdout: "works", }, }); itBundled("ts/ImportCTS", { files: { "/entry.ts": `require('./required.cjs')`, "/required.cjs": `console.log('works')`, }, run: { stdout: "works", }, }); itBundled("ts/SideEffectsFalseWarningTypeDeclarations", { files: { "/entry.ts": /* ts */ ` import "some-js" import "some-ts" import "empty-js" import "empty-ts" import "empty-dts" `, "/node_modules/some-js/package.json": `{ "main": "./foo.js", "sideEffects": false }`, "/node_modules/some-js/foo.js": `console.log('foo')`, "/node_modules/some-ts/package.json": `{ "main": "./foo.ts", "sideEffects": false }`, "/node_modules/some-ts/foo.ts": `console.log('foo' as string)`, "/node_modules/empty-js/package.json": `{ "main": "./foo.js", "sideEffects": false }`, "/node_modules/empty-js/foo.js": ``, "/node_modules/empty-ts/package.json": `{ "main": "./foo.ts", "sideEffects": false }`, "/node_modules/empty-ts/foo.ts": `export type Foo = number`, "/node_modules/empty-dts/package.json": `{ "main": "./foo.d.ts", "sideEffects": false }`, "/node_modules/empty-dts/foo.d.ts": `export type Foo = number`, }, onAfterBundle(api) { expect(api.readFile("/out.js").trim()).toBe(""); }, }); itBundled("ts/SiblingNamespaceLet", { todo: true, files: { "/let.ts": /* ts */ ` export namespace x { export let y = 123 } export namespace x { export let z = y } `, }, entryPoints: ["/let.ts"], bundling: false, runtimeFiles: { "/test.js": /* js */ ` import assert from 'assert' const m = (await import('./out.js')).x assert(m.x === m.z, "it worked") `, }, }); itBundled("ts/SiblingNamespaceFunction", { todo: true, files: { "/function.ts": /* ts */ ` export namespace x { export function y() {} } export namespace x { export let z = y } `, }, entryPoints: ["/function.ts"], bundling: false, runtimeFiles: { "/test.js": /* js */ ` import assert from 'assert' const m = (await import('./out.js')).x assert(m.x === m.z, "it worked worked") `, }, }); itBundled("ts/SiblingNamespaceClass", { todo: true, files: { "/let.ts": /* ts */ ` export namespace x { export class y {} } export namespace x { export let z = y } `, }, entryPoints: ["/function.ts"], bundling: false, runtimeFiles: { "/test.js": /* js */ ` import assert from 'assert' const m = (await import('./out.js')).x assert(m.x === m.z, "it worked worked") `, }, }); itBundled("ts/SiblingNamespaceNamespace", { todo: true, files: { "/namespace.ts": /* ts */ ` export namespace x { export namespace y { 0 } } export namespace x { export let z = y } `, }, entryPoints: ["/namespace.ts"], bundling: false, runtimeFiles: { "/test.js": /* js */ ` import assert from 'assert' const m = (await import('./out.js')).x assert(m.x === m.z, "it worked worked") `, }, }); itBundled("ts/SiblingNamespaceEnum", { todo: true, files: { "/enum.ts": /* ts */ ` export namespace x { export enum y {} } export namespace x { export let z = y } `, }, entryPoints: ["/enum.ts"], bundling: false, runtimeFiles: { "/test.js": /* js */ ` import assert from 'assert' const m = (await import('./out.js')).x assert(m.x === m.z, "it worked.ts worked") `, }, }); itBundled("ts/SiblingEnum", { todo: true, // GENERATED files: { "/number.ts": /* ts */ ` (0, eval)('globalThis.y = 1234'); (0, eval)('globalThis.z = 2345'); export enum x { y, yy = y } export enum x { z = y + 1 } declare let y: any, z: any export namespace x { console.log(y, z) } console.log(x.y, x.z) `, "/string.ts": /* ts */ ` (0, eval)('globalThis.y = 1234'); (0, eval)('globalThis.z = 2345'); export enum x { y = 'a', yy = y } export enum x { z = y } declare let y: any, z: any export namespace x { console.log(y, z) } console.log(x.y, x.z) `, "/propagation.ts": /* ts */ ` export enum a { b = 100 } export enum x { c = a.b, d = c * 2, e = x.d ** 2, f = x['e'] / 4, } export enum x { g = f >> 4 } console.log(a.b, a['b'], x.g, x['g']) `, "/nested-number.ts": /* ts */ ` (0, eval)('globalThis.y = 1234'); (0, eval)('globalThis.z = 2345'); export namespace foo { export enum x { y, yy = y } } export namespace foo { export enum x { z = y + 1 } } declare let y: any, z: any export namespace foo.x { console.log(y, z) console.log(x.y, x.z) } `, "/nested-string.ts": /* ts */ ` (0, eval)('globalThis.y = 1234'); (0, eval)('globalThis.z = 2345'); export namespace foo { export enum x { y = 'a', yy = y } } export namespace foo { export enum x { z = y } } declare let y: any, z: any export namespace foo.x { console.log(y, z) console.log(x.y, x.z) } `, "/nested-propagation.ts": /* ts */ ` export namespace n { export enum a { b = 100 } } export namespace n { export enum x { c = n.a.b, d = c * 2, e = x.d ** 2, f = x['e'] / 4, } } export namespace n { export enum x { g = f >> 4 } console.log(a.b, n.a.b, n['a']['b'], x.g, n.x.g, n['x']['g']) } `, }, entryPoints: [ "/number.ts", "/string.ts", "/propagation.ts", "/nested-number.ts", "/nested-string.ts", "/nested-propagation.ts", ], run: [ { file: "/out/number.js", stdout: "1234 2345\n0 1" }, { file: "/out/string.js", stdout: "1234 2345\na a" }, { file: "/out/propagation.js", stdout: "100 100 625 625" }, { file: "/out/nested-number.js", stdout: "1234 2345\n0 1" }, { file: "/out/nested-string.js", stdout: "1234 2345\na a" }, { file: "/out/nested-propagation.js", stdout: "100 100 100 625 625 625" }, ], }); itBundled("ts/EnumTreeShaking", { todo: true, files: { "/simple-member.ts": /* ts */ ` enum x_DROP { y_DROP = 123 } console.log(x_DROP.y_DROP) `, "/simple-enum.ts": /* ts */ ` enum x { y = 123 } console.log(JSON.stringify(x)) `, "/sibling-member.ts": /* ts */ ` enum drop_x { drop_y = 123 } enum drop_x { drop_z = drop_y * 2 } console.log(drop_x.drop_y, drop_x.drop_z) `, "/sibling-enum-before.ts": /* ts */ ` console.log(x) enum x { y = 123 } enum x { z = y * 2 } `, "/sibling-enum-middle.ts": /* ts */ ` enum x { y = 123 } console.log(JSON.stringify(x)) enum x { z = y * 2 } `, "/sibling-enum-after.ts": /* ts */ ` enum x { y = 123 } enum x { z = y * 2 } console.log(JSON.stringify(x)) `, "/namespace-before.ts": /* ts */ ` (0, eval)('globalThis.y = 1234'); (0, eval)('globalThis.x = 2345'); namespace x { console.log(x, y) } enum x { y = 123 } `, "/namespace-after.ts": /* ts */ ` (0, eval)('globalThis.y = 1234'); (0, eval)('globalThis.x = 2345'); enum x { y = 123 } namespace x { console.log(JSON.stringify(x), y) } `, }, entryPoints: [ "/simple-member.ts", "/simple-enum.ts", "/sibling-member.ts", "/sibling-enum-before.ts", "/sibling-enum-middle.ts", "/sibling-enum-after.ts", "/namespace-before.ts", "/namespace-after.ts", ], dce: true, run: [ { file: "/out/simple-member.js", stdout: "123" }, { file: "/out/simple-enum.js", stdout: '{"123":"y","y":123}' }, { file: "/out/sibling-member.js", stdout: "123 246" }, { file: "/out/sibling-enum-before.js", stdout: "undefined" }, { file: "/out/sibling-enum-middle.js", stdout: '{"123":"y","y":123}' }, { file: "/out/sibling-enum-after.js", stdout: '{"123":"y","246":"z","y":123,"z":246}' }, { file: "/out/namespace-before.js", stdout: "{} 1234" }, { file: "/out/namespace-after.js", stdout: '{"123":"y","y":123} 1234' }, ], }); itBundled("ts/EnumJSX", { todo: true, files: { "/element.tsx": /* tsx */ ` import { create } from 'not-react' export enum Foo { Div = 'div' } console.log(JSON.stringify()) `, "/fragment.tsx": /* tsx */ ` import { create } from 'not-react' export enum React { Fragment = 'div' } console.log(JSON.stringify(<>test)) `, "/nested-element.tsx": /* tsx */ ` import { create } from 'not-react' namespace x.y { export enum Foo { Div = 'div' } } namespace x.y { console.log(JSON.stringify()) } `, "/nested-fragment.tsx": /* tsx */ ` import { create } from 'not-react' namespace x.y { export enum React { Fragment = 'div' } } namespace x.y { console.log(JSON.stringify(<>test)) } `, }, entryPoints: ["/element.tsx", "/fragment.tsx", "/nested-element.tsx", "/nested-fragment.tsx"], outputPaths: ["/out/element.js", "/out/fragment.js", "/out/nested-element.js", "/out/nested-fragment.js"], external: ["*"], jsx: { runtime: "classic", factory: "create", }, runtimeFiles: { "/node_modules/not-react/index.js": /* js */ ` export const create = (tag, props, ...children) => [tag, props, children] `, }, run: [ { file: "/out/element.js", stdout: '["div",null,[]]' }, { file: "/out/fragment.js", stdout: '["div",null,["test"]]' }, { file: "/out/nested-element.js", stdout: '["div",null,[]]' }, { file: "/out/nested-fragment.js", stdout: '["div",null,["test"]]' }, ], }); itBundled("ts/EnumDefine", { todo: true, files: { "/entry.ts": ` enum a { b = 123, c = d } console.log(a.b, a.c) `, }, define: { d: "b", }, run: { stdout: "123 123" }, }); itBundled("ts/EnumSameModuleInliningAccess", { todo: true, files: { "/entry.ts": /* ts */ ` enum a_drop { x = 123 } enum b_drop { x = 123 } enum c { x = 123 } enum d { x = 123 } enum e { x = 123 } console.log(JSON.stringify([ a_drop.x, b_drop['x'], c?.x, d?.['x'], e, ])) `, }, dce: true, run: { stdout: '[123,123,123,123,{"123":"x","x":123}]' }, }); itBundled("ts/EnumCrossModuleInliningAccess", { todo: true, files: { "/entry.ts": /* ts */ ` import { drop_a, drop_b, c, d, e } from './enums' console.log(JSON.stringify([ drop_a.x, drop_b['x'], c?.x, d?.['x'], e, ])) `, "/enums.ts": /* ts */ ` export enum drop_a { x = 123 } export enum drop_b { x = 123 } export enum c { x = 123 } export enum d { x = 123 } export enum e { x = 123 } `, }, dce: true, }); itBundled("ts/EnumCrossModuleInliningDefinitions", { todo: true, files: { "/entry.ts": /* ts */ ` import { a } from './enums' (0, eval)('globalThis.capture = x => x'); console.log(JSON.stringify([ capture(a.implicit_number), capture(a.explicit_number), capture(a.explicit_string), a.non_constant, ])) `, "/enums.ts": /* ts */ ` (0, eval)('globalThis.foo = 321'); export enum a { implicit_number, explicit_number = 123, explicit_string = 'xyz', non_constant = foo, } `, }, onAfterBundle(api) { expect(api.captureFile("/out.js").map(x => x.replace(/\/\*.*\*\//g, "").trim())).toEqual(["0", "123", '"xyz"']); }, }); itBundled("ts/EnumCrossModuleInliningReExport", { todo: true, files: { "/entry.js": /* js */ ` import { a } from './re-export' import { b } from './re-export-star' import * as ns from './enums' console.log([ capture(a.x), capture(b.x), capture(ns.c.x), ]) `, "/re-export.js": `export { a } from './enums'`, "/re-export-star.js": `export * from './enums'`, "/enums.ts": /* ts */ ` export enum a { x = 'a' } export enum b { x = 'b' } export enum c { x = 'c' } `, }, onAfterBundle(api) { expect(api.captureFile("/out.js").map(x => x.replace(/\/\*.*\*\//g, "").trim())).toEqual(['"a"', '"b"', '"c"']); }, }); itBundled("ts/EnumCrossModuleTreeShaking", { todo: true, files: { "/entry.ts": /* ts */ ` import { a_DROP, b_DROP, c_DROP, } from './enums' console.log([ capture(a_DROP.x), capture(b_DROP['x']), capture(c_DROP.x), ]) import { a, b, c, d, e } from './enums' console.log([ capture(a.x), capture(b.x), capture(c), capture(d.y), capture(e.x), ]) `, "/enums.ts": /* ts */ ` export enum a_DROP { x = 1 } // test a dot access export enum b_DROP { x = 2 } // test an index access export enum c_DROP { x = '' } // test a string enum export enum a { x = false } // false is not inlinable export enum b { x = foo } // foo has side effects export enum c { x = 3 } // this enum object is captured export enum d { x = 4 } // we access "y" on this object export let e = {} // non-enum properties should be kept `, }, onAfterBundle(api) { expect(api.captureFile("/out.js").map(x => x.replace(/\/\*.*\*\//g, "").trim())).toEqual([ "1", "2", '""', "a.x", "b.x", "c", "d.y", "e.x", ]); }, }); itBundled("ts/EnumExportClause", { todo: true, files: { "/entry.ts": /* ts */ ` import { A, B, C as c, d as dd, } from './enums' console.log([ capture(A.A), capture(B.B), capture(c.C), capture(dd.D), ]) `, "/enums.ts": /* ts */ ` export enum A { A = 1 } enum B { B = 2 } export enum C { C = 3 } enum D { D = 4 } export { B, D as d } `, }, onAfterBundle(api) { expect(api.captureFile("/out.js").map(x => x.replace(/\/\*.*\*\//g, "").trim())).toEqual(["1", "2", "3", "4"]); }, }); // itBundled("ts/CommonJSVariableInESMTypeModule", { // // GENERATED // files: { // "/entry.ts": `module.exports = null`, // "/package.json": `{ "type": "module" }`, // }, // /* TODO FIX expectedScanLog: `entry.ts: WARNING: The CommonJS "module" variable is treated as a global variable in an ECMAScript module and may not work as expected // package.json: NOTE: This file is considered to be an ECMAScript module because the enclosing "package.json" file sets the type of this file to "module": // NOTE: Node's package format requires that CommonJS files in a "type": "module" package use the ".cjs" file extension. If you are using TypeScript, you can use the ".cts" file extension with esbuild instead. // `, */ // }); itBundled("ts/EnumRulesFrom_TypeScript_5_0", { // GENERATED files: { "/supported.ts": ` // From https://github.com/microsoft/TypeScript/pull/50528: // "An expression is considered a constant expression if it is const enum DROP { // a number or string literal, X0 = 123, X1 = 'x', // a unary +, -, or ~ applied to a numeric constant expression, X2 = +1, X3 = -2, X4 = ~3, // a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions, X5 = 1 + 2, X6 = 1 - 2, X7 = 2 * 3, X8 = 1 / 2, X9 = 3 % 2, X10 = 2 ** 3, X11 = 1 << 2, X12 = -9 >> 1, X13 = -9 >>> 1, X14 = 5 | 12, X15 = 5 & 12, X16 = 5 ^ 12, // a binary + applied to two constant expressions whereof at least one is a string, X17 = 'x' + 0, X18 = 0 + 'x', X19 = 'x' + 'y', X20 = '' + NaN, X21 = '' + Infinity, X22 = '' + -Infinity, X23 = '' + -0, // a template expression where each substitution expression is a constant expression, X24 = ` + "`A${0}B${'x'}C${1 + 3 - 4 / 2 * 5 ** 6}D`" + `, // a parenthesized constant expression, X25 = (321), // a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation, /* (we don't implement this one) */ // a dotted name that references an enum member with an enum literal type, or X26 = X0, X27 = X0 + 'x', X28 = 'x' + X0, X29 = ` + "`a${X0}b`" + `, X30 = DROP.X0, X31 = DROP.X0 + 'x', X32 = 'x' + DROP.X0, X33 = ` + "`a${DROP.X0}b`" + `, // a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type." X34 = X1, X35 = X1 + 'y', X36 = 'y' + X1, X37 = ` + "`a${X1}b`" + `, X38 = DROP['X1'], X39 = DROP['X1'] + 'y', X40 = 'y' + DROP['X1'], X41 = ` + "`a${DROP['X1']}b`" + `, } console.log(JSON.stringify([ // a number or string literal, DROP.X0, DROP.X1, // a unary +, -, or ~ applied to a numeric constant expression, DROP.X2, DROP.X3, DROP.X4, // a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions, DROP.X5, DROP.X6, DROP.X7, DROP.X8, DROP.X9, DROP.X10, DROP.X11, DROP.X12, DROP.X13, DROP.X14, DROP.X15, DROP.X16, // a template expression where each substitution expression is a constant expression, DROP.X17, DROP.X18, DROP.X19, DROP.X20, DROP.X21, DROP.X22, DROP.X23, // a template expression where each substitution expression is a constant expression, DROP.X24, // a parenthesized constant expression, DROP.X25, // a dotted name that references an enum member with an enum literal type, or DROP.X26, DROP.X27, DROP.X28, DROP.X29, DROP.X30, DROP.X31, DROP.X32, DROP.X33, // a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type." DROP.X34, DROP.X35, DROP.X36, DROP.X37, DROP.X38, DROP.X39, DROP.X40, DROP.X41, ])) `, "/not-supported.ts": /* ts */ ` (0, eval)('globalThis.capture = x => x'); const enum NonIntegerNumberToString { SUPPORTED = '' + 1, UNSUPPORTED = '' + 1.5, } console.log( capture(NonIntegerNumberToString.SUPPORTED), capture(NonIntegerNumberToString.UNSUPPORTED), ) const enum OutOfBoundsNumberToString { SUPPORTED = '' + 1_000_000_000, UNSUPPORTED = '' + 1_000_000_000_000, } console.log( capture(OutOfBoundsNumberToString.SUPPORTED), capture(OutOfBoundsNumberToString.UNSUPPORTED), ) const enum TemplateExpressions { // TypeScript enums don't handle any of these NULL = '' + null, TRUE = '' + true, FALSE = '' + false, BIGINT = '' + 123n, } console.log( capture(TemplateExpressions.NULL), capture(TemplateExpressions.TRUE), capture(TemplateExpressions.FALSE), capture(TemplateExpressions.BIGINT), ) `, }, // dce: true, entryPoints: ["/supported.ts", "/not-supported.ts"], run: [ { file: "/out/supported.js", stdout: '[123,"x",1,-2,-4,3,-1,6,0.5,1,8,4,-5,2147483643,13,4,9,"x0","0x","xy","NaN","Infinity","-Infinity","0","A0BxC-31246D",321,123,"123x","x123","a123b",123,"123x","x123","a123b","x","xy","yx","axb","x","xy","yx","axb"]', }, { file: "/out/not-supported.js", stdout: ` 1 1.5 1000000000 1000000000000 null true false 123 `, }, ], onAfterBundle(api) { // expect(api.captureFile("/out/not-supported.js").map(x => x.replace(/\/\*.*\*\//g, "").trim())).toEqual([ // '"1"', // "NonIntegerNumberToString.UNSUPPORTED", // '"1000000000"', // "OutOfBoundsNumberToString.UNSUPPORTED", // "TemplateExpressions.NULL", // "TemplateExpressions.TRUE", // "TemplateExpressions.FALSE", // "TemplateExpressions.BIGINT", // ]); }, }); itBundled("ts/EnumUseBeforeDeclare", { todo: true, files: { "/entry.ts": /* ts */ ` before(); after(); export function before() { console.log(JSON.stringify(Foo), Foo.FOO) } enum Foo { FOO } export function after() { console.log(JSON.stringify(Foo), Foo.FOO) } before(); after(); `, }, run: { stdout: ` undefined 0 undefined 0 {"0":"FOO","FOO":0} 0 {"0":"FOO","FOO":0} 0 `, }, }); });