5536 lines
230 KiB
JavaScript
5536 lines
230 KiB
JavaScript
import { describe, expect, it } from "bun:test";
|
|||
|
|
import { bunEnv, bunExe, hideFromStackTrace, tempDir } from "harness";
|
||
|
|
import { join } from "path";
|
||
|
|
|
||
|
|
describe("Bun.Transpiler", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
user_undefined: "undefined",
|
||
|
|
user_nested: "location.origin",
|
||
|
|
"hello.earth": "hello.mars",
|
||
|
|
"Math.log": "console.error",
|
||
|
|
},
|
||
|
|
macro: {
|
||
|
|
react: {
|
||
|
|
bacon: `${import.meta.dir}/macro-check.js`,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
platform: "browser",
|
||
|
|
});
|
||
|
|
const transpilerMinifySyntax = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
user_undefined: "undefined",
|
||
|
|
user_nested: "location.origin",
|
||
|
|
"hello.earth": "hello.mars",
|
||
|
|
"Math.log": "console.error",
|
||
|
|
},
|
||
|
|
macro: {
|
||
|
|
react: {
|
||
|
|
bacon: `${import.meta.dir}/macro-check.js`,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
minify: { syntax: true },
|
||
|
|
platform: "browser",
|
||
|
|
});
|
||
|
|
|
||
|
|
const ts = {
|
||
|
|
parsed: (code, trim = true, autoExport = false, minify = false) => {
|
||
|
|
if (autoExport) {
|
||
|
|
code = "export default (" + code + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
var out = (minify ? transpilerMinifySyntax : transpiler).transformSync(code, "ts");
|
||
|
|
if (autoExport && out.startsWith("export default ")) {
|
||
|
|
out = out.substring("export default ".length);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (trim) {
|
||
|
|
out = out.trim();
|
||
|
|
|
||
|
|
if (out.endsWith(";")) {
|
||
|
|
out = out.substring(0, out.length - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
return out.trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
return out;
|
||
|
|
},
|
||
|
|
|
||
|
|
parsedMin: (code, trim = true, autoExport = false) => {
|
||
|
|
return ts.parsed(code, trim, autoExport, true);
|
||
|
|
},
|
||
|
|
|
||
|
|
expectPrinted: (code, out) => {
|
||
|
|
expect(ts.parsed(code, true, true)).toBe(out);
|
||
|
|
},
|
||
|
|
|
||
|
|
expectPrinted_: (code, out) => {
|
||
|
|
expect(ts.parsed(code, !out.endsWith(";\n"), false)).toBe(out);
|
||
|
|
},
|
||
|
|
|
||
|
|
transpiledOutput: code => {
|
||
|
|
return ts.parsed(code, false, false);
|
||
|
|
},
|
||
|
|
|
||
|
|
expectPrintedMin_: (code, out) => {
|
||
|
|
expect(ts.parsedMin(code, !out.endsWith(";\n"), false)).toBe(out);
|
||
|
|
},
|
||
|
|
|
||
|
|
expectParseError: (code, message) => {
|
||
|
|
try {
|
||
|
|
ts.parsed(code, false, false);
|
||
|
|
} catch (er) {
|
||
|
|
var err = er;
|
||
|
|
if (er instanceof AggregateError) {
|
||
|
|
err = er.errors[0];
|
||
|
|
}
|
||
|
|
expect(err.message).toBe(message);
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("Expected parse error for code\n\t" + code);
|
||
|
|
},
|
||
|
|
};
|
||
|
|
hideFromStackTrace(ts.expectPrinted_);
|
||
|
|
hideFromStackTrace(ts.expectPrinted);
|
||
|
|
hideFromStackTrace(ts.expectParseError);
|
||
|
|
|
||
|
|
it("handles errors when parsing macros", () => {
|
||
|
|
expect(() => {
|
||
|
|
new Bun.Transpiler({ macro: "hi" });
|
||
|
|
}).toThrow("Unexpected hi");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("normalizes \\r\\n", () => {
|
||
|
|
ts.expectPrinted_("console.log(`\r\n\r\n\r\n`)", "console.log(`\n\n\n`);\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("doesn't hang indefinitely #2746", () => {
|
||
|
|
// this test passes by not hanging
|
||
|
|
expect(() => {
|
||
|
|
console.log("1");
|
||
|
|
const y = transpiler.transformSync(`
|
||
|
|
class Test {
|
||
|
|
test() {
|
||
|
|
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
console.error(y);
|
||
|
|
}).toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("property access inlining", () => {
|
||
|
|
it("bails out with spread", () => {
|
||
|
|
ts.expectPrintedMin_("const a = [...b][0];", "const a = [...b][0]");
|
||
|
|
ts.expectPrintedMin_("const a = {...b}[0];", "const a = { ...b }[0]");
|
||
|
|
});
|
||
|
|
it("bails out with multiple items", () => {
|
||
|
|
ts.expectPrintedMin_("const a = [b, c][0];", "const a = [b, c][0]");
|
||
|
|
});
|
||
|
|
it("works", () => {
|
||
|
|
ts.expectPrintedMin_('const a = ["hey"][0];', 'const a = "hey"');
|
||
|
|
});
|
||
|
|
it("works nested", () => {
|
||
|
|
ts.expectPrintedMin_('const a = ["hey"][0][0];', 'const a = "h"');
|
||
|
|
});
|
||
|
|
it("bails out when the array item is an optional chain", () => {
|
||
|
|
// Folding `[a?.b][0]` to `a?.b` is unsafe when the result lands as the
|
||
|
|
// target of a surrounding optional-chain continuation: the two chains
|
||
|
|
// would be spliced into one. `[[a?.b]][0]?.[0].c` must not become
|
||
|
|
// `a?.b.c`, which short-circuits to `undefined` for `a == null` instead
|
||
|
|
// of throwing on the trailing `.c`.
|
||
|
|
ts.expectPrintedMin_("x = [[a?.b]][0]?.[0].c", "x = [a?.b]?.[0].c");
|
||
|
|
ts.expectPrintedMin_("x = [[a?.b]][0]?.[0]()", "x = [a?.b]?.[0]()");
|
||
|
|
ts.expectPrintedMin_("x = [[a?.b]][0]?.[0][c]", "x = [a?.b]?.[0][c]");
|
||
|
|
ts.expectPrintedMin_("x = ({ f: [a?.b] }).f?.[0].c", "x = [a?.b]?.[0].c");
|
||
|
|
ts.expectPrintedMin_("x = [[a?.[b]]][0]?.[0].c", "x = [a?.[b]]?.[0].c");
|
||
|
|
ts.expectPrintedMin_("x = [[a?.()]][0]?.[0].c", "x = [a?.()]?.[0].c");
|
||
|
|
// Continuation (not Start) on the inlined item's outermost node:
|
||
|
|
ts.expectPrintedMin_("x = [[a?.b.c]][0]?.[0].d", "x = [a?.b.c]?.[0].d");
|
||
|
|
ts.expectPrintedMin_("x = [[a?.b[c]]][0]?.[0].d", "x = [a?.b[c]]?.[0].d");
|
||
|
|
ts.expectPrintedMin_("x = [[a?.b()]][0]?.[0].d", "x = [a?.b()]?.[0].d");
|
||
|
|
// Multi-item path (expr_can_be_removed_if_unused): a @__PURE__ optional
|
||
|
|
// call is removable, so the second fold arm sees it.
|
||
|
|
ts.expectPrintedMin_("x = [[0, /* @__PURE__ */ a?.()]][0]?.[1].c", "x = [0, a?.()]?.[1].c");
|
||
|
|
ts.expectPrintedMin_("x = [0, /* @__PURE__ */ a?.()][1]", "x = [0, a?.()][1]");
|
||
|
|
|
||
|
|
// The outer `?.` on an array literal is dropped at parse time, so these
|
||
|
|
// reach the fold with `optional_chain == None` on the index and the
|
||
|
|
// printer adds the `(a?.b)` wrapper itself. Keep bailing on the fold so
|
||
|
|
// the wrapper isn't load-bearing.
|
||
|
|
ts.expectPrintedMin_("x = [a?.b][0]", "x = [a?.b][0]");
|
||
|
|
ts.expectPrintedMin_("x = [a?.b]?.[0]", "x = [a?.b][0]");
|
||
|
|
ts.expectPrintedMin_("x = [a?.b][0].c", "x = [a?.b][0].c");
|
||
|
|
ts.expectPrintedMin_("x = [a?.b]?.[0].c", "x = [a?.b][0].c");
|
||
|
|
ts.expectPrintedMin_("x = [a?.b][0]()", "x = [a?.b][0]()");
|
||
|
|
|
||
|
|
// Same bailout protects LHS / delete / new / tagged-template positions
|
||
|
|
// from becoming `a?.b = v` / `delete a?.b` / `new a?.b()`.
|
||
|
|
ts.expectPrintedMin_("[a?.b][0] = v", "[a?.b][0] = v");
|
||
|
|
ts.expectPrintedMin_("delete [a?.b][0]", "delete [a?.b][0]");
|
||
|
|
ts.expectPrintedMin_("x = new [a?.b][0]()", "x = new [a?.b][0]");
|
||
|
|
ts.expectPrintedMin_("[a?.b][0]`x`", "[a?.b][0]`x`");
|
||
|
|
|
||
|
|
// Same predicate on the sibling `{f: x}.f -> x` fold: `new a?.b` /
|
||
|
|
// `a?.b`x`` are syntax errors, so bail there too.
|
||
|
|
ts.expectPrintedMin_("x = new ({f: a?.b}).f()", "x = new { f: a?.b }.f");
|
||
|
|
ts.expectPrintedMin_("x = new ({f: a?.[b]}).f()", "x = new { f: a?.[b] }.f");
|
||
|
|
ts.expectPrintedMin_("x = new ({f: a?.b.c}).f()", "x = new { f: a?.b.c }.f");
|
||
|
|
ts.expectPrintedMin_("({f: a?.b}).f`x`", "({ f: a?.b }).f`x`");
|
||
|
|
ts.expectPrintedMin_("x = ({f: a?.b}).f", "x = { f: a?.b }.f");
|
||
|
|
|
||
|
|
// Non-chain items are still inlined.
|
||
|
|
ts.expectPrintedMin_("x = [[y]][0]?.[0].c", "x = y.c");
|
||
|
|
ts.expectPrintedMin_("x = [a.b][0].c", "x = a.b.c");
|
||
|
|
ts.expectPrintedMin_("x = [(a?.b)][0]", "x = [a?.b][0]");
|
||
|
|
ts.expectPrintedMin_("x = ({f: y}).f", "x = y");
|
||
|
|
ts.expectPrintedMin_("x = new ({f: C}).f()", "x = new C");
|
||
|
|
});
|
||
|
|
it("bails out or strips `this` when the index is a call/assignment target", () => {
|
||
|
|
// `[obj.m][0]()` calls through a Reference into the temporary array,
|
||
|
|
// so `this` is the array; inlining to `obj.m()` would bind `this` to
|
||
|
|
// `obj`. Match the sibling folds and emit `(0, obj.m)()`.
|
||
|
|
ts.expectPrintedMin_("x = [obj.m][0]()", "x = (0, obj.m)()");
|
||
|
|
ts.expectPrintedMin_("x = [obj[m]][0]()", "x = (0, obj[m])()");
|
||
|
|
ts.expectPrintedMin_("x = [obj.m][0]", "x = obj.m");
|
||
|
|
ts.expectPrintedMin_("x = [y][0]()", "x = y()");
|
||
|
|
ts.expectPrintedMin_("x = [() => y][0]()", "x = (() => y)()");
|
||
|
|
|
||
|
|
// `[x][0] = v` writes into the temporary, not `x`. Same for `"s"[n]`.
|
||
|
|
ts.expectPrintedMin_("[obj.p][0] = 5", "[obj.p][0] = 5");
|
||
|
|
ts.expectPrintedMin_("[obj.p][0] += 5", "[obj.p][0] += 5");
|
||
|
|
ts.expectPrintedMin_("[obj.p][0]++", "[obj.p][0]++");
|
||
|
|
ts.expectPrintedMin_("[x][0] = 1", "[x][0] = 1");
|
||
|
|
ts.expectPrintedMin_("[,][0] = 1", "[,][0] = 1");
|
||
|
|
ts.expectPrintedMin_('"foo"[2] = 1', '"foo"[2] = 1');
|
||
|
|
ts.expectPrintedMin_('["a", "b"][1] = 1', '["a", "b"][1] = 1');
|
||
|
|
ts.expectPrintedMin_("({ a: [obj.p][0] } = {})", "({ a: [obj.p][0] } = {})");
|
||
|
|
});
|
||
|
|
it("preserves runtime semantics when inlining from a literal index", async () => {
|
||
|
|
const src = `
|
||
|
|
var a = null;
|
||
|
|
function check(label, fn, expected) {
|
||
|
|
var got;
|
||
|
|
try { got = "=> " + fn(); } catch (e) { got = e.constructor.name; }
|
||
|
|
console.log(label + ": " + (got === expected ? "ok" : got + " (want " + expected + ")"));
|
||
|
|
}
|
||
|
|
check("chain .c", () => [[a?.b]][0]?.[0].c, "TypeError");
|
||
|
|
check("chain ()", () => [[a?.b]][0]?.[0](), "TypeError");
|
||
|
|
check("chain [0]", () => [[a?.b]][0]?.[0][0], "TypeError");
|
||
|
|
check("chain obj", () => ({ f: [a?.b] }).f?.[0].c, "TypeError");
|
||
|
|
check("chain flat", () => [a?.b][0].c, "TypeError");
|
||
|
|
check("chain ?.[", () => [a?.b]?.[0].c, "TypeError");
|
||
|
|
check("chain cont", () => [[a?.b.c]][0]?.[0].d, "TypeError");
|
||
|
|
check("chain pure", () => [[0, /* @__PURE__ */ a?.()]][0]?.[1].c, "TypeError");
|
||
|
|
var obj = { n: "obj", m() { return this === obj; } };
|
||
|
|
check("this", () => [obj.m][0](), "=> false");
|
||
|
|
var o2 = { p: 1 };
|
||
|
|
check("assign", () => ([o2.p][0] = 5, o2.p), "=> 1");
|
||
|
|
var ab = { b: class {} };
|
||
|
|
check("new obj", () => new ({ f: ab?.b }).f() instanceof ab.b, "=> true");
|
||
|
|
`;
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "-e", src],
|
||
|
|
env: bunEnv,
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout.trim().split("\n")).toEqual([
|
||
|
|
"chain .c: ok",
|
||
|
|
"chain (): ok",
|
||
|
|
"chain [0]: ok",
|
||
|
|
"chain obj: ok",
|
||
|
|
"chain flat: ok",
|
||
|
|
"chain ?.[: ok",
|
||
|
|
"chain cont: ok",
|
||
|
|
"chain pure: ok",
|
||
|
|
"this: ok",
|
||
|
|
"assign: ok",
|
||
|
|
"new obj: ok",
|
||
|
|
]);
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
it("bails out on optional-chain index into enum", () => {
|
||
|
|
const pre = "enum Foo { A }\nenum Bar { 'a-b' = 1 }\n";
|
||
|
|
const lastLine = out => out.trimEnd().split("\n").at(-1);
|
||
|
|
expect(lastLine(ts.parsed(pre + 'export let y = Foo["A"];', false))).toBe("export let y = 0 /* A */;");
|
||
|
|
expect(lastLine(ts.parsed(pre + 'export let y = Foo?.["A"];', false))).toBe('export let y = Foo?.["A"];');
|
||
|
|
expect(lastLine(ts.parsed(pre + 'export let y = Foo?.["A"]();', false))).toBe('export let y = Foo?.["A"]();');
|
||
|
|
expect(lastLine(ts.parsed(pre + 'export let y = Bar?.["a-b"];', false))).toBe('export let y = Bar?.["a-b"];');
|
||
|
|
expect(lastLine(ts.parsedMin(pre + 'export let y = Foo?.["A"];', false))).toBe("export let y = Foo?.A;");
|
||
|
|
expect(lastLine(ts.parsedMin(pre + 'export let y = Bar?.["a-b"];', false))).toBe('export let y = Bar?.["a-b"];');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("TypeScript", () => {
|
||
|
|
it("import Foo = Baz.Bar", () => {
|
||
|
|
ts.expectPrinted_("import Foo = Baz.Bar;\nexport default Foo;", "const Foo = Baz.Bar;\nexport default Foo");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("ternary should parse correctly when parsing typescript fails", () => {
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"var c = Math.random() ? ({ ...{} }) : ({ ...{} })",
|
||
|
|
"var c = Math.random() ? { ...{} } : { ...{} }",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports Expected ":" for a conditional expression missing its colon', () => {
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
err("let x = a ? b;", 'Expected ":" but found ";"');
|
||
|
|
err("(a ? b)", 'Expected ":" but found ")"');
|
||
|
|
err("x = a ? b c", 'Expected ":" but found "c"');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("contextual keywords used as plain identifiers keep their statements", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
exp("declare = t => 0;", "declare = (t) => 0;\n");
|
||
|
|
exp("declare = (...t) => R;", "declare = (...t) => R;\n");
|
||
|
|
exp("declare.foo = 1;", "declare.foo = 1;\n");
|
||
|
|
exp("interface = t => 0;", "interface = (t) => 0;\n");
|
||
|
|
exp("type = t => 0;", "type = (t) => 0;\n");
|
||
|
|
exp("namespace = t => 0;", "namespace = (t) => 0;\n");
|
||
|
|
exp("module = t => 0;", "module = (t) => 0;\n");
|
||
|
|
exp("abstract = t => 0;", "abstract = (t) => 0;\n");
|
||
|
|
exp("global = t => 0;", "global = (t) => 0;\n");
|
||
|
|
exp(
|
||
|
|
"abstract = () => {}\nclass Foo { m() { return 1 } }",
|
||
|
|
"abstract = () => {};\n\nclass Foo {\n m() {\n return 1;\n }\n}",
|
||
|
|
);
|
||
|
|
|
||
|
|
exp("declare const x: number", "");
|
||
|
|
exp("declare let x: number", "");
|
||
|
|
exp("declare function f(): void", "");
|
||
|
|
exp("declare class Foo {}", "");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("contextual keywords followed by a newline apply ASI instead of acting as modifiers", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
// Statement-level "declare": a newline splits into "declare;" + the following declaration.
|
||
|
|
exp("declare\nfunction foo() { return 1 }\nfoo()", "declare;\nfunction foo() {\n return 1;\n}\nfoo();\n");
|
||
|
|
exp("declare\nlet x = 1\nuse(x)", "declare;\nlet x = 1;\nuse(x);\n");
|
||
|
|
exp("declare\nclass Foo {}\nnew Foo", "declare;\n\nclass Foo {\n}\nnew Foo;\n");
|
||
|
|
exp("declare function foo(): void", "");
|
||
|
|
exp("declare let x: number", "");
|
||
|
|
|
||
|
|
// Statement-level "abstract": a newline splits into "abstract;" + "class Foo {}".
|
||
|
|
exp("abstract\nclass Foo {}\nnew Foo", "abstract;\n\nclass Foo {\n}\nnew Foo;\n");
|
||
|
|
exp("abstract class Foo { abstract bar(): void }\nnew Foo", "class Foo {\n}\nnew Foo;\n");
|
||
|
|
|
||
|
|
// Statement-level "interface": a newline splits into three statements.
|
||
|
|
exp("interface\nFoo\n{ sideEffect() }", "interface;\nFoo;\n{\n sideEffect();\n}");
|
||
|
|
exp("interface Foo { x: number }", "");
|
||
|
|
|
||
|
|
// "export interface \n Foo {}" is a syntax error, matching esbuild.
|
||
|
|
err("export interface\nFoo {}", 'Unexpected "interface"');
|
||
|
|
// "export default interface \n Foo {}" is allowed (the interface name can be on the next line).
|
||
|
|
exp("export default interface\nFoo {}", "");
|
||
|
|
exp("export default interface Foo {}", "");
|
||
|
|
|
||
|
|
// "export default abstract \n class A {}" exports the identifier `abstract` and declares A separately.
|
||
|
|
exp(
|
||
|
|
"export default abstract\nclass A { foo() { return 1 } }\nnew A",
|
||
|
|
"export default abstract;\n\nclass A {\n foo() {\n return 1;\n }\n}\nnew A;\n",
|
||
|
|
);
|
||
|
|
exp("export default abstract class A {}", "export default class A {\n}");
|
||
|
|
|
||
|
|
// Class body "declare": a newline makes it a field named "declare" followed by a method.
|
||
|
|
exp(
|
||
|
|
"class Foo { declare\n foo() { return 1 } }\nnew Foo().foo()",
|
||
|
|
"class Foo {\n declare;\n foo() {\n return 1;\n }\n}\nnew Foo().foo();\n",
|
||
|
|
);
|
||
|
|
exp("class Foo { declare foo: number }", "class Foo {\n}");
|
||
|
|
|
||
|
|
// Class body "abstract": a newline makes it a field named "abstract" followed by a method.
|
||
|
|
exp("abstract class A { abstract\n foo() {} }\nnew A", "class A {\n abstract;\n foo() {}\n}\nnew A;\n");
|
||
|
|
exp("abstract class A { abstract foo(): void }\nnew A", "class A {\n}\nnew A;\n");
|
||
|
|
|
||
|
|
// Class body "accessor": a newline makes it a field named "accessor" followed by a field.
|
||
|
|
exp("class A { accessor\n x = 1 }\nnew A", "class A {\n accessor;\n x = 1;\n}\nnew A;\n");
|
||
|
|
|
||
|
|
// Class body "get"/"set" followed by "*": the asterisk starts a generator; the prior word is a field.
|
||
|
|
exp("class A { get\n *x() {} }\nnew A", "class A {\n get;\n *x() {}\n}\nnew A;\n");
|
||
|
|
exp("class A { set\n *x() {} }\nnew A", "class A {\n set;\n *x() {}\n}\nnew A;\n");
|
||
|
|
// "get"/"set" without the generator star still bind to the next key across a newline.
|
||
|
|
exp("class A { get\n x() { return 1 } }", "class A {\n get x() {\n return 1;\n }\n}");
|
||
|
|
|
||
|
|
// "declare X" where X is not a valid ambient declaration is rejected, so a
|
||
|
|
// newline-split keyword cannot leave the remainder as live runtime code.
|
||
|
|
err("declare interface\nFoo\n{ sideEffect() }", 'Unexpected "interface"');
|
||
|
|
err("declare abstract\nclass Foo {}", 'Unexpected "abstract"');
|
||
|
|
err("declare type\nFoo = number", 'Unexpected "type"');
|
||
|
|
err("declare namespace\nFoo { sideEffect() }", 'Unexpected "namespace"');
|
||
|
|
err("declare module\nFoo { sideEffect() }", 'Unexpected "module"');
|
||
|
|
err("declare declare\nlet x = 1", 'Unexpected "declare"');
|
||
|
|
err("declare foo", 'Unexpected "foo"');
|
||
|
|
err("declare foo: bar", 'Unexpected "foo"');
|
||
|
|
err("declare module : es2015", 'Unexpected "module"');
|
||
|
|
err("export declare interface\nFoo {}", 'Unexpected "interface"');
|
||
|
|
err("export declare abstract\nclass Foo {}", 'Unexpected "abstract"');
|
||
|
|
// All valid "declare X" forms still emit nothing.
|
||
|
|
exp("declare function f(): void", "");
|
||
|
|
exp("declare class C {}", "");
|
||
|
|
exp("declare enum E { A }", "");
|
||
|
|
exp("declare namespace N { let x: number }", "");
|
||
|
|
exp("declare abstract class C {}", "");
|
||
|
|
exp("export declare function f(): void", "");
|
||
|
|
exp("export declare const x: number", "");
|
||
|
|
// "export abstract \n class" and "export declare \n class" fall through silently like esbuild.
|
||
|
|
exp("export abstract\nclass Foo {}\nnew Foo", "abstract;\n\nclass Foo {\n}\nnew Foo;\n");
|
||
|
|
exp("export declare\nclass Foo {}\nnew Foo", "declare;\n\nclass Foo {\n}\nnew Foo;\n");
|
||
|
|
exp("export declare\nlet x = 1\nuse(x)", "declare;\nlet x = 1;\nuse(x);\n");
|
||
|
|
// Inside an ambient body the flag is propagated for body semantics, but the whole
|
||
|
|
// block is erased regardless, so newline-split keywords in the body are harmless.
|
||
|
|
exp("declare namespace N { abstract\nclass Foo {} }", "");
|
||
|
|
exp("declare namespace N { declare\nlet x: number }", "");
|
||
|
|
exp('declare module "m" { abstract\n class Foo {} }', "");
|
||
|
|
exp("declare global { abstract\nclass Foo {} }\nexport {}", "export {};\n");
|
||
|
|
|
||
|
|
// Decorators before "declare"/"abstract" with a newline must still demand a class.
|
||
|
|
err("function dec(c){return c}\n@dec declare\nclass Foo {}", 'Unexpected "declare"');
|
||
|
|
err("function dec(c){return c}\n@dec abstract\nclass Foo {}", 'Unexpected "abstract"');
|
||
|
|
err("function dec(c){return c}\n@dec export default abstract\nclass Foo {}", 'Unexpected "abstract"');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not crash when export default abstract is an expression followed by a class", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
exp("export default abstract = 1\nclass Foo {}", "export default abstract = 1;\n\nclass Foo {\n}");
|
||
|
|
exp("export default abstract ?? 1\nclass Foo {}", "export default abstract ?? 1;\n\nclass Foo {\n}");
|
||
|
|
exp("export default abstract = 1", "export default abstract = 1;\n");
|
||
|
|
|
||
|
|
exp("export default abstract class Foo { abstract bar(): void }", "export default class Foo {\n}");
|
||
|
|
exp("export default abstract class {}", "export default class {\n}");
|
||
|
|
|
||
|
|
err("@dec export default abstract = 1", 'Expected "class" but found end of file');
|
||
|
|
err("@dec(() => 0) export default abstract = 1\nclass Foo {}", 'Expected "class" but found "class"');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("scope tracking stays balanced when a contextual keyword starts a larger expression", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
exp("declare = (...t) => R;e((a) => {(u=> uge);\r\n})", "declare = (...t) => R;\ne((a) => {});\n");
|
||
|
|
exp("declare = t => 0; () => () => 0", "declare = (t) => 0;\n");
|
||
|
|
exp("declare = function () {}; () => () => 0", "declare = function() {};\n");
|
||
|
|
exp("declare = t => 0; function f() { () => 0; }\nf();", "declare = (t) => 0;\nfunction f() {}\nf();\n");
|
||
|
|
|
||
|
|
exp(
|
||
|
|
"abstract = (t) => 0\nclass Foo { m() { return () => () => 0 } }",
|
||
|
|
"abstract = (t) => 0;\n\nclass Foo {\n m() {\n return () => () => 0;\n }\n}",
|
||
|
|
);
|
||
|
|
err("type = (t) => 0 Foo = number; () => () => 0", 'Expected ";" but found "Foo"');
|
||
|
|
err("namespace = (t) => 0 Foo { () => () => 0 }", 'Expected ";" but found "Foo"');
|
||
|
|
err("module = (t) => 0 Foo { () => () => 0 }", 'Expected ";" but found "Foo"');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("scope tracking stays balanced for a forward-declared function inside an if", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
// A function declaration in a single-statement `if`/`else` body gets a fake
|
||
|
|
// block scope so it can be wrapped when it has a body. A TypeScript forward
|
||
|
|
// declaration (no body) emits nothing, so that fake block scope must be
|
||
|
|
// discarded from the scope order; otherwise the next statement's scope is
|
||
|
|
// read out of sync and the parser pops past the topmost scope.
|
||
|
|
exp("if(l)function f(ag): g;\nfor (g in {}) {}", "if (l)\n ;\nfor (g in {}) {}");
|
||
|
|
exp("if (x) function f(): void;\nfor (y in {}) {}", "if (x)\n ;\nfor (y in {}) {}");
|
||
|
|
exp("if (x) {} else function g(): void;\nfor (z in {}) {}", "if (x) {}\nfor (z in {}) {}");
|
||
|
|
|
||
|
|
// A function declaration with a body in the same position is still wrapped.
|
||
|
|
exp("if(l)function f(ag): g {}\nfor (g in {}) {}", "if (l) {\n let f = function(ag) {};\n}\nfor (g in {}) {}");
|
||
|
|
|
||
|
|
// The exact fuzz repro: ts loader, dead-code elimination, trailing \r.
|
||
|
|
const dce = new Bun.Transpiler({ loader: "ts", target: "browser", deadCodeElimination: true });
|
||
|
|
expect(dce.transformSync("if(l)function f(ag): g;\r\nfor (g in {}) {}\r")).toBe(
|
||
|
|
"if (l)\n ;\nfor (g in {}) {}\n",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("export default interface that is not an interface declaration does not crash", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
const unexpected = 'Unexpected "interface"';
|
||
|
|
|
||
|
|
// "interface" turns out to start an expression or a labeled statement, not an
|
||
|
|
// interface declaration. None of these can be a default export value.
|
||
|
|
err("export default interface=2", unexpected);
|
||
|
|
err("export default interface + 1", unexpected);
|
||
|
|
err("export default interface.foo()", unexpected);
|
||
|
|
err("export default interface => 1", unexpected);
|
||
|
|
err("export default interface: 2", unexpected);
|
||
|
|
|
||
|
|
// The exact fuzz repro: tsx loader, no trailing newline.
|
||
|
|
expect(() => transpiler.transformSync("export default interface=2")).toThrow(unexpected);
|
||
|
|
|
||
|
|
// Same shapes through the plain JavaScript loader must not crash either.
|
||
|
|
const js = new Bun.Transpiler({ loader: "js" });
|
||
|
|
expect(() => js.transformSync("export default interface=2")).toThrow(unexpected);
|
||
|
|
expect(() => js.transformSync("export default interface => 1")).toThrow(unexpected);
|
||
|
|
expect(() => js.transformSync("export default interface: 2")).toThrow(unexpected);
|
||
|
|
|
||
|
|
// Real interface declarations still parse and get erased.
|
||
|
|
exp("export default interface Foo {}", "");
|
||
|
|
exp("export default interface Foo { bar(): void }\nexport const x = 1;", "export const x = 1;\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("export default of an identifier written with unicode escapes does not crash", () => {
|
||
|
|
// `\u{66}` decodes to `f`, but the decoded buffer lives outside the source
|
||
|
|
// contents, so the parse-time Ref is tagged AllocatedName rather than
|
||
|
|
// SourceContentsSlice. The visit pass must still replace it with a real
|
||
|
|
// symbol before calling record_declared_symbol.
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
exp("export default \\u{66}", "export default f");
|
||
|
|
exp("export default \\u0066", "export default f");
|
||
|
|
exp("export default \\u{66}oo", "export default foo");
|
||
|
|
exp("const \\u{66} = 1; export default \\u{66};", "const f = 1;\nexport default f;\n");
|
||
|
|
exp("export default (function \\u{66}() {})", "export default (function f() {})");
|
||
|
|
exp("export default (class \\u{66} {})", "export default (class f {\n})");
|
||
|
|
exp("export default function \\u{66}() {}", "export default function f() {}");
|
||
|
|
exp("export default class \\u{66} {}", "export default class f {\n}");
|
||
|
|
|
||
|
|
// The exact fuzz repro used the ts loader with target:"node".
|
||
|
|
const node = new Bun.Transpiler({ loader: "ts", target: "node" });
|
||
|
|
expect(node.transformSync("export default \\u{66}")).toBe("export default f;\n");
|
||
|
|
|
||
|
|
// The same shape through the plain JavaScript loader.
|
||
|
|
const js = new Bun.Transpiler({ loader: "js" });
|
||
|
|
expect(js.transformSync("export default \\u{66}")).toBe("export default f;\n");
|
||
|
|
expect(js.transformSync("export default \\u0066oo")).toBe("export default foo;\n");
|
||
|
|
|
||
|
|
// exports.eliminate marks the default export dead and returns early before
|
||
|
|
// the default_name ref is rewritten to a symbol. record_on_exit must not
|
||
|
|
// feed that parse-time ref to record_declared_symbol.
|
||
|
|
const elim = new Bun.Transpiler({ loader: "ts", exports: { eliminate: ["default"] } });
|
||
|
|
expect(elim.transformSync("export default foo")).toBe("");
|
||
|
|
expect(elim.transformSync("export default \\u{66}")).toBe("");
|
||
|
|
expect(elim.transformSync("export default \\u{66}oo")).toBe("");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects export clauses inside a non-declare namespace", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
// Fuzz repro: an export clause referencing a sibling namespace member
|
||
|
|
// used to panic in the printer ("index out of bounds" on import_records).
|
||
|
|
err("namespace M {\rexport import M_A = M;}\r\nexport namespace M {\rexport {M_A as a};}\r", "Unexpected {");
|
||
|
|
err("namespace M { export import M_A = M; }\nexport namespace M { export { M_A as a }; }", "Unexpected {");
|
||
|
|
|
||
|
|
// esbuild and tsc (TS1194) both reject export declarations in a namespace.
|
||
|
|
err("namespace M { const x = 1; export { x }; }", "Unexpected {");
|
||
|
|
err("namespace M { export {}; }", "Unexpected {");
|
||
|
|
err("module M { const x = 1; export { x }; }", "Unexpected {");
|
||
|
|
err("namespace M { export { x } from 'y'; }", "Unexpected {");
|
||
|
|
err("namespace M { export * from 'y'; }", "Unexpected *");
|
||
|
|
|
||
|
|
// Still allowed in ambient contexts, where the body is type-only and erased.
|
||
|
|
exp("declare namespace M { export { x }; }", "");
|
||
|
|
exp("declare module 'm' { export { x }; }", "");
|
||
|
|
exp("declare namespace M { export * from 'y'; }", "");
|
||
|
|
exp("declare namespace A { namespace B { export { x }; } }", "");
|
||
|
|
|
||
|
|
// "export import" aliases inside a namespace keep working.
|
||
|
|
exp("namespace M { export import M_A = M; }", "var M;\n((M) => {\n M.M_A = M;\n})(M ||= {})");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should parse empty type parameters", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
exp("type X<> = never;var x: X", "var x");
|
||
|
|
exp("interface X<> {};var x: X", "var x");
|
||
|
|
err("class Foo<> {}", 'Expected identifier but found ">"');
|
||
|
|
err("function foo<>(): void {}", 'Expected identifier but found ">"');
|
||
|
|
err("const x: Foo<> = {}", "Unexpected >");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not crash on an unterminated template literal after type arguments", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
err("new C<T>\n`", "Unterminated string literal");
|
||
|
|
err("new C<T>`", "Unterminated string literal");
|
||
|
|
err("f<T>`", "Unterminated string literal");
|
||
|
|
exp("new C<T>`ok`", "new C`ok`;\n");
|
||
|
|
exp("f<T>`ok`", "f`ok`;\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("reports a parse error for a truncated class-in-extends without tripping the scope-order assert", async () => {
|
||
|
|
// An unterminated class-in-extends pushes two ClassBody scopes at the same
|
||
|
|
// EOF loc during error recovery. Run in a subprocess so the debug-only
|
||
|
|
// scope-order assert surfaces as a test failure instead of killing the runner.
|
||
|
|
const cases = {
|
||
|
|
"class o extends class": 'Expected "{" but found end of file',
|
||
|
|
"(class extends class": 'Expected "{" but found end of file',
|
||
|
|
"class o extends (class": 'Expected "{" but found end of file',
|
||
|
|
"class o extends class extends class": 'Expected "{" but found end of file',
|
||
|
|
};
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`
|
||
|
|
const t = new Bun.Transpiler({ loader: "tsx", target: "bun" });
|
||
|
|
const out = [];
|
||
|
|
for (const input of ${JSON.stringify(Object.keys(cases))}) {
|
||
|
|
try {
|
||
|
|
t.transformSync(input);
|
||
|
|
out.push("<no error>");
|
||
|
|
} catch (e) {
|
||
|
|
out.push((e instanceof AggregateError ? e.errors[0] : e).message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
process.stdout.write(JSON.stringify(out));
|
||
|
|
`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect({ stdout: stdout && JSON.parse(stdout), stderr, exitCode }).toEqual({
|
||
|
|
stdout: Object.values(cases),
|
||
|
|
stderr: "",
|
||
|
|
exitCode: 0,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("should parse infer extends ternary correctly #9959", () => {
|
||
|
|
ts.expectPrinted_("type Foo<T> = T extends infer U ? U : never;", "");
|
||
|
|
ts.expectPrinted_("var foo: Foo extends string | infer Foo extends string ? Foo : never", "var foo");
|
||
|
|
ts.expectPrinted_("var foo: Foo extends string & infer Foo extends string ? Foo : never", "var foo");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("deeply nested infer constraints in template literal types do not hang the parser", async () => {
|
||
|
|
// Every `infer X extends` constraint attempt that backtracks gets re-parsed as
|
||
|
|
// the `extends` clause of a conditional type. Without memoizing backtracked
|
||
|
|
// attempts, that re-parse repeats the attempts nested inside it, so inputs that
|
||
|
|
// nest the pattern inside template literal types take O(2^depth) time (found by
|
||
|
|
// fuzzing Bun.build with a ~140-level input).
|
||
|
|
const fill = (n, unit) => Buffer.alloc(n * unit.length, unit).toString();
|
||
|
|
const depth = 128;
|
||
|
|
// Shape found by fuzzing: every constraint attempt fails near EOF (hard error).
|
||
|
|
const malformed =
|
||
|
|
"type LengthDown<\r\n ? unknown extends " + fill(depth, "`${infer own extends ") + "`${infer $Rest}`\r";
|
||
|
|
// Valid variant: every constraint parses but is followed by "?", so every level
|
||
|
|
// backtracks and re-parses the constraint as part of a conditional type.
|
||
|
|
const valid =
|
||
|
|
"type X = " + fill(depth, "`${infer o extends ") + "number" + fill(depth, " ? 0 : 1}`") + ";\nexport {};\n";
|
||
|
|
|
||
|
|
using dir = tempDir("ts-infer-constraint-hang", {
|
||
|
|
"malformed.ts": malformed,
|
||
|
|
"valid.ts": valid,
|
||
|
|
"check.ts": `
|
||
|
|
const malformed = await Bun.build({ entrypoints: ["./malformed.ts"], target: "browser", throw: false });
|
||
|
|
if (malformed.success) throw new Error("malformed input should fail to parse");
|
||
|
|
const valid = await Bun.build({ entrypoints: ["./valid.ts"], target: "browser", throw: false });
|
||
|
|
if (!valid.success) throw new Error("valid input should build: " + valid.logs.join("\\n"));
|
||
|
|
console.log("DONE");
|
||
|
|
`,
|
||
|
|
});
|
||
|
|
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "check.ts"],
|
||
|
|
cwd: String(dir),
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
timeout: 30_000,
|
||
|
|
killSignal: "SIGKILL",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout).toContain("DONE");
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
}, 90_000);
|
||
|
|
|
||
|
|
it("type arguments in expression require a bare '>' closer", () => {
|
||
|
|
// TypeScript's "parseTypeArgumentsInExpression" only accepts a bare ">"
|
||
|
|
// to close the list, so a ">=" (or ">>", ">>>", ">>=", ">>>=") forces
|
||
|
|
// backtracking to the relational/shift interpretation. Previously we
|
||
|
|
// split the ">=" and committed to the type-argument parse, turning e.g.
|
||
|
|
// "f('s', x < 0, x >= 0 ? a : b)" into "f('s', x = b)".
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
exp('f("s", x < 0, x >= 0 ? "p:" + x : undefined);', 'f("s", x < 0, x >= 0 ? "p:" + x : undefined);\n');
|
||
|
|
exp("f(x < y, x >= z);", "f(x < y, x >= z);\n");
|
||
|
|
exp("const a = (x < 0, x >= 0 ? y : z);", "const a = (x < 0, x >= 0 ? y : z);\n");
|
||
|
|
exp("f<x>=g<y>;", "f < x >= g;\n");
|
||
|
|
exp("f<x>>g<y>;", "f < x >> g;\n");
|
||
|
|
exp("f<x>>>g<y>;", "f < x >>> g;\n");
|
||
|
|
exp("new C(a < b, a >= b);", "new C(a < b, a >= b);\n");
|
||
|
|
|
||
|
|
// Nested type arguments still work: the inner list runs in a type
|
||
|
|
// context and strips one ">" from ">>" before the outer closer sees it.
|
||
|
|
exp("f<Array<number>>();", "f();\n");
|
||
|
|
exp("f<Array<Array<number>>>();", "f();\n");
|
||
|
|
exp("new f<Array<number>>();", "new f;\n");
|
||
|
|
exp("const g = f<Array<number>>;", "const g = f;\n");
|
||
|
|
|
||
|
|
// A bare ">" followed by "=" on the next token still commits.
|
||
|
|
exp("f<x> = g<y>;", "f = g;\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not turn '<' ... '>=' into an assignment at runtime", async () => {
|
||
|
|
const source = `
|
||
|
|
const out: unknown[] = [];
|
||
|
|
const f = (...a: unknown[]) => out.push(a);
|
||
|
|
let x: number = 5;
|
||
|
|
f("s", x < 0, x >= 0 ? "p:" + x : undefined);
|
||
|
|
out.push(x);
|
||
|
|
class C { constructor(...a: unknown[]) { out.push(a); } }
|
||
|
|
let a: number = 1, b: number = 2;
|
||
|
|
new C(a < b, a >= b);
|
||
|
|
out.push(a);
|
||
|
|
console.log(JSON.stringify(out));
|
||
|
|
`;
|
||
|
|
using dir = tempDir("ts-ge-type-args", { "entry.ts": source });
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "run", "entry.ts"],
|
||
|
|
env: bunEnv,
|
||
|
|
cwd: String(dir),
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
if (exitCode !== 0) expect(stderr).toBe("");
|
||
|
|
expect({ stdout: stdout.trim(), exitCode }).toEqual({
|
||
|
|
stdout: JSON.stringify([["s", false, "p:5"], 5, [true, false], 1]),
|
||
|
|
exitCode: 0,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("instantiation expressions", async () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
// TODO: fix Unexpected end of file in these cases
|
||
|
|
// exp("f<number>", "f;\n");
|
||
|
|
// exp("f<number, boolean>", "f;\n");
|
||
|
|
// exp("f.g<number>", "f.g;\n");
|
||
|
|
exp("f<number>.g", "f.g;\n");
|
||
|
|
// exp("f<number>.g<number>", "f.g;\n");
|
||
|
|
// exp("f['g']<number>", 'f["g"];\n');
|
||
|
|
// exp("(f<number>)<number>", "f;\n");
|
||
|
|
|
||
|
|
// Function call
|
||
|
|
exp("const x1 = f<true>\n(true);", "const x1 = f(true);\n");
|
||
|
|
// Relational expression
|
||
|
|
// exp("const x1 = f<true>\ntrue;", "const x1 = f;\ntrue;\n");
|
||
|
|
// Instantiation expression
|
||
|
|
// exp("const x1 = f<true>;\n(true);", "const x1 = f;\ntrue;\n");
|
||
|
|
|
||
|
|
// Trailing commas are not allowed
|
||
|
|
exp("const x = Array<number>\n(0);", "const x = Array(0);\n");
|
||
|
|
// TODO: make these errors
|
||
|
|
// exp("const x = Array<number>;\n(0);", "const x = Array;\n0;\n");
|
||
|
|
// err("const x = Array<number,>\n(0);", 'Expected identifier but found ">"');
|
||
|
|
// err("const x = Array<number,>;\n(0);", 'Expected identifier but found ">"');
|
||
|
|
|
||
|
|
exp("f<number>?.();", "f?.();\n");
|
||
|
|
exp("f?.<number>();", "f?.();\n");
|
||
|
|
exp("f<<T>() => T>?.();", "f?.();\n");
|
||
|
|
exp("f?.<<T>() => T>();", "f?.();\n");
|
||
|
|
|
||
|
|
exp("f<number>['g'];", 'f < number > ["g"];\n');
|
||
|
|
|
||
|
|
exp("type T21 = typeof Array<string>; f();", "f();\n");
|
||
|
|
exp("type T22 = typeof Array<string, number>; f();", "f();\n");
|
||
|
|
|
||
|
|
exp("f<x>, g<y>;", "f, g;\n");
|
||
|
|
exp("f<<T>() => T>;", "f;\n");
|
||
|
|
exp("f.x<<T>() => T>;", "f.x;\n");
|
||
|
|
exp("f['x']<<T>() => T>;", 'f["x"];\n');
|
||
|
|
exp("f<x>g<y>;", "f < x > g;\n");
|
||
|
|
exp("f<x>=g<y>;", "f < x >= g;\n");
|
||
|
|
exp("f<x>>g<y>;", "f < x >> g;\n");
|
||
|
|
exp("f<x>>>g<y>;", "f < x >>> g;\n");
|
||
|
|
err("f<x>>=g<y>;", "Invalid assignment target");
|
||
|
|
err("f<x>>>=g<y>;", "Invalid assignment target");
|
||
|
|
exp("f<x> = g<y>;", "f = g;\n");
|
||
|
|
err("f<x> > g<y>;", "Unexpected >");
|
||
|
|
err("f<x> >> g<y>;", "Unexpected >>");
|
||
|
|
err("f<x> >>> g<y>;", "Unexpected >>>");
|
||
|
|
err("f<x> >= g<y>;", "Unexpected >=");
|
||
|
|
err("f<x> >>= g<y>;", "Unexpected >>=");
|
||
|
|
err("f<x> >>>= g<y>;", "Unexpected >>>=");
|
||
|
|
exp("a([f<x>]);", "a([f]);\n");
|
||
|
|
exp("f<x> ? g<y> : h<z>;", "f ? g : h;\n");
|
||
|
|
exp("{ f<x> }", "f");
|
||
|
|
exp("f<x> + g<y>;", "f < x > +g;\n");
|
||
|
|
exp("f<x> - g<y>;", "f < x > -g;\n");
|
||
|
|
exp("f<x> * g<y>;", "f * g;\n");
|
||
|
|
exp("f<x> *= g<y>;", "f *= g;\n");
|
||
|
|
exp("f<x> == g<y>;", "f == g;\n");
|
||
|
|
exp("f<x> ?? g<y>;", "f ?? g;\n");
|
||
|
|
exp("f<x> in g<y>;", "f in g;\n");
|
||
|
|
exp("f<x> instanceof g<y>;", "f instanceof g;\n");
|
||
|
|
exp("f<x> as g<y>;", "f;\n");
|
||
|
|
exp("f<x> satisfies g<y>;", "f;\n");
|
||
|
|
|
||
|
|
err("const a8 = f<number><number>;", "Unexpected ;");
|
||
|
|
err("const b1 = f?.<number>;", "Parse error");
|
||
|
|
// err("const b1 = f?.<number>;", 'Expected "(" but found ";"');
|
||
|
|
|
||
|
|
// TODO: why are the JSX tests failing but only in Bun.Transpiler? They
|
||
|
|
// work when run manually
|
||
|
|
|
||
|
|
// See: https://github.com/microsoft/TypeScript/issues/48711
|
||
|
|
// exp("type x = y\n<number>\nz", "z;\n");
|
||
|
|
// exp("type x = y\n<number>\nz\n</number>", '/* @__PURE__ */ React.createElement("number", null, "z");\n');
|
||
|
|
exp("type x = typeof y\n<number>\nz", "z;\n");
|
||
|
|
// exp("type x = typeof y\n<number>\nz\n</number>", '/* @__PURE__ */ React.createElement("number", null, "z");\n');
|
||
|
|
exp("interface Foo { \n (a: number): a \n <T>(): void \n }", "");
|
||
|
|
exp("interface Foo { \n (a: number): a \n <T>(): void \n }", "");
|
||
|
|
exp("interface Foo { \n (a: number): typeof a \n <T>(): void \n }", "");
|
||
|
|
exp("interface Foo { \n (a: number): typeof a \n <T>(): void \n }", "");
|
||
|
|
// err("type x = y\n<number>\nz\n</number>", "Unterminated regular expression");
|
||
|
|
// errX(
|
||
|
|
// "type x = y\n<number>\nz",
|
||
|
|
// 'Unexpected end of file before a closing "number" tag\n<stdin>: NOTE: The opening "number" tag is here:',
|
||
|
|
// );
|
||
|
|
err("type x = typeof y\n<number>\nz\n</number>", "Syntax Error!!");
|
||
|
|
// errX(
|
||
|
|
// "type x = typeof y\n<number>\nz",
|
||
|
|
// 'Unexpected end of file before a closing "number" tag\n<stdin>: NOTE: The opening "number" tag is here:',
|
||
|
|
// );
|
||
|
|
|
||
|
|
// See: https://github.com/microsoft/TypeScript/issues/48654
|
||
|
|
exp("x<true> y", "x < true > y;\n");
|
||
|
|
exp("x<true>\ny", "x;\ny;\n");
|
||
|
|
exp("x<true>\nif (y) {}", "x;\nif (y)");
|
||
|
|
exp("x<true>\nimport 'y'", 'x;\nimport"y";\n');
|
||
|
|
exp("x<true>\nimport('y')", 'x;\nimport("y");\n');
|
||
|
|
exp("x<true>\na(import.meta)", "x;\na(import.meta);\n");
|
||
|
|
exp("x<true> import('y')", 'x < true > import("y");\n');
|
||
|
|
exp("x<true> import.meta", "x < true > import.meta;\n");
|
||
|
|
exp("new x<number> y", "new x < number > y");
|
||
|
|
exp("new x<number>\ny", "new x;\ny");
|
||
|
|
exp("new x<number>\nif (y) {}", "new x;\nif (y)");
|
||
|
|
exp("new x<true>\nimport 'y'", 'new x;\nimport"y"');
|
||
|
|
exp("new x<true>\nimport('y')", 'new x;\nimport("y")');
|
||
|
|
exp("new x<true>\na(import.meta)", "new x;\na(import.meta)");
|
||
|
|
exp("new x<true> import('y')", 'new x < true > import("y")');
|
||
|
|
exp("new x<true> import.meta", "new x < true > import.meta");
|
||
|
|
|
||
|
|
// See: https://github.com/microsoft/TypeScript/issues/48759
|
||
|
|
err("x<true>\nimport<T>('y')", "Unexpected <");
|
||
|
|
err("new x<true>\nimport<T>('y')", "Unexpected <");
|
||
|
|
|
||
|
|
// See: https://github.com/evanw/esbuild/issues/2201
|
||
|
|
// err("return Array < ;", "Unexpected ;");
|
||
|
|
// err("return Array < > ;", "Unexpected >");
|
||
|
|
// err("return Array < , > ;", "Unexpected ,");
|
||
|
|
exp("return Array < number > ;", "return Array;\n");
|
||
|
|
exp("return Array < number > 1;", "return Array < number > 1;\n");
|
||
|
|
exp("return Array < number > +1;", "return Array < number > 1;\n");
|
||
|
|
exp("return Array < number > (1);", "return Array(1);\n");
|
||
|
|
exp("return Array < number >> 1;", "return Array < number >> 1;\n");
|
||
|
|
exp("return Array < number >>> 1;", "return Array < number >>> 1;\n");
|
||
|
|
exp("return Array < Array < number >> ;", "return Array;\n");
|
||
|
|
exp("return Array < Array < number > > ;", "return Array;\n");
|
||
|
|
err("return Array < Array < number > > 1;", "Unexpected >");
|
||
|
|
exp("return Array < Array < number >> 1;", "return Array < Array < number >> 1;\n");
|
||
|
|
err("return Array < Array < number > > +1;", "Unexpected >");
|
||
|
|
exp("return Array < Array < number >> +1;", "return Array < Array < number >> 1;\n");
|
||
|
|
exp("return Array < Array < number >> (1);", "return Array(1);\n");
|
||
|
|
exp("return Array < Array < number > > (1);", "return Array(1);\n");
|
||
|
|
exp("return Array < number > in x;", "return Array in x;\n");
|
||
|
|
exp("return Array < Array < number >> in x;", "return Array in x;\n");
|
||
|
|
exp("return Array < Array < number > > in x;", "return Array in x;\n");
|
||
|
|
exp("for (var x = Array < number > in y) ;", "x = Array;\nfor (x in y)\n ;\nvar x;\n");
|
||
|
|
// exp("for (var x = Array < Array < number >> in y) ;", "x = Array;\nfor (var x in y)\n ;\n");
|
||
|
|
exp("for (var x = Array < Array < number >> in y) ;", "x = Array;\nfor (x in y)\n ;\nvar x;\n");
|
||
|
|
// exp("for (var x = Array < Array < number > > in y) ;", "x = Array;\nfor (var x in y)\n ;\n");
|
||
|
|
exp("for (var x = Array < Array < number > > in y) ;", "x = Array;\nfor (x in y)\n ;\nvar x;\n");
|
||
|
|
|
||
|
|
// See: https://github.com/microsoft/TypeScript/pull/49353
|
||
|
|
exp("F<{}> 0", "F < {} > 0;\n");
|
||
|
|
exp("F<{}> class F<T> {}", "F < {} > class F {\n};\n");
|
||
|
|
exp("f<{}> function f<T>() {}", "f < {} > function f() {\n};\n");
|
||
|
|
exp("F<{}>\nabc()", "F;\nabc();\n");
|
||
|
|
|
||
|
|
// TODO: why are there two newlines here?
|
||
|
|
exp("F<{}>()\nclass F<T> {}", "F();\n\nclass F {\n}");
|
||
|
|
|
||
|
|
exp("f<{}>()\nfunction f<T>() {}", "let f = function() {\n};\nf()");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("malformed enums", () => {
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
err("enum Foo { [2]: 'hi' }", 'Expected identifier but found "["');
|
||
|
|
err("enum [] { a }", 'Expected identifier but found "["');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects yield/await/this/super in enum initializers", () => {
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
// The enum body is lowered into an arrow IIFE, so an enclosing function's
|
||
|
|
// generator/async context must not leak into initializer expressions.
|
||
|
|
err("function *f() { enum x { y = yield 1 } }", 'Cannot use "yield" outside a generator function');
|
||
|
|
err("async function f() { enum x { y = await 1 } }", '"await" can only be used inside an "async" function');
|
||
|
|
err("async function f() { const enum x { y = await 1 } }", '"await" can only be used inside an "async" function');
|
||
|
|
err("let g = async () => { enum x { y = await 1 } }", '"await" can only be used inside an "async" function');
|
||
|
|
err("enum x { y = await 1 }", '"await" can only be used inside an "async" function');
|
||
|
|
err("enum x { y = this }", 'Cannot use "this" here');
|
||
|
|
err("enum x { y = () => this }", 'Cannot use "this" here');
|
||
|
|
err("class C { m() { enum x { y = this } } }", 'Cannot use "this" here');
|
||
|
|
err("class C extends B { m() { enum x { y = super.foo } } }", 'Unexpected "super"');
|
||
|
|
err("class C extends B { constructor() { super(); enum x { y = super() } } }", 'Unexpected "super"');
|
||
|
|
err("class C { static { enum x { y = super.foo } } }", 'Unexpected "super"');
|
||
|
|
err("declare enum x { y = await 1 }", '"await" can only be used inside an "async" function');
|
||
|
|
err("declare enum x { y = this }", 'Cannot use "this" here');
|
||
|
|
|
||
|
|
// A nested function establishes its own context, so these remain valid.
|
||
|
|
exp(
|
||
|
|
"function *f() { enum x { y = (function*() { yield 1 })() } }",
|
||
|
|
'function* f() {\n let x;\n ((x) => {\n x[x["y"] = function* () {\n yield 1;\n }()] = "y";\n })(x ||= {});\n}',
|
||
|
|
);
|
||
|
|
exp(
|
||
|
|
"async function f() { enum x { y = (async () => await 1)() } }",
|
||
|
|
'async function f() {\n let x;\n ((x) => {\n x[x["y"] = (async () => await 1)()] = "y";\n })(x ||= {});\n}',
|
||
|
|
);
|
||
|
|
exp(
|
||
|
|
"enum x { y = (function() { return this })() }",
|
||
|
|
'var x;\n((x) => {\n x[x["y"] = function() {\n return this;\n }()] = "y";\n})(x ||= {})',
|
||
|
|
);
|
||
|
|
// The enclosing context is restored after the body: sibling statements
|
||
|
|
// keep their yield/await/super permissions.
|
||
|
|
exp(
|
||
|
|
"function *f() { enum x { y = 1 } yield 1; }",
|
||
|
|
'function* f() {\n let x;\n ((x) => {\n x[x["y"] = 1] = "y";\n })(x ||= {});\n yield 1;\n}',
|
||
|
|
);
|
||
|
|
exp(
|
||
|
|
"async function f() { enum x { y = 1 } await 1; }",
|
||
|
|
'async function f() {\n let x;\n ((x) => {\n x[x["y"] = 1] = "y";\n })(x ||= {});\n await 1;\n}',
|
||
|
|
);
|
||
|
|
exp(
|
||
|
|
"class C extends B { m() { enum x { y = 1 } super.foo(); } }",
|
||
|
|
'class C extends B {\n m() {\n let x;\n ((x) => {\n x[x["y"] = 1] = "y";\n })(x ||= {});\n super.foo();\n }\n}',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects await/this/return in namespace bodies", () => {
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
err("namespace x { export const y = await 1; }", '"await" can only be used inside an "async" function');
|
||
|
|
err("namespace x { await 1; }", '"await" can only be used inside an "async" function');
|
||
|
|
err("namespace x { return 1; }", "A return statement cannot be used here");
|
||
|
|
err("namespace x { return; }", "A return statement cannot be used here");
|
||
|
|
err("namespace x { const y: string = this; }", 'Cannot use "this" here');
|
||
|
|
err("namespace x { export const y = () => this; }", 'Cannot use "this" here');
|
||
|
|
err("namespace x.y { return 1; }", "A return statement cannot be used here");
|
||
|
|
err("module x { return 1; }", "A return statement cannot be used here");
|
||
|
|
err("declare namespace x { export const y = this; }", 'Cannot use "this" here');
|
||
|
|
err("namespace x { for await (const y of []); }", 'Cannot use "await" outside an async function');
|
||
|
|
|
||
|
|
// The namespace body lowers into a non-async arrow, where "await" is a
|
||
|
|
// valid binding identifier; the module-level reserved-word rule must
|
||
|
|
// not leak into it.
|
||
|
|
exp("namespace x { let await = 1; }", "var x;\n((x) => {\n let await = 1;\n})(x ||= {})");
|
||
|
|
exp(
|
||
|
|
"namespace x { export function f() { return 1; } }",
|
||
|
|
"var x;\n((x) => {\n function f() {\n return 1;\n }\n x.f = f;\n})(x ||= {})",
|
||
|
|
);
|
||
|
|
exp(
|
||
|
|
"namespace x { export const y = async () => await 1; }",
|
||
|
|
"var x;\n((x) => {\n x.y = async () => await 1;\n})(x ||= {})",
|
||
|
|
);
|
||
|
|
// Class methods and fields introduce their own "this" binding.
|
||
|
|
exp(
|
||
|
|
"namespace x { export class C { m() { return this } } }",
|
||
|
|
"var x;\n((x) => {\n\n class C {\n m() {\n return this;\n }\n }\n x.C = C;\n})(x ||= {})",
|
||
|
|
);
|
||
|
|
exp(
|
||
|
|
"namespace x { export class C { f = this } }",
|
||
|
|
"var x;\n((x) => {\n\n class C {\n f = this;\n }\n x.C = C;\n})(x ||= {})",
|
||
|
|
);
|
||
|
|
// The enclosing context is restored after the body: top-level await is
|
||
|
|
// still accepted immediately after a namespace.
|
||
|
|
exp("namespace x { export const y = 1; } await 1;", "var x;\n((x) => {\n x.y = 1;\n})(x ||= {});\nawait 1");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("doesn't crash with functions assigned to enum values", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
expect(
|
||
|
|
ts.transpiledOutput(
|
||
|
|
`
|
||
|
|
enum ABC {
|
||
|
|
A = () => {},
|
||
|
|
}
|
||
|
|
function foo() {}
|
||
|
|
`,
|
||
|
|
"hi",
|
||
|
|
),
|
||
|
|
).toMatchInlineSnapshot(`
|
||
|
|
"var ABC;
|
||
|
|
((ABC) => {
|
||
|
|
ABC[ABC["A"] = () => {}] = "A";
|
||
|
|
})(ABC ||= {});
|
||
|
|
function foo() {}
|
||
|
|
"
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// TODO: fix all the cases that report generic "Parse error"
|
||
|
|
it("types", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
exp("let x: T extends number\n ? T\n : number", "let x;\n");
|
||
|
|
exp("let x: {y: T extends number ? T : number}", "let x;\n");
|
||
|
|
exp("let x: {y: T \n extends: number}", "let x;\n");
|
||
|
|
exp("let x: {y: T \n extends?: number}", "let x;\n");
|
||
|
|
exp("let x: (number | string)[]", "let x;\n");
|
||
|
|
exp("let x: [string[]?]", "let x;\n");
|
||
|
|
exp("let x: [number?, string?]", "let x;\n");
|
||
|
|
exp("let x: [a: number, b?: string, ...c: number[]]", "let x;\n");
|
||
|
|
exp("type x =\n A\n | B\n C", "C;\n");
|
||
|
|
exp("type x =\n | A\n | B\n C", "C;\n");
|
||
|
|
exp("type x =\n A\n & B\n C", "C;\n");
|
||
|
|
exp("type x =\n & A\n & B\n C", "C;\n");
|
||
|
|
exp("type x = [-1, 0, 1]\na([])", "a([]);\n");
|
||
|
|
exp("type x = [-1n, 0n, 1n]\na([])", "a([]);\n");
|
||
|
|
exp("type x = {0: number, readonly 1: boolean}\na([])", "a([]);\n");
|
||
|
|
exp("type x = {'a': number, readonly 'b': boolean}\na([])", "a([]);\n");
|
||
|
|
exp("type\nFoo = {}", "type;\nFoo = {};\n");
|
||
|
|
err("export type\nFoo = {}", 'Unexpected newline after "type"');
|
||
|
|
exp("let x: {x: 'a', y: false, z: null}", "let x;\n");
|
||
|
|
exp("let x: {foo(): void}", "let x;\n");
|
||
|
|
exp("let x: {['x']: number}", "let x;\n");
|
||
|
|
exp("let x: {['x'](): void}", "let x;\n");
|
||
|
|
exp("let x: {[key: string]: number}", "let x;\n");
|
||
|
|
exp("let x: {[keyof: string]: number}", "let x;\n");
|
||
|
|
exp("let x: {[readonly: string]: number}", "let x;\n");
|
||
|
|
exp("let x: {[infer: string]: number}", "let x;\n");
|
||
|
|
exp("let x: [keyof: string]", "let x;\n");
|
||
|
|
exp("let x: [readonly: string]", "let x;\n");
|
||
|
|
exp("let x: [infer: string]", "let x;\n");
|
||
|
|
exp("let x: [keyof?: string]", "let x;\n");
|
||
|
|
exp("let x: [readonly?: string]", "let x;\n");
|
||
|
|
exp("let x: [infer?: string]", "let x;\n");
|
||
|
|
exp("let x: [import?: string]", "let x;\n");
|
||
|
|
exp("let x: [new?: string]", "let x;\n");
|
||
|
|
exp("let x: [typeof?: string]", "let x;\n");
|
||
|
|
exp("let x: [function?: string]", "let x;\n");
|
||
|
|
exp("let x: [a: number, readonly?: string, ...infer: number[]]", "let x;\n");
|
||
|
|
err("let x: A extends B ? keyof : string", "Unexpected :");
|
||
|
|
err("let x: A extends B ? readonly : string", "Unexpected :");
|
||
|
|
err("let x: A extends B ? infer : string", 'Expected identifier but found ":"');
|
||
|
|
err("let x: {[new: string]: number}", 'Expected "(" but found ":"');
|
||
|
|
err("let x: {[import: string]: number}", 'Expected "(" but found ":"');
|
||
|
|
err("let x: {[typeof: string]: number}", 'Expected identifier but found ":"');
|
||
|
|
exp("let x: () => void = Foo", "let x = Foo;\n");
|
||
|
|
exp("let x: new () => void = Foo", "let x = Foo;\n");
|
||
|
|
exp("let x = 'x' as keyof T", 'let x = "x";\n');
|
||
|
|
exp("let x = [1] as readonly [number]", "let x = [1];\n");
|
||
|
|
exp("let x = 'x' as keyof typeof Foo", 'let x = "x";\n');
|
||
|
|
exp("let fs: typeof import('fs') = require('fs')", 'let fs = require("fs");\n');
|
||
|
|
exp("let fs: typeof import('fs').exists = require('fs').exists", 'let fs = require("fs").exists;\n');
|
||
|
|
exp("let fs: typeof import('fs', { assert: { type: 'json' } }) = require('fs')", 'let fs = require("fs");\n');
|
||
|
|
exp(
|
||
|
|
"let fs: typeof import('fs', { assert: { 'resolution-mode': 'import' } }) = require('fs')",
|
||
|
|
'let fs = require("fs");\n',
|
||
|
|
);
|
||
|
|
exp("let x: <T>() => Foo<T>", "let x;\n");
|
||
|
|
exp("let x: new <T>() => Foo<T>", "let x;\n");
|
||
|
|
exp("let x: <T extends object>() => Foo<T>", "let x;\n");
|
||
|
|
exp("let x: new <T extends object>() => Foo<T>", "let x;\n");
|
||
|
|
exp("type Foo<T> = {[P in keyof T]?: T[P]}", "");
|
||
|
|
exp("type Foo<T> = {[P in keyof T]+?: T[P]}", "");
|
||
|
|
exp("type Foo<T> = {[P in keyof T]-?: T[P]}", "");
|
||
|
|
exp("type Foo<T> = {readonly [P in keyof T]: T[P]}", "");
|
||
|
|
exp("type Foo<T> = {-readonly [P in keyof T]: T[P]}", "");
|
||
|
|
exp("type Foo<T> = {+readonly [P in keyof T]: T[P]}", "");
|
||
|
|
exp("type Foo<T> = {[infer in T]?: Foo}", "");
|
||
|
|
exp("type Foo<T> = {[keyof in T]?: Foo}", "");
|
||
|
|
exp("type Foo<T> = {[asserts in T]?: Foo}", "");
|
||
|
|
exp("type Foo<T> = {[abstract in T]?: Foo}", "");
|
||
|
|
exp("type Foo<T> = {[readonly in T]?: Foo}", "");
|
||
|
|
exp("type Foo<T> = {[satisfies in T]?: Foo}", "");
|
||
|
|
exp("let x: number! = y", "let x = y;\n");
|
||
|
|
// TODO: dead code elimination causes this to become "y;\n" because !
|
||
|
|
// operator is side effect free on an identifier
|
||
|
|
// exp("let x: number \n !y", "let x;\n!y;\n");
|
||
|
|
exp("const x: unique = y", "const x = y;\n");
|
||
|
|
exp("const x: unique<T> = y", "const x = y;\n");
|
||
|
|
exp("const x: unique\nsymbol = y", "const x = y;\n");
|
||
|
|
exp("let x: typeof a = y", "let x = y;\n");
|
||
|
|
exp("let x: typeof a.b = y", "let x = y;\n");
|
||
|
|
exp("let x: typeof a.if = y", "let x = y;\n");
|
||
|
|
exp("let x: typeof if.a = y", "let x = y;\n");
|
||
|
|
exp("let x: typeof readonly = y", "let x = y;\n");
|
||
|
|
err("let x: typeof readonly Array", 'Expected ";" but found "Array"');
|
||
|
|
exp("let x: `y`", "let x;\n");
|
||
|
|
err("let x: tag`y`", 'Expected ";" but found "`y`"');
|
||
|
|
exp("let x: { <A extends B>(): c.d \n <E extends F>(): g.h }", "let x;\n");
|
||
|
|
// exp("type x = a.b \n <c></c>", '/* @__PURE__ */ React.createElement("c", null);\n');
|
||
|
|
exp("type Foo = a.b \n | c.d", "");
|
||
|
|
exp("type Foo = a.b \n & c.d", "");
|
||
|
|
exp("type Foo = \n | a.b \n | c.d", "");
|
||
|
|
exp("type Foo = \n & a.b \n & c.d", "");
|
||
|
|
exp("type Foo = Bar extends [infer T] ? T : null", "");
|
||
|
|
exp("type Foo = Bar extends [infer T extends string] ? T : null", "");
|
||
|
|
exp("type Foo = {} extends infer T extends {} ? A<T> : never", "");
|
||
|
|
exp("type Foo = {} extends (infer T extends {}) ? A<T> : never", "");
|
||
|
|
exp("let x: A extends B<infer C extends D> ? D : never", "let x;\n");
|
||
|
|
exp("let x: A extends B<infer C extends D ? infer C : never> ? D : never", "let x;\n");
|
||
|
|
exp("type Foo = `${infer T extends `${infer U extends `${infer V}`}`}`", "");
|
||
|
|
exp("type Foo = `${infer T extends `${infer U extends string ? 1 : 2}` ? 3 : 4}`", "");
|
||
|
|
exp("let x: T extends infer U extends `${infer V extends W ? 1 : 2}` ? U : never", "let x;\n");
|
||
|
|
exp("let x: ([e1, e2, ...es]: any) => any", "let x;\n");
|
||
|
|
exp("let x: (...[e1, e2, es]: any) => any", "let x;\n");
|
||
|
|
exp("let x: (...[e1, e2, ...es]: any) => any", "let x;\n");
|
||
|
|
exp("let x: (y, [e1, e2, ...es]: any) => any", "let x;\n");
|
||
|
|
exp("let x: (y, ...[e1, e2, es]: any) => any", "let x;\n");
|
||
|
|
exp("let x: (y, ...[e1, e2, ...es]: any) => any", "let x;\n");
|
||
|
|
|
||
|
|
exp("let x: A.B<X.Y>", "let x;\n");
|
||
|
|
exp("let x: A.B<X.Y>=2", "let x = 2;\n");
|
||
|
|
exp("let x: A.B<X.Y<Z>>", "let x;\n");
|
||
|
|
exp("let x: A.B<X.Y<Z>>=2", "let x = 2;\n");
|
||
|
|
exp("let x: A.B<X.Y<Z<T>>>", "let x;\n");
|
||
|
|
exp("let x: A.B<X.Y<Z<T>>>=2", "let x = 2;\n");
|
||
|
|
|
||
|
|
exp("a((): A<T>=> 0)", "a(() => 0);\n");
|
||
|
|
exp("a((): A<B<T>>=> 0)", "a(() => 0);\n");
|
||
|
|
exp("a((): A<B<C<T>>>=> 0)", "a(() => 0);\n");
|
||
|
|
|
||
|
|
exp("let foo: any\n<x>y", "let foo;\ny;\n");
|
||
|
|
// expectPrintedTSX(t, "let foo: any\n<x>y</x>", 'let foo;\n/* @__PURE__ */ React.createElement("x", null, "y");\n');
|
||
|
|
err("let foo: (any\n<x>y)", 'Expected ")" but found "<"');
|
||
|
|
|
||
|
|
exp("let foo = bar as (null)", "let foo = bar;\n");
|
||
|
|
exp("let foo = bar\nas (null)", "let foo = bar;\nas(null);\n");
|
||
|
|
err("let foo = (bar\nas (null))", 'Expected ")" but found "as"');
|
||
|
|
|
||
|
|
exp("a as any ? b : c;", "a ? b : c;\n");
|
||
|
|
exp("a as any ? async () => b : c;", "a || c;\n");
|
||
|
|
|
||
|
|
exp("foo as number extends Object ? any : any;", "foo;\n");
|
||
|
|
exp("foo as number extends Object ? () => void : any;", "foo;\n");
|
||
|
|
exp(
|
||
|
|
"let a = b ? c : d as T extends T ? T extends T ? T : never : never ? e : f;",
|
||
|
|
"let a = b ? c : d ? e : f;\n",
|
||
|
|
);
|
||
|
|
err("type a = b extends c", 'Expected "?" but found end of file');
|
||
|
|
err("type a = b extends c extends d", 'Expected "?" but found "extends"');
|
||
|
|
err("type a = b ? c : d", 'Expected ";" but found "?"');
|
||
|
|
|
||
|
|
exp("let foo: keyof Object = 'toString'", 'let foo = "toString";\n');
|
||
|
|
exp("let foo: keyof\nObject = 'toString'", 'let foo = "toString";\n');
|
||
|
|
exp("let foo: (keyof\nObject) = 'toString'", 'let foo = "toString";\n');
|
||
|
|
|
||
|
|
exp("type Foo = Array<<T>(x: T) => T>\n x", "x;\n");
|
||
|
|
// expectPrintedTSX(t, "<Foo<<T>(x: T) => T>/>", "/* @__PURE__ */ React.createElement(Foo, null);\n");
|
||
|
|
|
||
|
|
// Certain built-in types do not accept type parameters
|
||
|
|
exp("x as 1 < 1", "x < 1;\n");
|
||
|
|
exp("x as 1n < 1", "x < 1;\n");
|
||
|
|
exp("x as -1 < 1", "x < 1;\n");
|
||
|
|
exp("x as -1n < 1", "x < 1;\n");
|
||
|
|
exp("x as '' < 1", "x < 1;\n");
|
||
|
|
exp("x as `` < 1", "x < 1;\n");
|
||
|
|
exp("x as any < 1", "x < 1;\n");
|
||
|
|
exp("x as bigint < 1", "x < 1;\n");
|
||
|
|
exp("x as false < 1", "x < 1;\n");
|
||
|
|
exp("x as never < 1", "x < 1;\n");
|
||
|
|
exp("x as null < 1", "x < 1;\n");
|
||
|
|
exp("x as number < 1", "x < 1;\n");
|
||
|
|
exp("x as object < 1", "x < 1;\n");
|
||
|
|
exp("x as string < 1", "x < 1;\n");
|
||
|
|
exp("x as symbol < 1", "x < 1;\n");
|
||
|
|
exp("x as this < 1", "x < 1;\n");
|
||
|
|
exp("x as true < 1", "x < 1;\n");
|
||
|
|
exp("x as undefined < 1", "x < 1;\n");
|
||
|
|
exp("x as unique symbol < 1", "x < 1;\n");
|
||
|
|
exp("x as unknown < 1", "x < 1;\n");
|
||
|
|
exp("x as void < 1", "x < 1;\n");
|
||
|
|
err("x as Foo < 1", 'Expected ">" but found end of file');
|
||
|
|
|
||
|
|
// These keywords are valid tuple labels
|
||
|
|
exp("type _false = [false: string]", "");
|
||
|
|
exp("type _function = [function: string]", "");
|
||
|
|
exp("type _import = [import: string]", "");
|
||
|
|
exp("type _new = [new: string]", "");
|
||
|
|
exp("type _null = [null: string]", "");
|
||
|
|
exp("type _this = [this: string]", "");
|
||
|
|
exp("type _true = [true: string]", "");
|
||
|
|
exp("type _typeof = [typeof: string]", "");
|
||
|
|
exp("type _void = [void: string]", "");
|
||
|
|
|
||
|
|
// These keywords are invalid tuple labels
|
||
|
|
err("type _break = [break: string]", "Unexpected break");
|
||
|
|
err("type _case = [case: string]", "Unexpected case");
|
||
|
|
err("type _catch = [catch: string]", "Unexpected catch");
|
||
|
|
err("type _class = [class: string]", "Unexpected class");
|
||
|
|
err("type _const = [const: string]", 'Unexpected "const"');
|
||
|
|
err("type _continue = [continue: string]", "Unexpected continue");
|
||
|
|
err("type _debugger = [debugger: string]", "Unexpected debugger");
|
||
|
|
err("type _default = [default: string]", "Unexpected default");
|
||
|
|
err("type _delete = [delete: string]", "Unexpected delete");
|
||
|
|
err("type _do = [do: string]", "Unexpected do");
|
||
|
|
err("type _else = [else: string]", "Unexpected else");
|
||
|
|
err("type _enum = [enum: string]", "Unexpected enum");
|
||
|
|
err("type _export = [export: string]", "Unexpected export");
|
||
|
|
err("type _extends = [extends: string]", "Unexpected extends");
|
||
|
|
err("type _finally = [finally: string]", "Unexpected finally");
|
||
|
|
err("type _for = [for: string]", "Unexpected for");
|
||
|
|
err("type _if = [if: string]", "Unexpected if");
|
||
|
|
err("type _in = [in: string]", "Unexpected in");
|
||
|
|
err("type _instanceof = [instanceof: string]", "Unexpected instanceof");
|
||
|
|
err("type _return = [return: string]", "Unexpected return");
|
||
|
|
err("type _super = [super: string]", "Unexpected super");
|
||
|
|
err("type _switch = [switch: string]", "Unexpected switch");
|
||
|
|
err("type _throw = [throw: string]", "Unexpected throw");
|
||
|
|
err("type _try = [try: string]", "Unexpected try");
|
||
|
|
err("type _var = [var: string]", "Unexpected var");
|
||
|
|
err("type _while = [while: string]", "Unexpected while");
|
||
|
|
err("type _with = [with: string]", "Unexpected with");
|
||
|
|
|
||
|
|
// TypeScript 4.1
|
||
|
|
exp("let foo: `${'a' | 'b'}-${'c' | 'd'}` = 'a-c'", 'let foo = "a-c";\n');
|
||
|
|
|
||
|
|
// TypeScript 4.2
|
||
|
|
exp("let x: abstract new () => void = Foo", "let x = Foo;\n");
|
||
|
|
exp("let x: abstract new <T>() => Foo<T>", "let x;\n");
|
||
|
|
exp("let x: abstract new <T extends object>() => Foo<T>", "let x;\n");
|
||
|
|
err("let x: abstract () => void = Foo", 'Expected ";" but found "("');
|
||
|
|
err("let x: abstract <T>() => Foo<T>", 'Expected ";" but found "("');
|
||
|
|
err("let x: abstract <T extends object>() => Foo<T>", 'Expected "?" but found ">"');
|
||
|
|
|
||
|
|
// TypeScript 4.7
|
||
|
|
// jsxErrorArrow := "The character \">\" is not valid inside a JSX element\n" +
|
||
|
|
// "NOTE: Did you mean to escape it as \"{'>'}\" instead?\n"
|
||
|
|
exp("type Foo<in T> = T", "");
|
||
|
|
exp("type Foo<out T> = T", "");
|
||
|
|
exp("type Foo<in out> = T", "");
|
||
|
|
exp("type Foo<out out> = T", "");
|
||
|
|
exp("type Foo<in out out> = T", "");
|
||
|
|
exp("type Foo<in X, out Y> = [X, Y]", "");
|
||
|
|
exp("type Foo<out X, in Y> = [X, Y]", "");
|
||
|
|
exp("type Foo<out X, out Y extends keyof X> = [X, Y]", "");
|
||
|
|
err("type Foo<i\\u006E T> = T", 'Expected identifier but found "i\\u006E"');
|
||
|
|
err("type Foo<ou\\u0074 T> = T", 'Expected ">" but found "T"');
|
||
|
|
err("type Foo<in in> = T", 'The modifier "in" is not valid here');
|
||
|
|
err("type Foo<out in> = T", 'The modifier "in" is not valid here');
|
||
|
|
err("type Foo<out in T> = T", 'The modifier "in" is not valid here');
|
||
|
|
err("type Foo<public T> = T", 'Expected ">" but found "T"');
|
||
|
|
err("type Foo<in out in T> = T", 'The modifier "in" is not valid here');
|
||
|
|
err("type Foo<in out out T> = T", 'The modifier "out" is not valid here');
|
||
|
|
exp("class Foo<in T> {}", "class Foo {\n}");
|
||
|
|
exp("class Foo<out T> {}", "class Foo {\n}");
|
||
|
|
exp("export default class Foo<in T> {}", "export default class Foo {\n}");
|
||
|
|
exp("export default class Foo<out T> {}", "export default class Foo {\n}");
|
||
|
|
exp("export default class <in T> {}", "export default class {\n}");
|
||
|
|
exp("export default class <out T> {}", "export default class {\n}");
|
||
|
|
exp("interface Foo<in T> {}", "");
|
||
|
|
exp("interface Foo<out T> {}", "");
|
||
|
|
exp("declare class Foo<in T> {}", "");
|
||
|
|
exp("declare class Foo<out T> {}", "");
|
||
|
|
exp("declare interface Foo<in T> {}", "");
|
||
|
|
exp("declare interface Foo<out T> {}", "");
|
||
|
|
err("function foo<in T>() {}", 'The modifier "in" is not valid here');
|
||
|
|
err("function foo<out T>() {}", 'The modifier "out" is not valid here');
|
||
|
|
err("export default function foo<in T>() {}", 'The modifier "in" is not valid here');
|
||
|
|
err("export default function foo<out T>() {}", 'The modifier "out" is not valid here');
|
||
|
|
err("export default function <in T>() {}", 'The modifier "in" is not valid here');
|
||
|
|
err("export default function <out T>() {}", 'The modifier "out" is not valid here');
|
||
|
|
err("let foo: Foo<in T>", "Unexpected in");
|
||
|
|
err("let foo: Foo<out T>", 'Expected ">" but found "T"');
|
||
|
|
err("declare function foo<in T>()", 'The modifier "in" is not valid here');
|
||
|
|
err("declare function foo<out T>()", 'The modifier "out" is not valid here');
|
||
|
|
err("declare let foo: Foo<in T>", "Unexpected in");
|
||
|
|
err("declare let foo: Foo<out T>", 'Expected ">" but found "T"');
|
||
|
|
exp("Foo = class <in T> {}", "Foo = class {\n}");
|
||
|
|
exp("Foo = class <out T> {}", "Foo = class {\n}");
|
||
|
|
exp("Foo = class Bar<in T> {}", "Foo = class Bar {\n}");
|
||
|
|
exp("Foo = class Bar<out T> {}", "Foo = class Bar {\n}");
|
||
|
|
err("foo = function <in T>() {}", 'The modifier "in" is not valid here');
|
||
|
|
err("foo = function <out T>() {}", 'The modifier "out" is not valid here');
|
||
|
|
err("class Foo { foo<in T>(): T {} }", 'The modifier "in" is not valid here');
|
||
|
|
err("class Foo { foo<out T>(): T {} }", 'The modifier "out" is not valid here');
|
||
|
|
err("foo = { foo<in T>(): T {} }", 'The modifier "in" is not valid here');
|
||
|
|
err("foo = { foo<out T>(): T {} }", 'The modifier "out" is not valid here');
|
||
|
|
err("<in T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
err("<out T>() => {}", 'The modifier "out" is not valid here');
|
||
|
|
err("<in T, out T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
err("let x: <in T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
err("let x: <out T>() => {}", 'The modifier "out" is not valid here');
|
||
|
|
err("let x: <in T, out T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
err("let x: new <in T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
err("let x: new <out T>() => {}", 'The modifier "out" is not valid here');
|
||
|
|
err("let x: new <in T, out T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
|
||
|
|
err("let x: { y<in T>(): any }", 'The modifier "in" is not valid here');
|
||
|
|
err("let x: { y<out T>(): any }", 'The modifier "out" is not valid here');
|
||
|
|
err("let x: new <in T, out T>() => {}", 'The modifier "in" is not valid here');
|
||
|
|
|
||
|
|
// expectPrintedTSX(t, "<in T></in>", "/* @__PURE__ */ React.createElement(\"in\", { T: true });\n")
|
||
|
|
// expectPrintedTSX(t, "<out T></out>", "/* @__PURE__ */ React.createElement(\"out\", { T: true });\n")
|
||
|
|
// expectPrintedTSX(t, "<in out T></in>", "/* @__PURE__ */ React.createElement(\"in\", { out: true, T: true });\n")
|
||
|
|
// expectPrintedTSX(t, "<out in T></out>", "/* @__PURE__ */ React.createElement(\"out\", { in: true, T: true });\n")
|
||
|
|
// expectPrintedTSX(t, "<in T extends={true}></in>", "/* @__PURE__ */ React.createElement(\"in\", { T: true, extends: true });\n")
|
||
|
|
// expectPrintedTSX(t, "<out T extends={true}></out>", "/* @__PURE__ */ React.createElement(\"out\", { T: true, extends: true });\n")
|
||
|
|
// expectPrintedTSX(t, "<in out T extends={true}></in>", "/* @__PURE__ */ React.createElement(\"in\", { out: true, T: true, extends: true });\n")
|
||
|
|
// errX(t, "<in T,>() => {}", "Expected \">\" but found \",\"\n")
|
||
|
|
// errX(t, "<out T,>() => {}", "Expected \">\" but found \",\"\n")
|
||
|
|
// errX(t, "<in out T,>() => {}", "Expected \">\" but found \",\"\n")
|
||
|
|
// errX(t, "<in T extends any>() => {}", jsxErrorArrow+"Unexpected end of file before a closing \"in\" tag\n<stdin>: NOTE: The opening \"in\" tag is here:\n")
|
||
|
|
// errX(t, "<out T extends any>() => {}", jsxErrorArrow+"Unexpected end of file before a closing \"out\" tag\n<stdin>: NOTE: The opening \"out\" tag is here:\n")
|
||
|
|
// errX(t, "<in out T extends any>() => {}", jsxErrorArrow+"Unexpected end of file before a closing \"in\" tag\n<stdin>: NOTE: The opening \"in\" tag is here:\n")
|
||
|
|
exp("class Container { get data(): typeof this.#data {} }", "class Container {\n get data() {}\n}");
|
||
|
|
exp("const a: typeof this.#a = 1;", "const a = 1;\n");
|
||
|
|
err("const a: typeof #a = 1;", 'Expected identifier but found "#a"');
|
||
|
|
|
||
|
|
// TypeScript 5.0
|
||
|
|
exp("class Foo<const T> {}", "class Foo {\n}");
|
||
|
|
exp("class Foo<const T extends X> {}", "class Foo {\n}");
|
||
|
|
exp("Foo = class <const T> {}", "Foo = class {\n}");
|
||
|
|
exp("Foo = class Bar<const T> {}", "Foo = class Bar {\n}");
|
||
|
|
exp("function foo<const T>() {}", "function foo() {}");
|
||
|
|
exp("foo = function <const T>() {}", "foo = function() {}");
|
||
|
|
exp("foo = function bar<const T>() {}", "foo = function bar() {}");
|
||
|
|
exp("class Foo { bar<const T>() {} }", "class Foo {\n bar() {}\n}");
|
||
|
|
exp("interface Foo { bar<const T>(): T }", "");
|
||
|
|
exp("interface Foo { new bar<const T>(): T }", "");
|
||
|
|
exp("let x: { bar<const T>(): T }", "let x;\n");
|
||
|
|
exp("let x: { new bar<const T>(): T }", "let x;\n");
|
||
|
|
exp("foo = { bar<const T>() {} }", "foo = { bar() {} }");
|
||
|
|
exp("x = <const>(y)", "x = y;\n");
|
||
|
|
exp("export let f = <const T>() => {}", "export let f = () => {};\n");
|
||
|
|
exp("export let f = <const const T>() => {}", "export let f = () => {};\n");
|
||
|
|
exp("export let f = async <const T>() => {}", "export let f = async () => {};\n");
|
||
|
|
exp("export let f = async <const const T>() => {}", "export let f = async () => {};\n");
|
||
|
|
exp("let x: <const T>() => T = y", "let x = y;\n");
|
||
|
|
exp("let x: <const const T>() => T = y", "let x = y;\n");
|
||
|
|
exp("let x: new <const T>() => T = y", "let x = y;\n");
|
||
|
|
exp("let x: new <const const T>() => T = y", "let x = y;\n");
|
||
|
|
err("type Foo<const T> = T", 'The modifier "const" is not valid here');
|
||
|
|
err("interface Foo<const T> {}", 'The modifier "const" is not valid here');
|
||
|
|
err("let x: <const>() => {}", 'Expected identifier but found ">"');
|
||
|
|
err("let x: new <const>() => {}", 'Expected identifier but found ">"');
|
||
|
|
err("let x: Foo<const T>", 'Expected ">" but found "T"');
|
||
|
|
err("x = <T,>(y)", 'Expected "=>" but found end of file');
|
||
|
|
err("x = <const T>(y)", 'Expected "=>" but found end of file');
|
||
|
|
err("x = <T extends X>(y)", 'Expected "=>" but found end of file');
|
||
|
|
err("x = async <T,>(y)", 'Expected "=>" but found end of file');
|
||
|
|
err("x = async <const T>(y)", 'Expected "=>" but found end of file');
|
||
|
|
err("x = async <T extends X>(y)", 'Expected "=>" but found end of file');
|
||
|
|
err("x = <const const>() => {}", 'Expected ">" but found "const"');
|
||
|
|
exp("class Foo<const const const T> {}", "class Foo {\n}");
|
||
|
|
exp("class Foo<const in out T> {}", "class Foo {\n}");
|
||
|
|
exp("class Foo<in const out T> {}", "class Foo {\n}");
|
||
|
|
exp("class Foo<in out const T> {}", "class Foo {\n}");
|
||
|
|
exp("class Foo<const in const out const T> {}", "class Foo {\n}");
|
||
|
|
err("class Foo<in const> {}", 'Expected identifier but found ">"');
|
||
|
|
err("class Foo<out const> {}", 'Expected identifier but found ">"');
|
||
|
|
err("class Foo<in out const> {}", 'Expected identifier but found ">"');
|
||
|
|
// expectPrintedTSX(t, "<const>(x)</const>", '/* @__PURE__ */ React.createElement("const", null, "(x)");\n');
|
||
|
|
// expectPrintedTSX(t, "<const const/>", '/* @__PURE__ */ React.createElement("const", { const: true });\n');
|
||
|
|
// expectPrintedTSX(t, "<const const></const>", '/* @__PURE__ */ React.createElement("const", { const: true });\n');
|
||
|
|
// expectPrintedTSX(t, "<const T/>", '/* @__PURE__ */ React.createElement("const", { T: true });\n');
|
||
|
|
// expectPrintedTSX(t, "<const T></const>", '/* @__PURE__ */ React.createElement("const", { T: true });\n');
|
||
|
|
// expectPrintedTSX(
|
||
|
|
// t,
|
||
|
|
// "<const T>(y) = {}</const>",
|
||
|
|
// '/* @__PURE__ */ React.createElement("const", { T: true }, "(y) = ");\n',
|
||
|
|
// );
|
||
|
|
// expectPrintedTSX(
|
||
|
|
// t,
|
||
|
|
// "<const T extends/>",
|
||
|
|
// '/* @__PURE__ */ React.createElement("const", { T: true, extends: true });\n',
|
||
|
|
// );
|
||
|
|
// expectPrintedTSX(
|
||
|
|
// t,
|
||
|
|
// "<const T extends></const>",
|
||
|
|
// '/* @__PURE__ */ React.createElement("const", { T: true, extends: true });\n',
|
||
|
|
// );
|
||
|
|
// expectPrintedTSX(
|
||
|
|
// t,
|
||
|
|
// "<const T extends>(y) = {}</const>",
|
||
|
|
// '/* @__PURE__ */ React.createElement("const", { T: true, extends: true }, "(y) = ");\n',
|
||
|
|
// );
|
||
|
|
// expectPrintedTSX(t, "<const T,>() => {}", "() => {};\n");
|
||
|
|
// expectPrintedTSX(t, "<const T, X>() => {}", "() => {};\n");
|
||
|
|
// expectPrintedTSX(t, "<const T, const X>() => {}", "() => {};\n");
|
||
|
|
// expectPrintedTSX(t, "<const T, const const X>() => {}", "() => {};\n");
|
||
|
|
// expectPrintedTSX(t, "<const T extends X>() => {}", "() => {};\n");
|
||
|
|
// expectPrintedTSX(t, "async <const T,>() => {}", "async () => {};\n");
|
||
|
|
// expectPrintedTSX(t, "async <const T, X>() => {}", "async () => {};\n");
|
||
|
|
// expectPrintedTSX(t, "async <const T, const X>() => {}", "async () => {};\n");
|
||
|
|
// expectPrintedTSX(t, "async <const T, const const X>() => {}", "async () => {};\n");
|
||
|
|
// expectPrintedTSX(t, "async <const T extends X>() => {}", "async () => {};\n");
|
||
|
|
// errX(
|
||
|
|
// t,
|
||
|
|
// "<const T>() => {}",
|
||
|
|
// jsxErrorArrow +
|
||
|
|
// 'Unexpected end of file before a closing "const" tag\n<stdin>: NOTE: The opening "const" tag is here:\n',
|
||
|
|
// );
|
||
|
|
// errX(
|
||
|
|
// t,
|
||
|
|
// "<const const>() => {}",
|
||
|
|
// jsxErrorArrow +
|
||
|
|
// 'Unexpected end of file before a closing "const" tag\n<stdin>: NOTE: The opening "const" tag is here:\n',
|
||
|
|
// );
|
||
|
|
// errX(t, "<const const T,>() => {}", 'Expected ">" but found ","');
|
||
|
|
// errX(
|
||
|
|
// t,
|
||
|
|
// "<const const T extends X>() => {}",
|
||
|
|
// jsxErrorArrow +
|
||
|
|
// 'Unexpected end of file before a closing "const" tag\n<stdin>: NOTE: The opening "const" tag is here:\n',
|
||
|
|
// );
|
||
|
|
err("async <const T>() => {}", "Unexpected const");
|
||
|
|
err("async <const const>() => {}", "Unexpected const");
|
||
|
|
|
||
|
|
// TODO: why doesn't this one fail?
|
||
|
|
// err("async <const const T,>() => {}", "Unexpected const");
|
||
|
|
// err("async <const const T extends X>() => {}", "Unexpected const");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("non-null assertion with new operator", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
// Basic non-null assertion with new operator on nested class
|
||
|
|
exp(
|
||
|
|
"const obj = { a: class Abc {} }; const instance = new obj!.a();",
|
||
|
|
"const obj = { a: class Abc {\n} };\nconst instance = new obj.a",
|
||
|
|
);
|
||
|
|
|
||
|
|
// with constructor
|
||
|
|
exp(
|
||
|
|
"const obj = { a: class Abc { constructor(x) { this.x = x; }} }; const instance = new obj!.a(1);",
|
||
|
|
"const obj = { a: class Abc {\n constructor(x) {\n this.x = x;\n }\n} };\nconst instance = new obj.a(1)",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Non-null assertion with new operator on nested property
|
||
|
|
exp(
|
||
|
|
"const obj = { nested: { Class: class NestedClass {} } }; const instance = new obj!.nested.Class();",
|
||
|
|
"const obj = { nested: { Class: class NestedClass {\n} } };\nconst instance = new obj.nested.Class",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Multiple non-null assertions in new expression
|
||
|
|
exp(
|
||
|
|
"const obj = { a: { b: class DeepClass {} } }; const instance = new obj!.a!.b();",
|
||
|
|
"const obj = { a: { b: class DeepClass {\n} } };\nconst instance = new obj.a.b",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Non-null assertion with new operator and method call
|
||
|
|
exp(
|
||
|
|
"const obj = { getClass() { return class MyClass {}; } }; const C = obj!.getClass(); const instance = new C();",
|
||
|
|
"const obj = { getClass() {\n return class MyClass {\n };\n} };\nconst C = obj.getClass();\nconst instance = new C",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("modifiers", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
exp("class Foo { public foo: number }", "class Foo {\n foo;\n}");
|
||
|
|
exp("class Foo { private foo: number }", "class Foo {\n foo;\n}");
|
||
|
|
exp("class Foo { protected foo: number }", "class Foo {\n foo;\n}");
|
||
|
|
exp("class Foo { declare foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { declare public foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { public declare foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { override foo: number }", "class Foo {\n foo;\n}");
|
||
|
|
exp("class Foo { override public foo: number }", "class Foo {\n foo;\n}");
|
||
|
|
exp("class Foo { public override foo: number }", "class Foo {\n foo;\n}");
|
||
|
|
exp("class Foo { declare override public foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { declare foo = 123 }", "class Foo {\n}");
|
||
|
|
|
||
|
|
exp("class Foo { public static foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { private static foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { protected static foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { declare static foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { declare public static foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { public declare static foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { public static declare foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { override static foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { override public static foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { public override static foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { public static override foo: number }", "class Foo {\n static foo;\n}");
|
||
|
|
exp("class Foo { declare override public static foo: number }", "class Foo {\n}");
|
||
|
|
exp("class Foo { declare static foo = 123 }", "class Foo {\n}");
|
||
|
|
exp("class Foo { static declare foo = 123 }", "class Foo {\n}");
|
||
|
|
|
||
|
|
exp("let x: abstract new () => void = Foo", "let x = Foo");
|
||
|
|
exp("let x: abstract new <T>() => Foo<T>", "let x");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("as", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
exp("x as 1 < 1", "x < 1");
|
||
|
|
exp("x as 1n < 1", "x < 1");
|
||
|
|
exp("x as -1 < 1", "x < 1");
|
||
|
|
exp("x as -1n < 1", "x < 1");
|
||
|
|
exp("x as '' < 1", "x < 1");
|
||
|
|
exp("x as `` < 1", "x < 1");
|
||
|
|
exp("x as any < 1", "x < 1");
|
||
|
|
exp("x as bigint < 1", "x < 1");
|
||
|
|
exp("x as false < 1", "x < 1");
|
||
|
|
exp("x as never < 1", "x < 1");
|
||
|
|
exp("x as null < 1", "x < 1");
|
||
|
|
exp("x as number < 1", "x < 1");
|
||
|
|
exp("x as object < 1", "x < 1");
|
||
|
|
exp("x as string < 1", "x < 1");
|
||
|
|
exp("x as symbol < 1", "x < 1");
|
||
|
|
exp("x as this < 1", "x < 1");
|
||
|
|
exp("x as true < 1", "x < 1");
|
||
|
|
exp("x as undefined < 1", "x < 1");
|
||
|
|
exp("x as unique symbol < 1", "x < 1");
|
||
|
|
exp("x as unknown < 1", "x < 1");
|
||
|
|
exp("x as void < 1", "x < 1");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("class constructor", () => {
|
||
|
|
const fixtures = [
|
||
|
|
[
|
||
|
|
`class Test {
|
||
|
|
b: string;
|
||
|
|
|
||
|
|
constructor(private a: string) {
|
||
|
|
this.b = a;
|
||
|
|
}
|
||
|
|
}`,
|
||
|
|
|
||
|
|
`
|
||
|
|
class Test {
|
||
|
|
a;
|
||
|
|
b;
|
||
|
|
constructor(a) {
|
||
|
|
this.a = a;
|
||
|
|
this.b = a;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`.trim(),
|
||
|
|
],
|
||
|
|
[
|
||
|
|
`class Test extends Bar {
|
||
|
|
b: string;
|
||
|
|
|
||
|
|
constructor(private a: string) {
|
||
|
|
super();
|
||
|
|
this.b = a;
|
||
|
|
}
|
||
|
|
}`,
|
||
|
|
|
||
|
|
`
|
||
|
|
class Test extends Bar {
|
||
|
|
a;
|
||
|
|
b;
|
||
|
|
constructor(a) {
|
||
|
|
super();
|
||
|
|
this.a = a;
|
||
|
|
this.b = a;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`.trim(),
|
||
|
|
],
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const [code, out] of fixtures) {
|
||
|
|
expect(ts.parsed(code, false, false).trim()).toBe(out);
|
||
|
|
expect(
|
||
|
|
ts
|
||
|
|
.parsed("var Test = " + code.trim(), false, false)
|
||
|
|
.trim()
|
||
|
|
.replaceAll("\n", "")
|
||
|
|
.replaceAll(" ", ""),
|
||
|
|
).toBe(("var Test = " + out.trim() + ";\n").replaceAll("\n", "").replaceAll(" ", ""));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("import Foo = require('bar')", () => {
|
||
|
|
ts.expectPrinted_("import React = require('react')", 'const React = require("react")');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("import type Foo = require('bar')", () => {
|
||
|
|
ts.expectPrinted_("import type Foo = require('bar')", "");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("unused import = gets removed", () => {
|
||
|
|
ts.expectPrinted_("import Foo = Baz.Bar;", "");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("export import Foo = Baz.Bar", () => {
|
||
|
|
ts.expectPrinted_("export import Foo = Baz.Bar;", "export const Foo = Baz.Bar");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("re-declaring an import binding that is kept in the output is an error", () => {
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
err('import{Observable}from""\nimport{Observable} from "x"', '"Observable" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nexport class Foo {}', '"Foo" has already been declared');
|
||
|
|
err('import Foo from "./x";\nclass Foo {}', '"Foo" has already been declared');
|
||
|
|
err('import * as Foo from "./x";\nclass Foo {}', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nfunction Foo() {}', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nvar Foo = 1;', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nvar Foo = 1;\nvar Foo = 2;', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nlet Foo = 1;', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nenum Foo {}', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nimport Foo = require("./y");', '"Foo" has already been declared');
|
||
|
|
err('import { Foo } from "./x";\nimport Foo = Bar.Baz;', '"Foo" has already been declared');
|
||
|
|
err('import { Foo, Foo } from "./x";', '"Foo" has already been declared');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("re-declaring an elided import binding is allowed", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
|
||
|
|
exp('import type { Foo } from "./x";\nclass Foo {}', "class Foo {\n}");
|
||
|
|
exp(
|
||
|
|
'import { type Foo, Bar } from "./x";\nclass Foo {}\nconsole.log(Bar);',
|
||
|
|
'import { Bar } from "./x";\n\nclass Foo {\n}\nconsole.log(Bar);\n',
|
||
|
|
);
|
||
|
|
exp('import { Foo } from "./x";\ndeclare class Foo {}\nnew Foo();', 'import { Foo } from "./x";\nnew Foo;\n');
|
||
|
|
exp('import { foo } from "./x";\nfunction foo(): void;', 'import { foo } from "./x";\n');
|
||
|
|
|
||
|
|
const trimming = new Bun.Transpiler({ loader: "ts", trimUnusedImports: true });
|
||
|
|
expect(trimming.transformSync('import { Foo } from "./x";\nexport class Foo {}')).toBe("export class Foo {\n}\n");
|
||
|
|
expect(trimming.transformSync('import{Observable}from""\nimport{Observable} from "x"')).toBe("");
|
||
|
|
expect(trimming.transformSync('import { Foo } from "./x";\nnamespace Foo { export const x = 1 }')).toBe(
|
||
|
|
"var Foo;\n((Foo) => {\n Foo.x = 1;\n})(Foo ||= {});\n",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("export = {foo: 123}", () => {
|
||
|
|
ts.expectPrinted_("export = {foo: 123}", "module.exports = { foo: 123 }");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("export default class implements TypeScript regression", () => {
|
||
|
|
expect(
|
||
|
|
transpiler
|
||
|
|
.transformSync(
|
||
|
|
`
|
||
|
|
export default class implements ITest {
|
||
|
|
async* runTest(path: string): AsyncGenerator<number> {
|
||
|
|
yield Math.random();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
"ts",
|
||
|
|
)
|
||
|
|
.trim(),
|
||
|
|
).toBe(
|
||
|
|
`
|
||
|
|
export default class {
|
||
|
|
async* runTest(path) {
|
||
|
|
yield Math.random();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("identifier named async followed by as/satisfies is not an arrow function", () => {
|
||
|
|
// https://github.com/evanw/esbuild/issues/4027
|
||
|
|
// https://github.com/microsoft/TypeScript/pull/8444
|
||
|
|
ts.expectPrinted_("function f(async?) { g(async as boolean) }", "function f(async) {\n g(async);\n}");
|
||
|
|
ts.expectPrinted_("function f(async?) { g(async satisfies boolean) }", "function f(async) {\n g(async);\n}");
|
||
|
|
ts.expectPrinted_("function f(async?) { g(async in x) }", "function f(async) {\n g(async in x);\n}");
|
||
|
|
ts.expectPrinted_("function f() { g(async as => boolean) }", "function f() {\n g(async (as) => boolean);\n}");
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"function f() { g(async satisfies => boolean) }",
|
||
|
|
"function f() {\n g(async (satisfies) => boolean);\n}",
|
||
|
|
);
|
||
|
|
ts.expectPrinted_("let async = true; let x = async as boolean;", "let async = true;\nlet x = async;\n");
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"let async = true; console.log(async satisfies boolean);",
|
||
|
|
"let async = true;\nconsole.log(async);\n",
|
||
|
|
);
|
||
|
|
ts.expectPrinted_("const async = 1; export default async as any;", "const async = 1;\nexport default async;\n");
|
||
|
|
ts.expectPrinted_("let f = async x => {}", "let f = async (x) => {}");
|
||
|
|
ts.expectParseError("function f(async) { g(async as) }", "Unexpected )");
|
||
|
|
|
||
|
|
// "for (async of" must stay rejected in TypeScript mode once the
|
||
|
|
// lookahead above stops the arrow commit; see the [lookahead != async of]
|
||
|
|
// restriction on the for-of grammar.
|
||
|
|
ts.expectParseError("for (async of [7]);", 'For loop initializers cannot start with "async of"');
|
||
|
|
ts.expectParseError("for (async\nof [7]);", 'For loop initializers cannot start with "async of"');
|
||
|
|
ts.expectPrinted_("for (async.x of [7]);", "for (async.x of [7])\n ;\n");
|
||
|
|
ts.expectPrinted_("for (async as any of [7]);", "for ((async) of [7])\n ;\n");
|
||
|
|
ts.expectPrinted_("for (async satisfies T of [7]);", "for ((async) of [7])\n ;\n");
|
||
|
|
ts.expectPrinted_("for (async! of [7]);", "for ((async) of [7])\n ;\n");
|
||
|
|
ts.expectPrinted_("for (async of => {};;);", "for (async (of) => {};; )\n ;\n");
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"async function f() { for await (async of [7]); }",
|
||
|
|
"async function f() {\n for await ((async) of [7])\n ;\n}",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("satisfies", () => {
|
||
|
|
ts.expectPrinted_("const t1 = { a: 1 } satisfies I1;", "const t1 = { a: 1 };\n");
|
||
|
|
ts.expectPrinted_("const t2 = { a: 1, b: 1 } satisfies I1;", "const t2 = { a: 1, b: 1 };\n");
|
||
|
|
ts.expectPrinted_("const t3 = { } satisfies I1;", "const t3 = {};\n");
|
||
|
|
ts.expectPrinted_("const t4: T1 = { a: 'a' } satisfies T1;", 'const t4 = { a: "a" };\n');
|
||
|
|
ts.expectPrinted_("const t5 = (m => m.substring(0)) satisfies T2;", "const t5 = (m) => m.substring(0);\n");
|
||
|
|
ts.expectPrinted_("const t6 = [1, 2] satisfies [number, number];", "const t6 = [1, 2];\n");
|
||
|
|
ts.expectPrinted_("let t7 = { a: 'test' } satisfies A;", 'let t7 = { a: "test" };\n');
|
||
|
|
ts.expectPrinted_("let t8 = { a: 'test', b: 'test' } satisfies A;", 'let t8 = { a: "test", b: "test" };\n');
|
||
|
|
ts.expectPrinted_("export default {} satisfies Foo;", "export default {};\n");
|
||
|
|
ts.expectPrinted_("export default { a: 1 } satisfies Foo;", "export default { a: 1 };\n");
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"const p = { isEven: n => n % 2 === 0, isOdd: n => n % 2 === 1 } satisfies Predicates;",
|
||
|
|
"const p = { isEven: (n) => n % 2 === 0, isOdd: (n) => n % 2 === 1 };\n",
|
||
|
|
);
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"let obj: { f(s: string): void } & Record<string, unknown> = { f(s) { }, g(s) { } } satisfies { g(s: string): void } & Record<string, unknown>;",
|
||
|
|
"let obj = { f(s) {}, g(s) {} };\n",
|
||
|
|
);
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"const car = { start() { }, move(d) { }, stop() { } } satisfies Movable & Record<string, unknown>;",
|
||
|
|
"const car = { start() {}, move(d) {}, stop() {} };\n",
|
||
|
|
);
|
||
|
|
ts.expectPrinted_("var v = undefined satisfies 1;", "var v = undefined;\n");
|
||
|
|
ts.expectPrinted_("const a = { x: 10 } satisfies Partial<Point2d>;", "const a = { x: 10 };\n");
|
||
|
|
ts.expectPrinted_(
|
||
|
|
'const p = { a: 0, b: "hello", x: 8 } satisfies Partial<Record<Keys, unknown>>;',
|
||
|
|
'const p = { a: 0, b: "hello", x: 8 };\n',
|
||
|
|
);
|
||
|
|
ts.expectPrinted_(
|
||
|
|
'const p = { a: 0, b: "hello", x: 8 } satisfies Record<Keys, unknown>;',
|
||
|
|
'const p = { a: 0, b: "hello", x: 8 };\n',
|
||
|
|
);
|
||
|
|
ts.expectPrinted_('const x2 = { m: true, s: "false" } satisfies Facts;', 'const x2 = { m: true, s: "false" };\n');
|
||
|
|
ts.expectPrinted_(
|
||
|
|
"export const Palette = { white: { r: 255, g: 255, b: 255 }, black: { r: 0, g: 0, d: 0 }, blue: { r: 0, g: 0, b: 255 }, } satisfies Record<string, Color>;",
|
||
|
|
"export const Palette = { white: { r: 255, g: 255, b: 255 }, black: { r: 0, g: 0, d: 0 }, blue: { r: 0, g: 0, b: 255 } };\n",
|
||
|
|
);
|
||
|
|
ts.expectPrinted_('const a: "baz" = "foo" satisfies "foo" | "bar";', 'const a = "foo";\n');
|
||
|
|
ts.expectPrinted_(
|
||
|
|
'const b: { xyz: "baz" } = { xyz: "foo" } satisfies { xyz: "foo" | "bar" };',
|
||
|
|
'const b = { xyz: "foo" };\n',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects using declarations in ambient (declare) contexts", () => {
|
||
|
|
const exp = ts.expectPrinted_;
|
||
|
|
const err = ts.expectParseError;
|
||
|
|
|
||
|
|
// These used to crash the parser with an index-out-of-bounds panic
|
||
|
|
// because ambient ("declare") bindings are never declared as symbols.
|
||
|
|
err("declare await using basic;var", 'Cannot use "declare" with an "await using" declaration');
|
||
|
|
err("declare using basic;var", 'Cannot use "declare" with a "using" declaration');
|
||
|
|
err("declare using x", 'Cannot use "declare" with a "using" declaration');
|
||
|
|
err("declare await using x", 'Cannot use "declare" with an "await using" declaration');
|
||
|
|
err("declare using x = foo()", 'Cannot use "declare" with a "using" declaration');
|
||
|
|
err("declare await using x = foo()", 'Cannot use "declare" with an "await using" declaration');
|
||
|
|
err("declare namespace NS { using x; }", 'Cannot use "declare" with a "using" declaration');
|
||
|
|
err('declare module "m" { using x; }', 'Cannot use "declare" with a "using" declaration');
|
||
|
|
err("declare global { await using x; }", 'Cannot use "declare" with an "await using" declaration');
|
||
|
|
|
||
|
|
// "declare" on other declarations is still erased without error
|
||
|
|
exp("declare const x: number; var y", "var y");
|
||
|
|
exp("declare let x: number; var y", "var y");
|
||
|
|
exp("declare var x: number; var y", "var y");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("generated closures", () => {
|
||
|
|
const input1 = `namespace test {
|
||
|
|
export enum x { y }
|
||
|
|
}`;
|
||
|
|
const output1 = `var test;
|
||
|
|
((test) => {
|
||
|
|
let x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x = test.x ||= {});
|
||
|
|
})(test ||= {})`;
|
||
|
|
|
||
|
|
it("namespace with exported enum", () => {
|
||
|
|
ts.expectPrinted_(input1, output1);
|
||
|
|
});
|
||
|
|
|
||
|
|
const input2 = `export namespace test {
|
||
|
|
export enum x { y }
|
||
|
|
}`;
|
||
|
|
const output2 = `export var test;
|
||
|
|
((test) => {
|
||
|
|
let x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x = test.x ||= {});
|
||
|
|
})(test ||= {})`;
|
||
|
|
|
||
|
|
it("exported namespace with exported enum", () => {
|
||
|
|
ts.expectPrinted_(input2, output2);
|
||
|
|
});
|
||
|
|
|
||
|
|
const input3 = `namespace first {
|
||
|
|
export namespace second {
|
||
|
|
enum x { y }
|
||
|
|
}
|
||
|
|
}`;
|
||
|
|
const output3 = `var first;
|
||
|
|
((first) => {
|
||
|
|
let second;
|
||
|
|
((second) => {
|
||
|
|
let x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x ||= {});
|
||
|
|
})(second = first.second ||= {});
|
||
|
|
})(first ||= {})`;
|
||
|
|
|
||
|
|
it("exported inner namespace", () => {
|
||
|
|
ts.expectPrinted_(input3, output3);
|
||
|
|
});
|
||
|
|
|
||
|
|
const input4 = `export enum x { y }`;
|
||
|
|
const output4 = `export var x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x ||= {})`;
|
||
|
|
|
||
|
|
it("exported enum", () => {
|
||
|
|
ts.expectPrinted_(input4, output4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("enum in a nested scope uses let", () => {
|
||
|
|
// tsc and esbuild both emit "let" for enums that aren't at the top level
|
||
|
|
// so the binding doesn't leak out of the enclosing block/function scope.
|
||
|
|
ts.expectPrinted_(
|
||
|
|
`{ enum x { y } }`,
|
||
|
|
`{
|
||
|
|
let x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x ||= {});
|
||
|
|
}`,
|
||
|
|
);
|
||
|
|
ts.expectPrinted_(
|
||
|
|
`function f() { enum x { y } }`,
|
||
|
|
`function f() {
|
||
|
|
let x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x ||= {});
|
||
|
|
}`,
|
||
|
|
);
|
||
|
|
// Top-level enum still emits "var" so sibling declarations can merge.
|
||
|
|
ts.expectPrinted_(
|
||
|
|
`enum x { y }`,
|
||
|
|
`var x;
|
||
|
|
((x) => {
|
||
|
|
x[x["y"] = 0] = "y";
|
||
|
|
})(x ||= {})`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("enum in a block scope does not leak into the enclosing scope at runtime", async () => {
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`{ enum a { b = 1 } }
|
||
|
|
try {
|
||
|
|
console.log(a);
|
||
|
|
} catch (e) {
|
||
|
|
console.log(e instanceof ReferenceError ? "ReferenceError" : e.constructor.name);
|
||
|
|
}`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
|
||
|
|
expect({ stdout, exitCode }).toEqual({ stdout: "ReferenceError\n", exitCode: 0 });
|
||
|
|
void stderr;
|
||
|
|
});
|
||
|
|
|
||
|
|
// https://github.com/evanw/esbuild/commit/108484982c8f1d74bd87ce172ae02a6ffe8ddce3
|
||
|
|
it("same-named enums in separate block scopes do not merge at runtime", async () => {
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`{
|
||
|
|
enum a { b = 1 }
|
||
|
|
}
|
||
|
|
{
|
||
|
|
enum a { c = 2 }
|
||
|
|
console.log(JSON.stringify({ c: a.c, two: a[2], b: a.b, one: a[1] }));
|
||
|
|
}`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
|
||
|
|
expect({ stdout, exitCode }).toEqual({ stdout: '{"c":2,"two":"c"}\n', exitCode: 0 });
|
||
|
|
void stderr;
|
||
|
|
});
|
||
|
|
|
||
|
|
const input5 = `namespace ns {
|
||
|
|
export class ns {}
|
||
|
|
}`;
|
||
|
|
const output5 = `var ns;
|
||
|
|
((_ns) => {
|
||
|
|
|
||
|
|
class ns {
|
||
|
|
}
|
||
|
|
_ns.ns = ns;
|
||
|
|
})(ns ||= {})`;
|
||
|
|
|
||
|
|
it("namespace argument renamed to avoid a member with the same name", () => {
|
||
|
|
ts.expectPrinted_(input5, output5);
|
||
|
|
});
|
||
|
|
|
||
|
|
const input6 = `namespace m2 {
|
||
|
|
class m2 {}
|
||
|
|
class _m2 {}
|
||
|
|
}`;
|
||
|
|
const output6 = `var m2;
|
||
|
|
((__m2) => {
|
||
|
|
|
||
|
|
class m2 {
|
||
|
|
}
|
||
|
|
|
||
|
|
class _m2 {
|
||
|
|
}
|
||
|
|
})(m2 ||= {})`;
|
||
|
|
|
||
|
|
it("namespace argument does not collide with declarations in the namespace body", () => {
|
||
|
|
ts.expectPrinted_(input6, output6);
|
||
|
|
});
|
||
|
|
|
||
|
|
// The runtime transpiler does not run a renamer, so the generated closure
|
||
|
|
// argument must not shadow declarations inside the namespace body.
|
||
|
|
it("namespace closure argument does not redeclare members at runtime", async () => {
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`namespace m2 {
|
||
|
|
class m2 {}
|
||
|
|
class _m2 {}
|
||
|
|
export const names = [m2.name, _m2.name];
|
||
|
|
}
|
||
|
|
console.log(JSON.stringify(m2.names));`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout).toBe('["m2","_m2"]\n');
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("exports.replace", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
exports: {
|
||
|
|
replace: {
|
||
|
|
// export var foo = function() { }
|
||
|
|
// =>
|
||
|
|
// export var foo = "bar";
|
||
|
|
foo: "bar",
|
||
|
|
|
||
|
|
// export const getStaticProps = /* code */
|
||
|
|
// =>
|
||
|
|
// export var __N_SSG = true;
|
||
|
|
getStaticProps: ["__N_SSG", true],
|
||
|
|
getStaticPaths: ["__N_SSG", true],
|
||
|
|
// export function getStaticProps(ctx) { /* code */ }
|
||
|
|
// =>
|
||
|
|
// export var __N_SSP = true;
|
||
|
|
getServerSideProps: ["__N_SSP", true],
|
||
|
|
},
|
||
|
|
|
||
|
|
// Explicitly remove the top-level export, even if it is in use by
|
||
|
|
// another part of the file
|
||
|
|
eliminate: ["loader", "localVarToRemove"],
|
||
|
|
},
|
||
|
|
/* only per-file for now, so this isn't good yet */
|
||
|
|
treeShaking: true,
|
||
|
|
|
||
|
|
// remove non-bare unused exports, even if they may have side effects
|
||
|
|
// Consistent with tsc & esbuild, this is enabled by default for TypeScript files
|
||
|
|
// this flag lets you enable it for JavaScript files
|
||
|
|
// this already existed, just wasn't exposed in the API
|
||
|
|
trimUnusedImports: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
it("a deletes dead exports and any imports only referenced in dead regions", () => {
|
||
|
|
const out = transpiler.transformSync(`
|
||
|
|
import {getUserById} from './my-database';
|
||
|
|
|
||
|
|
export async function getStaticProps(ctx){
|
||
|
|
return { props: { user: await getUserById(ctx.params.id) } };
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function MyComponent({user}) {
|
||
|
|
getStaticProps();
|
||
|
|
return <div id='user'>{user.name}</div>;
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("deletes dead exports and any imports only referenced in dead regions", () => {
|
||
|
|
const output = transpiler.transformSync(`
|
||
|
|
import deadFS from 'fs';
|
||
|
|
import liveFS from 'fs';
|
||
|
|
|
||
|
|
export var deleteMe = 100;
|
||
|
|
|
||
|
|
export function loader() {
|
||
|
|
deadFS.readFileSync("/etc/passwd");
|
||
|
|
liveFS.readFileSync("/etc/passwd");
|
||
|
|
}
|
||
|
|
|
||
|
|
export function action() {
|
||
|
|
require("foo");
|
||
|
|
liveFS.readFileSync("/etc/passwd")
|
||
|
|
deleteMe = 101;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function baz() {
|
||
|
|
require("bar");
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
expect(output.includes("loader")).toBe(false);
|
||
|
|
expect(output.includes("react")).toBe(false);
|
||
|
|
expect(output.includes("action")).toBe(true);
|
||
|
|
expect(output.includes("deadFS")).toBe(false);
|
||
|
|
expect(output.includes("liveFS")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("supports replacing exports", () => {
|
||
|
|
const output = transpiler.transformSync(`
|
||
|
|
import deadFS from 'fs';
|
||
|
|
import anotherDeadFS from 'fs';
|
||
|
|
import liveFS from 'fs';
|
||
|
|
|
||
|
|
export var localVarToRemove = deadFS.readFileSync("/etc/passwd");
|
||
|
|
export var localVarToReplace = 1;
|
||
|
|
|
||
|
|
var getStaticProps = function () {
|
||
|
|
deadFS.readFileSync("/etc/passwd")
|
||
|
|
};
|
||
|
|
|
||
|
|
export {getStaticProps}
|
||
|
|
|
||
|
|
export function baz() {
|
||
|
|
liveFS.readFileSync("/etc/passwd");
|
||
|
|
require("bar");
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
expect(output.includes("loader")).toBe(false);
|
||
|
|
expect(output.includes("react")).toBe(false);
|
||
|
|
expect(output.includes("deadFS")).toBe(false);
|
||
|
|
expect(output.includes("default")).toBe(false);
|
||
|
|
expect(output.includes("anotherDeadFS")).toBe(false);
|
||
|
|
expect(output.includes("liveFS")).toBe(true);
|
||
|
|
expect(output.includes("__N_SSG")).toBe(true);
|
||
|
|
expect(output.includes("localVarToReplace")).toBe(true);
|
||
|
|
expect(output.includes("localVarToRemove")).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
const bunTranspiler = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
user_undefined: "undefined",
|
||
|
|
},
|
||
|
|
platform: "bun",
|
||
|
|
macro: {
|
||
|
|
inline: {
|
||
|
|
whatDidIPass: `${import.meta.dir}/inline.macro.js`,
|
||
|
|
promiseReturningFunction: `${import.meta.dir}/inline.macro.js`,
|
||
|
|
promiseReturningCtx: `${import.meta.dir}/inline.macro.js`,
|
||
|
|
},
|
||
|
|
react: {
|
||
|
|
bacon: `${import.meta.dir}/macro-check.js`,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
minify: {
|
||
|
|
syntax: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const code = `import { useParams } from "remix";
|
||
|
|
import type { LoaderFunction, ActionFunction } from "remix";
|
||
|
|
import { type xx } from 'mod';
|
||
|
|
import { type xx as yy } from 'mod';
|
||
|
|
import { type 'xx' as yy } from 'mod';
|
||
|
|
import { type if as yy } from 'mod';
|
||
|
|
import React, { type ReactNode, Component as Romponent, Component } from 'react';
|
||
|
|
|
||
|
|
|
||
|
|
export const loader: LoaderFunction = async ({
|
||
|
|
params
|
||
|
|
}) => {
|
||
|
|
console.log(params.postId);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const action: ActionFunction = async ({
|
||
|
|
params
|
||
|
|
}) => {
|
||
|
|
console.log(params.postId);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function PostRoute() {
|
||
|
|
const params = useParams();
|
||
|
|
console.log(params.postId);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
`;
|
||
|
|
|
||
|
|
it.todo("jsxFactory (two level)", () => {
|
||
|
|
var bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
allowBunRuntime: false,
|
||
|
|
tsconfig: JSON.stringify({
|
||
|
|
compilerOptions: {
|
||
|
|
jsxFragmentFactory: "foo.frag",
|
||
|
|
jsx: "react",
|
||
|
|
jsxFactory: "foo.factory",
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
const element = bun.transformSync(`
|
||
|
|
export default <div>hi</div>
|
||
|
|
`);
|
||
|
|
|
||
|
|
expect(element.includes("var $jsxEl = foo.factory;")).toBe(true);
|
||
|
|
|
||
|
|
const fragment = bun.transformSync(`
|
||
|
|
export default <>hi</>
|
||
|
|
`);
|
||
|
|
expect(fragment.includes("var $JSXFrag = foo.frag,")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("jsxFactory (one level)", () => {
|
||
|
|
var bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
allowBunRuntime: false,
|
||
|
|
tsconfig: JSON.stringify({
|
||
|
|
compilerOptions: {
|
||
|
|
jsxFragmentFactory: "foo.frag",
|
||
|
|
jsx: "react",
|
||
|
|
jsxFactory: "h",
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
const element = bun.transformSync(`
|
||
|
|
export default <div>hi</div>
|
||
|
|
`);
|
||
|
|
expect(element.includes("var jsxEl = h;")).toBe(true);
|
||
|
|
|
||
|
|
const fragment = bun.transformSync(`
|
||
|
|
export default <>hi</>
|
||
|
|
`);
|
||
|
|
expect(fragment.includes("var JSXFrag = foo.frag,")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('logLevel: "error" throws', () => {
|
||
|
|
var bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
},
|
||
|
|
logLevel: "error",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(() => bun.transformSync("bad??!?!?!")).toThrow("Unexpected ?");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("invalid logLevel throws", () => {
|
||
|
|
expect(
|
||
|
|
() =>
|
||
|
|
new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
},
|
||
|
|
logLevel: "poop",
|
||
|
|
}),
|
||
|
|
).toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("define with an empty-string key is ignored without leaving uninitialized slots", async () => {
|
||
|
|
// `JSPropertyIterator` skips empty-name properties, but `names`/`values` were being
|
||
|
|
// indexed by the property position instead of a dense counter, leaving garbage in the
|
||
|
|
// skipped slot that `stringHashMapFromArrays` then tried to hash. Run in a subprocess
|
||
|
|
// so a crash surfaces as a test failure instead of taking down the test runner.
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`
|
||
|
|
const t = new Bun.Transpiler({ define: { "": "1", FOO: '"bar"' } });
|
||
|
|
process.stdout.write(t.transformSync("console.log(FOO);", "js"));
|
||
|
|
`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout.trim()).toBe('console.log("bar");');
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("JSX keys", () => {
|
||
|
|
var bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
},
|
||
|
|
logLevel: "error",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div key={() => {}} points={() => {}}></div>);")).toBe(
|
||
|
|
`console.log(jsxDEV_7x81h0kn("div", {
|
||
|
|
points: () => {}
|
||
|
|
}, () => {}, false, undefined, this));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div points={() => {}} key={() => {}}></div>);")).toBe(
|
||
|
|
`console.log(jsxDEV_7x81h0kn("div", {
|
||
|
|
points: () => {}
|
||
|
|
}, () => {}, false, undefined, this));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div key={() => {}} key={() => {}}></div>);")).toBe(
|
||
|
|
'console.log(jsxDEV_7x81h0kn("div", {\n key: () => {}\n}, () => {}, false, undefined, this));\n',
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div key={() => {}}></div>, () => {});")).toBe(
|
||
|
|
'console.log(jsxDEV_7x81h0kn("div", {}, () => {}, false, undefined, this), () => {});\n',
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div key={() => {}} a={() => {}} key={() => {}}></div>, () => {});")).toBe(
|
||
|
|
'console.log(jsxDEV_7x81h0kn("div", {\n key: () => {},\n a: () => {}\n}, () => {}, false, undefined, this), () => {});\n',
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div key={() => {}} key={() => {}} a={() => {}}></div>, () => {});")).toBe(
|
||
|
|
'console.log(jsxDEV_7x81h0kn("div", {\n key: () => {},\n a: () => {}\n}, () => {}, false, undefined, this), () => {});\n',
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div points={() => {}} key={() => {}}></div>);")).toBe(
|
||
|
|
`console.log(jsxDEV_7x81h0kn("div", {
|
||
|
|
points: () => {}
|
||
|
|
}, () => {}, false, undefined, this));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div key={() => {}}></div>);")).toBe(
|
||
|
|
`console.log(jsxDEV_7x81h0kn("div", {}, () => {}, false, undefined, this));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("console.log(<div></div>);")).toBe(
|
||
|
|
`console.log(jsxDEV_7x81h0kn("div", {}, undefined, false, undefined, this));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
// key after spread props
|
||
|
|
// https://github.com/oven-sh/bun/issues/7328
|
||
|
|
expect(bun.transformSync(`console.log(<div {...obj} key="after" />, <div key="before" {...obj} />);`)).toBe(
|
||
|
|
`console.log(createElement_mvmpqhxp(\"div\", {\n ...obj,\n key: \"after\"\n}), jsxDEV_7x81h0kn(\"div\", {\n ...obj\n}, \"before\", false, undefined, this));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync(`console.log(<div {...obj} key="after" {...obj2} />);`)).toBe(
|
||
|
|
`console.log(createElement_mvmpqhxp(\"div\", {\n ...obj,\n key: \"after\",\n ...obj2\n}));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
bun.transformSync(`// @jsx foo;
|
||
|
|
console.log(<div {...obj} key="after" />);`),
|
||
|
|
).toBe(
|
||
|
|
`console.log(createElement_mvmpqhxp(\"div\", {\n ...obj,\n key: \"after\"\n}));
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Non-bundle transpile without `minify.identifiers` uses NoOpRenamer
|
||
|
|
// (prints symbol.original_name verbatim), so the `generatedSymbolName`
|
||
|
|
// hash suffix on the automatic JSX runtime import is the sole collision
|
||
|
|
// guard against a user local of the same name in the first JSX element's
|
||
|
|
// scope. With `minify.identifiers`, MinifyRenamer assigns distinct slots
|
||
|
|
// and the hash suffix is not emitted.
|
||
|
|
it("JSX automatic runtime import is not shadowed by a user local", () => {
|
||
|
|
const input =
|
||
|
|
"export function f() { let jsx: any; let jsxDEV: any; let Fragment: any; return [<><div /></>, jsx, jsxDEV, Fragment] }";
|
||
|
|
|
||
|
|
const noop = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
define: { "process.env.NODE_ENV": JSON.stringify("development") },
|
||
|
|
}).transformSync(input);
|
||
|
|
expect(noop).toContain("jsxDEV_7x81h0kn(");
|
||
|
|
expect(noop).toContain("Fragment_8vg9x3sq,");
|
||
|
|
expect(noop).toContain("let jsx;");
|
||
|
|
expect(noop).toContain("let jsxDEV;");
|
||
|
|
expect(noop).toContain("let Fragment;");
|
||
|
|
expect(noop).not.toMatch(/\bjsxDEV\(/);
|
||
|
|
|
||
|
|
const minified = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
define: { "process.env.NODE_ENV": JSON.stringify("development") },
|
||
|
|
minify: { identifiers: true },
|
||
|
|
autoImportJSX: true,
|
||
|
|
}).transformSync(input);
|
||
|
|
expect(minified).not.toContain("_7x81h0kn");
|
||
|
|
expect(minified).not.toContain("_8vg9x3sq");
|
||
|
|
// Import aliases must not collide with the user's `let` bindings.
|
||
|
|
const aliases = [...minified.matchAll(/\b\w+ as (\w+)\b/g)].map(m => m[1]);
|
||
|
|
const locals = [...minified.matchAll(/\blet (\w+);/g)].map(m => m[1]);
|
||
|
|
expect(aliases).toHaveLength(2);
|
||
|
|
expect(locals).toHaveLength(3);
|
||
|
|
for (const alias of aliases) {
|
||
|
|
expect(locals).not.toContain(alias);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("JSX bare key prop followed by key with a value does not crash", async () => {
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`
|
||
|
|
const t = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: { "process.env.NODE_ENV": JSON.stringify("development") },
|
||
|
|
logLevel: "error",
|
||
|
|
});
|
||
|
|
process.stdout.write(t.transformSync('console.log(<div key key="duplicate"></div>);'));
|
||
|
|
process.stdout.write(t.transformSync('console.log(<div key className="x" key="duplicate"></div>);'));
|
||
|
|
`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout).toBe(
|
||
|
|
'console.log(jsxDEV_7x81h0kn("div", {}, "duplicate", false, undefined, this));\n' +
|
||
|
|
'console.log(jsxDEV_7x81h0kn("div", {\n className: "x"\n}, "duplicate", false, undefined, this));\n',
|
||
|
|
);
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("parses TSX arrow functions correctly", () => {
|
||
|
|
var transpiler = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
});
|
||
|
|
expect(transpiler.transformSync("console.log(A = <T = unknown,>() => null)")).toBe(
|
||
|
|
"console.log(A = () => null);\n",
|
||
|
|
);
|
||
|
|
expect(transpiler.transformSync("const B = <T extends string>() => null")).toBe("const B = () => null;\n");
|
||
|
|
expect(transpiler.transformSync("const element = <T extends/>")).toContain("jsxDEV");
|
||
|
|
expect(transpiler.transformSync("const element2 = <T extends={true}/>")).toContain("jsxDEV");
|
||
|
|
expect(transpiler.transformSync("const element3 = <T extends></T>")).toContain("jsxDEV");
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("JSX", () => {
|
||
|
|
var bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(bun.transformSync("export var foo = <div foo />")).toBe(
|
||
|
|
`export var foo = jsxDEV_7x81h0kn("div", {
|
||
|
|
foo: true
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var foo = <div foo={foo} />")).toBe(
|
||
|
|
`export var foo = jsxDEV_7x81h0kn("div", {
|
||
|
|
foo
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var foo = <div {...foo} />")).toBe(
|
||
|
|
`export var foo = jsxDEV_7x81h0kn("div", {
|
||
|
|
...foo
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("export var hi = <div {foo} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
foo
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var hi = <div {foo.bar.baz} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
baz: foo.bar.baz
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var hi = <div {foo?.bar?.baz} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
baz: foo?.bar?.baz
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var hi = <div {foo['baz'].bar?.baz} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
baz: foo["baz"].bar?.baz
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
// cursed
|
||
|
|
expect(bun.transformSync("export var hi = <div {foo[() => true].hi} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
hi: foo[() => true].hi
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var hi = <Foo {process.env.NODE_ENV} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn(Foo, {
|
||
|
|
NODE_ENV: "development"
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("export var hi = <div {foo['baz'].bar?.baz} />")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
baz: foo["baz"].bar?.baz
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
try {
|
||
|
|
bun.transformSync("export var hi = <div {foo}={foo}= />");
|
||
|
|
throw new Error("Expected error");
|
||
|
|
} catch (e) {
|
||
|
|
expect(e.errors[0].message.includes('Expected ">"')).toBe(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(bun.transformSync("export var hi = <div {Foo}><Foo></Foo></div>")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
Foo,
|
||
|
|
children: jsxDEV_7x81h0kn(Foo, {}, undefined, false, undefined, this)
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
expect(bun.transformSync("export var hi = <div {Foo}><Foo></Foo></div>")).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
Foo,
|
||
|
|
children: jsxDEV_7x81h0kn(Foo, {}, undefined, false, undefined, this)
|
||
|
|
}, undefined, false, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("export var hi = <div>{123}}</div>").trim()).toBe(
|
||
|
|
`export var hi = jsxDEV_7x81h0kn("div", {
|
||
|
|
children: [
|
||
|
|
123,
|
||
|
|
"}"
|
||
|
|
]
|
||
|
|
}, undefined, true, undefined, this);
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("JSX spread children", () => {
|
||
|
|
var bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(bun.transformSync("export var foo = <div>{...a}b</div>")).toBe(
|
||
|
|
`export var foo = jsxDEV_7x81h0kn("div", {
|
||
|
|
children: [
|
||
|
|
...a,
|
||
|
|
"b"
|
||
|
|
]
|
||
|
|
}, undefined, true, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(bun.transformSync("export var foo = <div>{...a}</div>")).toBe(
|
||
|
|
`export var foo = jsxDEV_7x81h0kn("div", {
|
||
|
|
children: [...a]
|
||
|
|
}, undefined, true, undefined, this);
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("JSX tag names containing '-' or ':' are string tags regardless of case", () => {
|
||
|
|
// Matches esbuild/Babel/TypeScript: a dashed (custom element) or namespaced
|
||
|
|
// name is never a component reference, even when it starts uppercase.
|
||
|
|
const bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
for (const [tag, expected] of [
|
||
|
|
["Foo-Bar", `"Foo-Bar"`],
|
||
|
|
["Ns:Comp", `"Ns:Comp"`],
|
||
|
|
["my-el", `"my-el"`],
|
||
|
|
["svg:path", `"svg:path"`],
|
||
|
|
["Foo", `Foo`],
|
||
|
|
["div", `"div"`],
|
||
|
|
]) {
|
||
|
|
expect(bun.transformSync(`export var foo = <${tag} />`)).toBe(
|
||
|
|
`export var foo = jsxDEV_7x81h0kn(${expected}, {}, undefined, false, undefined, this);\n`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// https://github.com/oven-sh/bun/issues/30958
|
||
|
|
// A numeric JSX entity outside the Unicode range (0..=0x10FFFF) used to
|
||
|
|
// trip a debug_assert in u16_lead (src/bun_core/lib.rs) when the lexer
|
||
|
|
// encoded the value as a UTF-16 surrogate pair. The lexer must reject the
|
||
|
|
// entity with the same "JSX entity escape is too big" diagnostic it emits
|
||
|
|
// for i32-overflow, instead of forwarding an invalid code point.
|
||
|
|
describe("rejects out-of-range numeric JSX entities without panicking", () => {
|
||
|
|
const bun = new Bun.Transpiler({
|
||
|
|
loader: "jsx",
|
||
|
|
define: { "process.env.NODE_ENV": JSON.stringify("development") },
|
||
|
|
});
|
||
|
|
|
||
|
|
// Covers: the original fuzzer input (0x76B22B), max+1, i32::MAX, and the
|
||
|
|
// negative-i32 path — parse_int::<i32> accepts a leading '-' so `&#-1;`
|
||
|
|
// lands as a negative CodePoint that must be rejected before it reaches
|
||
|
|
// the u32 cast in push_codepoint_utf16.
|
||
|
|
it.each(["�", "�", "�", "&#-1;"])("%s is rejected", entity => {
|
||
|
|
expect(() => bun.transformSync(`export var x = <div>${entity}</div>`)).toThrow(/JSX entity escape is too big/);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Boundary: 0x10FFFF is the maximum valid code point and must still work.
|
||
|
|
it(" (max valid) still transpiles", () => {
|
||
|
|
expect(bun.transformSync("export var x = <div></div>")).toContain("jsxDEV_");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("require with a dynamic non-string expression", () => {
|
||
|
|
var nodeTranspiler = new Bun.Transpiler({ platform: "node" });
|
||
|
|
expect(nodeTranspiler.transformSync("require('hi' + bar)")).toBe('require("hi" + bar);\n');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("CommonJS", () => {
|
||
|
|
var nodeTranspiler = new Bun.Transpiler({ platform: "node", minify: { syntax: false } });
|
||
|
|
|
||
|
|
// note: even if minify syntax is off, constant folding must happen within require calls
|
||
|
|
expect(nodeTranspiler.transformSync("module.require('hi' + 123)")).toBe('require("hi123");\n');
|
||
|
|
|
||
|
|
expect(nodeTranspiler.transformSync("module.require(1 ? 'foo' : 'bar')")).toBe('require("foo");\n');
|
||
|
|
expect(nodeTranspiler.transformSync("require(1 ? 'foo' : 'bar')")).toBe('require("foo");\n');
|
||
|
|
|
||
|
|
expect(nodeTranspiler.transformSync("module.require(unknown ? 'foo' : 'bar')")).toBe(
|
||
|
|
'unknown ? require("foo") : require("bar");\n',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("regressions", () => {
|
||
|
|
it("unexpected super", () => {
|
||
|
|
const input = `
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const ErrorReportingMixinBase = require('./mixin-base');
|
||
|
|
const PositionTrackingPreprocessorMixin = require('../position-tracking/preprocessor-mixin');
|
||
|
|
const Mixin = require('../../utils/mixin');
|
||
|
|
|
||
|
|
class ErrorReportingPreprocessorMixin extends ErrorReportingMixinBase {
|
||
|
|
constructor(preprocessor, opts) {
|
||
|
|
super(preprocessor, opts);
|
||
|
|
|
||
|
|
this.posTracker = Mixin.install(preprocessor, PositionTrackingPreprocessorMixin);
|
||
|
|
this.lastErrOffset = -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
_reportError(code) {
|
||
|
|
//NOTE: avoid reporting error twice on advance/retreat
|
||
|
|
if (this.lastErrOffset !== this.posTracker.offset) {
|
||
|
|
this.lastErrOffset = this.posTracker.offset;
|
||
|
|
super._reportError(code);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = ErrorReportingPreprocessorMixin;
|
||
|
|
|
||
|
|
|
||
|
|
`;
|
||
|
|
expect(transpiler.transformSync(input, "js").length > 0).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("scanImports", () => {
|
||
|
|
it("reports import paths, excluding types", () => {
|
||
|
|
const imports = transpiler.scanImports(code, "tsx");
|
||
|
|
expect(imports.filter(({ path }) => path === "remix")).toHaveLength(1);
|
||
|
|
expect(imports.filter(({ path }) => path === "mod")).toHaveLength(0);
|
||
|
|
expect(imports.filter(({ path }) => path === "react")).toHaveLength(1);
|
||
|
|
expect(imports).toHaveLength(2);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
const parsed = (code, trim = true, autoExport = false, transpiler_ = transpiler) => {
|
||
|
|
if (autoExport) {
|
||
|
|
code = "export default (" + code + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
var out = transpiler_.transformSync(code, "js");
|
||
|
|
if (autoExport && out.startsWith("export default ")) {
|
||
|
|
out = out.substring("export default ".length);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (trim) {
|
||
|
|
out = out.trim();
|
||
|
|
|
||
|
|
if (out.endsWith(";")) {
|
||
|
|
out = out.substring(0, out.length - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
return out.trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrinted = (code, out) => {
|
||
|
|
expect(parsed(code, true, true)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrinted_ = (code, out) => {
|
||
|
|
expect(parsed(code, !out.endsWith(";\n"), false)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrintedMin_ = (code, out) => {
|
||
|
|
expect(parsed(code, !out.endsWith(";\n"), false, transpilerMinifySyntax)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrintedNoTrim = (code, out) => {
|
||
|
|
expect(parsed(code, false, false)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectBunPrinted_ = (code, out) => {
|
||
|
|
expect(parsed(code, !out.endsWith(";\n"), false, bunTranspiler)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectParseError = (code, message) => {
|
||
|
|
try {
|
||
|
|
parsed(code, false, false);
|
||
|
|
} catch (er) {
|
||
|
|
var err = er;
|
||
|
|
if (er instanceof AggregateError) {
|
||
|
|
err = err.errors[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(er.message).toBe(message);
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("Expected parse error for code\n\t" + code);
|
||
|
|
};
|
||
|
|
|
||
|
|
describe("parser", () => {
|
||
|
|
it("arrays", () => {
|
||
|
|
expectPrinted("[]", "[]");
|
||
|
|
expectPrinted("[,]", "[,]");
|
||
|
|
expectPrinted("[1]", "[1]");
|
||
|
|
expectPrinted("[1,]", "[1]");
|
||
|
|
expectPrinted("[,1]", "[, 1]");
|
||
|
|
expectPrinted("[1,2]", "[1, 2]");
|
||
|
|
expectPrinted("[,1,2]", "[, 1, 2]");
|
||
|
|
expectPrinted("[1,,2]", "[1, , 2]");
|
||
|
|
expectPrinted("[1,2,]", "[1, 2]");
|
||
|
|
expectPrinted("[1,2,,]", "[1, 2, ,]");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("exponentiation", () => {
|
||
|
|
expectPrinted("(delete x) ** 0", "(delete x) ** 0");
|
||
|
|
expectPrinted("(delete x.prop) ** 0", "(delete x.prop) ** 0");
|
||
|
|
expectPrinted("(delete x[0]) ** 0", "(delete x[0]) ** 0");
|
||
|
|
|
||
|
|
expectPrinted("(delete x?.prop) ** 0", "(delete x?.prop) ** 0");
|
||
|
|
|
||
|
|
expectPrinted("(void x) ** 0", "(void x) ** 0");
|
||
|
|
expectPrinted("(typeof x) ** 0", "(typeof x) ** 0");
|
||
|
|
expectPrinted("(+x) ** 0", "(+x) ** 0");
|
||
|
|
expectPrinted("(-x) ** 0", "(-x) ** 0");
|
||
|
|
expectPrinted("(~x) ** 0", "(~x) ** 0");
|
||
|
|
expectPrinted("(!x) ** 0", "(!x) ** 0");
|
||
|
|
expectPrinted("(await x) ** 0", "(await x) ** 0");
|
||
|
|
expectPrinted("(await -x) ** 0", "(await -x) ** 0");
|
||
|
|
|
||
|
|
expectPrinted("--x ** 2", "--x ** 2");
|
||
|
|
expectPrinted("++x ** 2", "++x ** 2");
|
||
|
|
expectPrinted("x-- ** 2", "x-- ** 2");
|
||
|
|
expectPrinted("x++ ** 2", "x++ ** 2");
|
||
|
|
|
||
|
|
expectPrinted("(-x) ** 2", "(-x) ** 2");
|
||
|
|
expectPrinted("(+x) ** 2", "(+x) ** 2");
|
||
|
|
expectPrinted("(~x) ** 2", "(~x) ** 2");
|
||
|
|
expectPrinted("(!x) ** 2", "(!x) ** 2");
|
||
|
|
expectPrinted("(-1) ** 2", "(-1) ** 2");
|
||
|
|
expectPrinted("(+1) ** 2", "1 ** 2");
|
||
|
|
expectPrinted("(~1) ** 2", "(~1) ** 2");
|
||
|
|
expectPrinted("(!1) ** 2", "false ** 2");
|
||
|
|
expectPrinted("(void x) ** 2", "(void x) ** 2");
|
||
|
|
expectPrinted("(delete x) ** 2", "(delete x) ** 2");
|
||
|
|
expectPrinted("(typeof x) ** 2", "(typeof x) ** 2");
|
||
|
|
expectPrinted("undefined ** 2", "undefined ** 2");
|
||
|
|
|
||
|
|
expectParseError("-x ** 2", "Unexpected **");
|
||
|
|
expectParseError("+x ** 2", "Unexpected **");
|
||
|
|
expectParseError("~x ** 2", "Unexpected **");
|
||
|
|
expectParseError("!x ** 2", "Unexpected **");
|
||
|
|
expectParseError("void x ** 2", "Unexpected **");
|
||
|
|
expectParseError("delete x ** 2", "Unexpected **");
|
||
|
|
expectParseError("typeof x ** 2", "Unexpected **");
|
||
|
|
|
||
|
|
expectParseError("-x.y() ** 2", "Unexpected **");
|
||
|
|
expectParseError("+x.y() ** 2", "Unexpected **");
|
||
|
|
expectParseError("~x.y() ** 2", "Unexpected **");
|
||
|
|
expectParseError("!x.y() ** 2", "Unexpected **");
|
||
|
|
expectParseError("void x.y() ** 2", "Unexpected **");
|
||
|
|
expectParseError("delete x.y() ** 2", "Unexpected **");
|
||
|
|
expectParseError("typeof x.y() ** 2", "Unexpected **");
|
||
|
|
|
||
|
|
expectParseError("delete x ** 0", "Unexpected **");
|
||
|
|
expectParseError("delete x.prop ** 0", "Unexpected **");
|
||
|
|
expectParseError("delete x[0] ** 0", "Unexpected **");
|
||
|
|
expectParseError("delete x?.prop ** 0", "Unexpected **");
|
||
|
|
expectParseError("void x ** 0", "Unexpected **");
|
||
|
|
expectParseError("typeof x ** 0", "Unexpected **");
|
||
|
|
expectParseError("+x ** 0", "Unexpected **");
|
||
|
|
expectParseError("-x ** 0", "Unexpected **");
|
||
|
|
expectParseError("~x ** 0", "Unexpected **");
|
||
|
|
expectParseError("!x ** 0", "Unexpected **");
|
||
|
|
expectParseError("await x ** 0", "Unexpected **");
|
||
|
|
expectParseError("await -x ** 0", "Unexpected **");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("for-of loop variable named async", () => {
|
||
|
|
// "\u0061sync" is the identifier `async`, which is legal as a for-of loop
|
||
|
|
// variable, but printing it as the raw token sequence `async of` is a
|
||
|
|
// syntax error, so the printer must parenthesize it.
|
||
|
|
expectPrinted_("for (\\u0061sync of [7]);", "for ((async) of [7])\n ;\n");
|
||
|
|
expectPrinted_("for ((async) of [7]);", "for ((async) of [7])\n ;\n");
|
||
|
|
expectPrinted_(
|
||
|
|
"async function f() { for await (\\u0061sync of [7]); }",
|
||
|
|
"async function f() {\n for await ((async) of [7])\n ;\n}",
|
||
|
|
);
|
||
|
|
|
||
|
|
// The same identifier needs no parentheses when it is not directly followed by `of`
|
||
|
|
expectPrinted_("for (async.x of [7]);", "for (async.x of [7])\n ;\n");
|
||
|
|
expectPrinted_("for (\\u0061sync[0] of [7]);", "for (async[0] of [7])\n ;\n");
|
||
|
|
expectPrinted_("for (x[\\u0061sync] of [7]);", "for (x[async] of [7])\n ;\n");
|
||
|
|
expectPrinted_("for (\\u0061sync in x);", "for (async in x)\n ;\n");
|
||
|
|
|
||
|
|
// `let` as a for-of loop variable keeps its parentheses too
|
||
|
|
expectPrinted_("for ((let) of [7]);", "for ((let) of [7])\n ;\n");
|
||
|
|
|
||
|
|
// The keyword spelling is a syntax error, which is why the parentheses matter
|
||
|
|
expect(() => parsed("for (async of [7]);", false, false)).toThrow();
|
||
|
|
expectParseError("for (async\nof [7]);", 'For loop initializers cannot start with "async of"');
|
||
|
|
expectPrinted_(
|
||
|
|
"async function f() { for await (async\nof [7]); }",
|
||
|
|
"async function f() {\n for await ((async) of [7])\n ;\n}",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("await", () => {
|
||
|
|
expectPrinted("await x", "await x");
|
||
|
|
expectPrinted("await +x", "await +x");
|
||
|
|
expectPrinted("await -x", "await -x");
|
||
|
|
expectPrinted("await ~x", "await ~x");
|
||
|
|
expectPrinted("await !x", "await !x");
|
||
|
|
expectPrinted("await --x", "await --x");
|
||
|
|
expectPrinted("await ++x", "await ++x");
|
||
|
|
expectPrinted("await x--", "await x--");
|
||
|
|
expectPrinted("await x++", "await x++");
|
||
|
|
expectPrinted("await void x", "await void x");
|
||
|
|
expectPrinted("await typeof x", "await typeof x");
|
||
|
|
expectPrinted("await (x * y)", "await (x * y)");
|
||
|
|
expectPrinted("await (x ** y)", "await (x ** y)");
|
||
|
|
|
||
|
|
expectPrinted_("async function f() { await delete x }", "async function f() {\n await delete x;\n}");
|
||
|
|
|
||
|
|
// expectParseError(
|
||
|
|
// "await delete x",
|
||
|
|
// "Delete of a bare identifier cannot be used in an ECMAScript module"
|
||
|
|
// );
|
||
|
|
});
|
||
|
|
|
||
|
|
it("import assert", () => {
|
||
|
|
expectPrinted_(`import json from "./foo.json" assert { type: "json" };`, `import json from "./foo.json"`);
|
||
|
|
expectPrinted_(`import json from "./foo.json";`, `import json from "./foo.json"`);
|
||
|
|
expectPrinted_(`import("./foo.json", { type: "json" });`, `import("./foo.json", { type: \"json\" })`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("import with unicode", () => {
|
||
|
|
expectPrinted_(`import { name } from 'modထ';`, `import { name } from "modထ"`);
|
||
|
|
expectPrinted_(`import { name } from 'mod\\u1011';`, `import { name } from "modထ"`);
|
||
|
|
expectPrinted_(`import('modထ');`, `import("modထ")`);
|
||
|
|
expectPrinted_(`import('mod\\u1011');`, `import("modထ")`);
|
||
|
|
});
|
||
|
|
it("import with quote", () => {
|
||
|
|
expectPrinted_(`import { name } from '".ts';`, `import { name } from '".ts'`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("import with separator-only or trailing-separator path", () => {
|
||
|
|
expectPrinted_(`import "/"`, `import"/"`);
|
||
|
|
expectPrinted_(`import "//"`, `import"//"`);
|
||
|
|
expectPrinted_(`import "///"`, `import"///"`);
|
||
|
|
expectPrinted_(`import "foo//"`, `import"foo//"`);
|
||
|
|
expectPrinted_(`import "foo///"`, `import"foo///"`);
|
||
|
|
expectPrinted_(`export * from "/"`, `export * from "/"`);
|
||
|
|
expectPrinted_(`export * from "foo//"`, `export * from "foo//"`);
|
||
|
|
expectPrinted_(`export { a } from "/"`, `export { a } from "/"`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("empty string as import/export clause alias", () => {
|
||
|
|
// ModuleExportName may be any well-formed string literal, including "".
|
||
|
|
expectPrinted_(`import { "" as z } from "m"; z`, `import { "" as z } from "m";\nz`);
|
||
|
|
expectPrinted_(`let z = 1; export { z as "" }`, `let z = 1;\n\nexport { z as "" }`);
|
||
|
|
expectPrinted_(`export { "" as z } from "m"`, `export { "" as z } from "m"`);
|
||
|
|
expectPrinted_(`export { z as "" } from "m"`, `export { z as "" } from "m"`);
|
||
|
|
expectPrinted_(`export { "" as "" } from "m"`, `export { "" } from "m"`);
|
||
|
|
expectPrinted_(`export * as "" from "m"`, `export * as "" from "m"`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("string quote selection", () => {
|
||
|
|
expectPrinted_(`console.log("\\n")`, "console.log(`\n`)");
|
||
|
|
expectPrinted_(`console.log("\\"")`, `console.log('"')`);
|
||
|
|
expectPrinted_(`console.log('\\'')`, `console.log("'")`);
|
||
|
|
expectPrinted_("console.log(`\\`hi\\``)", "console.log(`\\`hi\\``)");
|
||
|
|
expectPrinted_(`console.log("ထ")`, `console.log("ထ")`);
|
||
|
|
expectPrinted_(`console.log("\\u1011")`, `console.log("ထ")`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("unicode surrogates", () => {
|
||
|
|
expectPrinted_(`console.log("𐌴")`, 'console.log("\\uD800\\uDF34")');
|
||
|
|
expectPrinted_(`console.log("\\u{10334}")`, 'console.log("\\uD800\\uDF34")');
|
||
|
|
expectPrinted_(`console.log("\\uD800\\uDF34")`, 'console.log("\\uD800\\uDF34")');
|
||
|
|
expectPrinted_(`console.log("\\u{10334}" === "\\uD800\\uDF34")`, "console.log(true)");
|
||
|
|
expectPrinted_(`console.log("\\u{10334}" === "\\uDF34\\uD800")`, "console.log(false)");
|
||
|
|
expectPrintedMin_(`console.log("abc" + "def")`, 'console.log("abcdef")');
|
||
|
|
expectPrintedMin_(`console.log("\\uD800" + "\\uDF34")`, 'console.log("\\uD800" + "\\uDF34")');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("fold string addition", () => {
|
||
|
|
expectPrinted_(
|
||
|
|
`
|
||
|
|
const a = "[^aeiou]";
|
||
|
|
const b = a + "[^aeiouy]*";
|
||
|
|
console.log(a);
|
||
|
|
`,
|
||
|
|
`
|
||
|
|
const a = "[^aeiou]";
|
||
|
|
const b = a + "[^aeiouy]*";
|
||
|
|
console.log(a)
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
|
||
|
|
expectPrintedMin_(`export const foo = "a" + "b";`, `export const foo = "ab"`);
|
||
|
|
expectPrintedMin_(
|
||
|
|
`export const foo = "F" + "0" + "F" + "0123456789" + "ABCDEF" + "0123456789ABCDEFF0123456789ABCDEF00" + "b";`,
|
||
|
|
`export const foo = "F0F0123456789ABCDEF0123456789ABCDEFF0123456789ABCDEF00b"`,
|
||
|
|
);
|
||
|
|
expectPrintedMin_(`export const foo = "a" + 1 + "b";`, `export const foo = "a1b"`);
|
||
|
|
expectPrintedMin_(`export const foo = "a" + "b" + 1 + "b";`, `export const foo = "ab1b"`);
|
||
|
|
expectPrintedMin_(`export const foo = "a" + "b" + 1 + "b" + "c";`, `export const foo = "ab1bc"`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("numeric constants", () => {
|
||
|
|
expectBunPrinted_("export const foo = 1 + 2", "export const foo = 3");
|
||
|
|
expectBunPrinted_("export const foo = 1 - 2", "export const foo = -1");
|
||
|
|
expectBunPrinted_("export const foo = 1 * 2", "export const foo = 2");
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("pass objects to macros", () => {
|
||
|
|
var object = {
|
||
|
|
helloooooooo: {
|
||
|
|
message: [12345],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const output = bunTranspiler.transformSync(
|
||
|
|
`
|
||
|
|
import {whatDidIPass} from 'inline';
|
||
|
|
|
||
|
|
export function foo() {
|
||
|
|
return whatDidIPass();
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
object,
|
||
|
|
);
|
||
|
|
expect(output).toBe(`export function foo() {
|
||
|
|
return {
|
||
|
|
helloooooooo: {
|
||
|
|
message: [
|
||
|
|
12345
|
||
|
|
]
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("macros can return a promise", () => {
|
||
|
|
var object = {
|
||
|
|
helloooooooo: {
|
||
|
|
message: [12345],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const output = bunTranspiler.transformSync(
|
||
|
|
`
|
||
|
|
import {promiseReturningFunction} from 'inline';
|
||
|
|
|
||
|
|
export function foo() {
|
||
|
|
return promiseReturningFunction();
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
object,
|
||
|
|
);
|
||
|
|
expect(output).toBe(`export function foo() {
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("macros can return a Response body", () => {
|
||
|
|
// "promiseReturningCtx" is this:
|
||
|
|
// export function promiseReturningCtx(expr, ctx) {
|
||
|
|
// return new Promise((resolve, reject) => {
|
||
|
|
// setTimeout(() => {
|
||
|
|
// resolve(ctx);
|
||
|
|
// }, 1);
|
||
|
|
// });
|
||
|
|
// }
|
||
|
|
var object = Response.json({ hello: "world" });
|
||
|
|
|
||
|
|
const input = `
|
||
|
|
import {promiseReturningCtx} from 'inline';
|
||
|
|
|
||
|
|
export function foo() {
|
||
|
|
return promiseReturningCtx();
|
||
|
|
}
|
||
|
|
`.trim();
|
||
|
|
|
||
|
|
const output = `
|
||
|
|
export function foo() {
|
||
|
|
return { hello: "world" };
|
||
|
|
}
|
||
|
|
`.trim();
|
||
|
|
|
||
|
|
expect(bunTranspiler.transformSync(input, object).trim()).toBe(output);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("macros get dead code eliminated", () => {
|
||
|
|
var object = Response.json({
|
||
|
|
big: {
|
||
|
|
object: {
|
||
|
|
beep: "boop",
|
||
|
|
huge: 123,
|
||
|
|
},
|
||
|
|
blobby: {
|
||
|
|
beep: "boop",
|
||
|
|
huge: 123,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
dead: "hello world!",
|
||
|
|
});
|
||
|
|
|
||
|
|
const input = `
|
||
|
|
import {promiseReturningCtx} from 'inline';
|
||
|
|
|
||
|
|
export const {dead} = promiseReturningCtx();
|
||
|
|
`.trim();
|
||
|
|
|
||
|
|
const output = `
|
||
|
|
export const { dead } = { dead: "hello world!" };
|
||
|
|
`.trim();
|
||
|
|
|
||
|
|
expect(bunTranspiler.transformSync(input, object).trim()).toBe(output);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rewrite string to length", () => {
|
||
|
|
expectBunPrinted_(`export const foo = "a".length + "b".length;`, `export const foo = 2`);
|
||
|
|
// check rope string
|
||
|
|
expectBunPrinted_(`export const foo = ("a" + "b").length;`, `export const foo = 2`);
|
||
|
|
expectBunPrinted_(
|
||
|
|
// check UTF-16
|
||
|
|
`export const foo = "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌".length;`,
|
||
|
|
`export const foo = 52`,
|
||
|
|
);
|
||
|
|
// no rope string for non-ascii
|
||
|
|
expectBunPrinted_(`export const foo = ("æ" + "™").length;`, `export const foo = ("æ" + "™").length`);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("Bun.js", () => {
|
||
|
|
it.todo("require -> import.meta.require", () => {
|
||
|
|
expectBunPrinted_(
|
||
|
|
`export const foo = require('bar.node')`,
|
||
|
|
`export const foo = import.meta.require("bar.node")`,
|
||
|
|
);
|
||
|
|
expectBunPrinted_(
|
||
|
|
`export const foo = require('bar.node')`,
|
||
|
|
`export const foo = import.meta.require("bar.node")`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("require.resolve -> import.meta.require.resolve", () => {
|
||
|
|
expectBunPrinted_(
|
||
|
|
`export const foo = require.resolve('bar.node')`,
|
||
|
|
`export const foo = import.meta.require.resolve("bar.node")`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo('require.resolve(path, {paths: ["blah"]}) -> import.meta.require.resolve', () => {
|
||
|
|
expectBunPrinted_(
|
||
|
|
`export const foo = require.resolve('bar.node', {paths: ["blah"]})`,
|
||
|
|
`export const foo = import.meta.require.resolve("bar.node", { paths: ["blah"] })`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("require is defined", () => {
|
||
|
|
expectBunPrinted_(
|
||
|
|
`
|
||
|
|
const {resolve} = require;
|
||
|
|
console.log(resolve.length)
|
||
|
|
`.trim(),
|
||
|
|
`
|
||
|
|
const { resolve } = import.meta.require;
|
||
|
|
console.log(resolve.length)
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("Browsers", () => {
|
||
|
|
it('require.resolve("my-module") is untouched', () => {
|
||
|
|
// we used to inline the string for this, but that is always incorrect as require.resolve builds an exact path.
|
||
|
|
expectPrinted_(
|
||
|
|
`export const foo = require.resolve('my-module')`,
|
||
|
|
`export const foo = require.resolve("my-module")`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("define", () => {
|
||
|
|
expectPrinted_(`export default typeof user_undefined === 'undefined';`, `export default true`);
|
||
|
|
expectPrinted_(`export default typeof user_undefined !== 'undefined';`, `export default false`);
|
||
|
|
|
||
|
|
expectPrinted_(`export default typeof user_undefined !== 'undefined';`, `export default false`);
|
||
|
|
expectPrinted_(`export default !user_undefined;`, `export default true`);
|
||
|
|
|
||
|
|
expectPrinted_(`export default user_nested;`, `export default location.origin`);
|
||
|
|
expectPrinted_("hello.earth('hi')", 'hello.mars("hi")');
|
||
|
|
expectPrinted_("Math.log('hi')", 'console.error("hi")');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("jsx symbol should work", () => {
|
||
|
|
expectBunPrinted_(`var x = jsx; export default x;`, "var x = jsx;\nexport default x");
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("decls", () => {
|
||
|
|
// expectParseError("var x = 0", "");
|
||
|
|
// expectParseError("let x = 0", "");
|
||
|
|
// expectParseError("const x = 0", "");
|
||
|
|
// expectParseError("for (var x = 0;;) ;", "");
|
||
|
|
// expectParseError("for (let x = 0;;) ;", "");
|
||
|
|
// expectParseError("for (const x = 0;;) ;", "");
|
||
|
|
|
||
|
|
// expectParseError("for (var x in y) ;", "");
|
||
|
|
// expectParseError("for (let x in y) ;", "");
|
||
|
|
// expectParseError("for (const x in y) ;", "");
|
||
|
|
// expectParseError("for (var x of y) ;", "");
|
||
|
|
// expectParseError("for (let x of y) ;", "");
|
||
|
|
// expectParseError("for (const x of y) ;", "");
|
||
|
|
|
||
|
|
// expectParseError("var x", "");
|
||
|
|
// expectParseError("let x", "");
|
||
|
|
expectParseError("const x", 'The constant "x" must be initialized');
|
||
|
|
expectParseError("const {}", "This constant must be initialized");
|
||
|
|
expectParseError("const []", "This constant must be initialized");
|
||
|
|
// expectParseError("for (var x;;) ;", "");
|
||
|
|
// expectParseError("for (let x;;) ;", "");
|
||
|
|
expectParseError("for (const x;;) ;", 'The constant "x" must be initialized');
|
||
|
|
expectParseError("for (const {};;) ;", "This constant must be initialized");
|
||
|
|
expectParseError("for (const [];;) ;", "This constant must be initialized");
|
||
|
|
|
||
|
|
// Make sure bindings are visited during parsing
|
||
|
|
expectPrinted_("var {[x]: y} = {}", "var { [x]: y } = {}");
|
||
|
|
expectPrinted_("var {...x} = {}", "var { ...x } = {}");
|
||
|
|
|
||
|
|
// Test destructuring patterns
|
||
|
|
expectPrinted_("var [...x] = []", "var [...x] = []");
|
||
|
|
expectPrinted_("var {...x} = {}", "var { ...x } = {}");
|
||
|
|
|
||
|
|
expectPrinted_("export var foo = ([...x] = []) => {}", "export var foo = ([...x] = []) => {}");
|
||
|
|
|
||
|
|
expectPrinted_("export var foo = ({...x} = {}) => {}", "export var foo = ({ ...x } = {}) => {}");
|
||
|
|
|
||
|
|
expectParseError("var [...x,] = []", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("var {...x,} = {}", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError(
|
||
|
|
"export default function() { return ([...x,] = []) => {} }",
|
||
|
|
"Unexpected trailing comma after rest element",
|
||
|
|
);
|
||
|
|
expectParseError("({...x,} = {}) => {}", "Unexpected trailing comma after rest element");
|
||
|
|
|
||
|
|
expectPrinted_("[b, ...c] = d", "[b, ...c] = d");
|
||
|
|
expectPrinted_("([b, ...c] = d)", "[b, ...c] = d");
|
||
|
|
expectPrinted_("({b, ...c} = d)", "({ b, ...c } = d)");
|
||
|
|
expectPrinted_("({a = b} = c)", "({ a = b } = c)");
|
||
|
|
expectPrinted_("({a: b = c} = d)", "({ a: b = c } = d)");
|
||
|
|
expectPrinted_("({a: b.c} = d)", "({ a: b.c } = d)");
|
||
|
|
expectPrinted_("[a = {}] = b", "[a = {}] = b");
|
||
|
|
expectPrinted_("[[...a, b].x] = c", "[[...a, b].x] = c");
|
||
|
|
expectPrinted_("[{...a, b}.x] = c", "[{ ...a, b }.x] = c");
|
||
|
|
expectPrinted_("({x: [...a, b].x} = c)", "({ x: [...a, b].x } = c)");
|
||
|
|
expectPrinted_("({x: {...a, b}.x} = c)", "({ x: { ...a, b }.x } = c)");
|
||
|
|
expectPrinted_("[x = [...a, b]] = c", "[x = [...a, b]] = c");
|
||
|
|
expectPrinted_("[x = {...a, b}] = c", "[x = { ...a, b }] = c");
|
||
|
|
expectPrinted_("({x = [...a, b]} = c)", "({ x = [...a, b] } = c)");
|
||
|
|
expectPrinted_("({x = {...a, b}} = c)", "({ x = { ...a, b } } = c)");
|
||
|
|
|
||
|
|
expectPrinted_("(x = y)", "x = y");
|
||
|
|
expectPrinted_("([] = [])", "[] = []");
|
||
|
|
expectPrinted_("({} = {})", "({} = {})");
|
||
|
|
expectPrinted_("([[]] = [[]])", "[[]] = [[]]");
|
||
|
|
expectPrinted_("({x: {}} = {x: {}})", "({ x: {} } = { x: {} })");
|
||
|
|
expectPrinted_("(x) = y", "x = y");
|
||
|
|
expectParseError("([]) = []", "Invalid assignment target");
|
||
|
|
expectParseError("({}) = {}", "Invalid assignment target");
|
||
|
|
expectParseError("[([])] = [[]]", "Invalid assignment target");
|
||
|
|
expectParseError("({x: ({})} = {x: {}})", "Invalid assignment target");
|
||
|
|
expectParseError("(([]) = []) => {}", "Unexpected parentheses in binding pattern");
|
||
|
|
expectParseError("(({}) = {}) => {}", "Unexpected parentheses in binding pattern");
|
||
|
|
expectParseError("function f(([]) = []) {}", "Parse error");
|
||
|
|
expectParseError(
|
||
|
|
"function f(({}) = {}) {}",
|
||
|
|
"Parse error",
|
||
|
|
// 'Expected identifier but found "("\n'
|
||
|
|
);
|
||
|
|
|
||
|
|
expectPrintedNoTrim("for (x in y) ;", "for (x in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ([] in y) ;", "for ([] in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({} in y) ;", "for ({} in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ((x) in y) ;", "for (x in y)\n ;\n");
|
||
|
|
expectParseError("for (x in y)", "Unexpected end of file");
|
||
|
|
expectParseError("for ([] in y)", "Unexpected end of file");
|
||
|
|
expectParseError("for ({} in y)", "Unexpected end of file");
|
||
|
|
expectParseError("for ((x) in y)", "Unexpected end of file");
|
||
|
|
expectParseError("for (([]) in y) ;", "Invalid assignment target");
|
||
|
|
expectParseError("for (({}) in y) ;", "Invalid assignment target");
|
||
|
|
|
||
|
|
expectPrintedNoTrim("for (x of y) ;", "for (x of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ([] of y) ;", "for ([] of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({} of y) ;", "for ({} of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ((x) of y) ;", "for (x of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for (x of y) {}", "for (x of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ([] of y) {}", "for ([] of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({} of y) {}", "for ({} of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ((x) of y) {}", "for (x of y)\n ;\n");
|
||
|
|
expectParseError("for (x of y)", "Unexpected end of file");
|
||
|
|
expectParseError("for ([] of y)", "Unexpected end of file");
|
||
|
|
expectParseError("for ({} of y)", "Unexpected end of file");
|
||
|
|
expectParseError("for ((x) of y)", "Unexpected end of file");
|
||
|
|
expectParseError("for (([]) of y) ;", "Invalid assignment target");
|
||
|
|
expectParseError("for (({}) of y) ;", "Invalid assignment target");
|
||
|
|
|
||
|
|
expectParseError("[[...a, b]] = c", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("[{...a, b}] = c", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("({x: [...a, b]} = c)", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("({x: {...a, b}} = c)", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("[b, ...c,] = d", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("([b, ...c,] = d)", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("({b, ...c,} = d)", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("({a = b})", 'Unexpected "="');
|
||
|
|
expectParseError("({x = {a = b}} = c)", 'Unexpected "="');
|
||
|
|
expectParseError("[a = {b = c}] = d", 'Unexpected "="');
|
||
|
|
|
||
|
|
expectPrintedNoTrim("for ([{a = {}}] in b) ;", "for ([{ a = {} }] in b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ([{a = {}}] of b) ;", "for ([{ a = {} }] of b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({a = {}} in b) ;", "for ({ a = {} } in b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({a = {}} of b) ;", "for ({ a = {} } of b)\n ;\n");
|
||
|
|
expectParseError("for ([{a = {}}] in b)", "Unexpected end of file");
|
||
|
|
expectParseError("for ([{a = {}}] of b)", "Unexpected end of file");
|
||
|
|
expectParseError("for ({a = {}} in b)", "Unexpected end of file");
|
||
|
|
expectParseError("for ({a = {}} of b)", "Unexpected end of file");
|
||
|
|
|
||
|
|
// this is different from esbuild
|
||
|
|
expectPrintedNoTrim("for ([{a = {}}] in b) {}", "for ([{ a = {} }] in b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ([{a = {}}] of b) {}", "for ([{ a = {} }] of b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({a = {}} in b) {}", "for ({ a = {} } in b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({a = {}} of b) {}", "for ({ a = {} } of b)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for (x in y) {}", "for (x in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ([] in y) {}", "for ([] in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ({} in y) {}", "for ({} in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for ((x) in y) {}", "for (x in y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("while (true) {}", "while (true)\n ;\n");
|
||
|
|
|
||
|
|
expectPrintedNoTrim("while (true) ;", "while (true)\n ;\n");
|
||
|
|
expectParseError("while (1)", "Unexpected end of file");
|
||
|
|
|
||
|
|
expectParseError("({a = {}} in b)", 'Unexpected "="');
|
||
|
|
expectParseError("[{a = {}}]\nof()", 'Unexpected "="');
|
||
|
|
expectParseError("for ([...a, b] in c) {}", 'Unexpected "," after rest pattern');
|
||
|
|
expectParseError("for ([...a, b] of c) {}", 'Unexpected "," after rest pattern');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("binding pattern error locations point at the operator", () => {
|
||
|
|
const parseErrorAt = code => {
|
||
|
|
try {
|
||
|
|
parsed(code, false, false);
|
||
|
|
} catch (er) {
|
||
|
|
const err = er instanceof AggregateError ? er.errors[0] : er;
|
||
|
|
return { message: err.message, offset: err.position?.offset };
|
||
|
|
}
|
||
|
|
throw new Error("Expected parse error for code\n\t" + code);
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(parseErrorAt("((...a = 1) => {})")).toEqual({
|
||
|
|
message: "A rest argument cannot have a default initializer",
|
||
|
|
offset: "((...a ".length,
|
||
|
|
});
|
||
|
|
expect(parseErrorAt("x = 1; ([...a = 1]) => {}")).toEqual({
|
||
|
|
message: "A rest argument cannot have a default initializer",
|
||
|
|
offset: "x = 1; ([...a ".length,
|
||
|
|
});
|
||
|
|
expect(parseErrorAt("a;b;(([]) = []) => {}")).toEqual({
|
||
|
|
message: "Unexpected parentheses in binding pattern",
|
||
|
|
offset: "a;b;(".length,
|
||
|
|
});
|
||
|
|
expect(parseErrorAt("a;b;(({}) = {}) => {}")).toEqual({
|
||
|
|
message: "Unexpected parentheses in binding pattern",
|
||
|
|
offset: "a;b;(".length,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("for-in and for-of loop initializers", () => {
|
||
|
|
// Annex B: a plain identifier "var" binding may keep its initializer in a sloppy-mode for-in
|
||
|
|
expectPrintedNoTrim("for (var x = 1 in y) ;", "x = 1;\nfor (x in y)\n ;\nvar x;\n");
|
||
|
|
|
||
|
|
// A destructuring binding can never have an initializer in a for-in/for-of head
|
||
|
|
expectParseError("for (var [a] = 1 in y) ;", "for-in loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (var {a} = 1 in y) ;", "for-in loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (var [a] = 1 of y) ;", "for-of loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (var {a} = 1 of y) ;", "for-of loop variables cannot have an initializer");
|
||
|
|
|
||
|
|
// "let" and "const" bindings can never have an initializer in a for-in/for-of head
|
||
|
|
expectParseError("for (let x = 1 in y) ;", "for-in loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (let x = 1 of y) ;", "for-of loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (let [a] = 1 in y) ;", "for-in loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (let {a} = 1 of y) ;", "for-of loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (const x = 1 in y) ;", "for-in loop variables cannot have an initializer");
|
||
|
|
expectParseError("for (const x = 1 of y) ;", "for-of loop variables cannot have an initializer");
|
||
|
|
|
||
|
|
// for-in/for-of heads must have exactly one declaration
|
||
|
|
expectParseError("for (var x, y in z) ;", "for-in loops must have a single declaration");
|
||
|
|
expectParseError("for (let x, y in z) ;", "for-in loops must have a single declaration");
|
||
|
|
expectParseError("for (let x, y of z) ;", "for-of loops must have a single declaration");
|
||
|
|
|
||
|
|
// Declarations without an initializer are still allowed
|
||
|
|
expectPrintedNoTrim("for (var x in y) ;", "for (x in y)\n ;\nvar x;\n");
|
||
|
|
expectPrintedNoTrim("for (var [a] in y) ;", "for ([a] in y)\n ;\nvar a;\n");
|
||
|
|
expectPrintedNoTrim("for (let [a] of y) ;", "for (let [a] of y)\n ;\n");
|
||
|
|
expectPrintedNoTrim("for (const {a} of y) ;", "for (const { a } of y)\n ;\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("regexp", () => {
|
||
|
|
expectPrinted("/x/g", "/x/g");
|
||
|
|
expectPrinted("/x/i", "/x/i");
|
||
|
|
expectPrinted("/x/m", "/x/m");
|
||
|
|
expectPrinted("/x/s", "/x/s");
|
||
|
|
expectPrinted("/x/u", "/x/u");
|
||
|
|
expectPrinted("/x/y", "/x/y");
|
||
|
|
expectPrinted("/gimme/g", "/gimme/g");
|
||
|
|
expectPrinted("/gimgim/g", "/gimgim/g");
|
||
|
|
|
||
|
|
expectParseError("/x/msuygig", 'Duplicate flag "g" in regular expression');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("identifier escapes", () => {
|
||
|
|
expectPrinted_("var _\u0076\u0061\u0072", "var _var");
|
||
|
|
expectParseError("var \u0076\u0061\u0072", 'Expected identifier but found "\u0076\u0061\u0072"');
|
||
|
|
expectParseError("\\u0076\\u0061\\u0072 foo", "Unexpected \\u0076\\u0061\\u0072");
|
||
|
|
|
||
|
|
expectPrinted_("foo._\u0076\u0061\u0072", "foo._var");
|
||
|
|
expectPrinted_("foo.\u0076\u0061\u0072", "foo.var");
|
||
|
|
|
||
|
|
// expectParseError("\u200Ca", 'Unexpected "\\u200c"');
|
||
|
|
// expectParseError("\u200Da", 'Unexpected "\\u200d"');
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("dead code elimination", () => {
|
||
|
|
const transpilerNoDCE = new Bun.Transpiler({ deadCodeElimination: false });
|
||
|
|
it("should DCE with deadCodeElimination: true or by default", () => {
|
||
|
|
expect(parsed("123", true, false)).toBe("");
|
||
|
|
expect(parsed("[-1, 2n, null]", true, false)).toBe("");
|
||
|
|
expect(parsed("true", true, false)).toBe("");
|
||
|
|
expect(parsed("!0", true, false)).toBe("");
|
||
|
|
expect(parsed('if (!1) "dead";', true, false)).toBe("if (false)");
|
||
|
|
expect(parsed("if (!1) var x = 2;", true, false)).toBe("if (false)\n var x");
|
||
|
|
expect(parsed("if (undefined) { let y = Math.random(); }", true, false)).toBe("if (undefined) {}");
|
||
|
|
});
|
||
|
|
it("should not DCE with deadCodeElimination: false", () => {
|
||
|
|
expect(parsed("123", true, false, transpilerNoDCE)).toBe("123");
|
||
|
|
expect(parsed("[1, 2n, null]", true, false, transpilerNoDCE)).toBe("[1, 2n, null]");
|
||
|
|
expect(parsed("true", true, false, transpilerNoDCE)).toBe("true");
|
||
|
|
expect(parsed("!0", true, false, transpilerNoDCE)).toBe("!0");
|
||
|
|
expect(parsed('if (!1) "dead";', true, false, transpilerNoDCE)).toBe('if (!1)\n "dead"');
|
||
|
|
expect(parsed("if (!1) var x = 2;", true, false, transpilerNoDCE)).toBe("if (!1)\n var x = 2");
|
||
|
|
expect(parsed("if (undefined) { let y = Math.random(); }", true, false, transpilerNoDCE)).toBe(
|
||
|
|
"if (undefined) {\n let y = Math.random();\n}",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("private identifiers", () => {
|
||
|
|
expectParseError("#foo", "Unexpected #foo");
|
||
|
|
expectParseError("#foo in this", "Unexpected #foo");
|
||
|
|
expectParseError("this.#foo", 'Expected identifier but found "#foo"');
|
||
|
|
expectParseError("this?.#foo", 'Expected identifier but found "#foo"');
|
||
|
|
expectParseError("({ #foo: 1 })", 'Expected identifier but found "#foo"');
|
||
|
|
expectParseError("class Foo { x = { #foo: 1 } }", 'Expected identifier but found "#foo"');
|
||
|
|
expectParseError("class Foo { x = #foo }", 'Expected "in" but found "}"');
|
||
|
|
expectParseError("class Foo { #foo; foo() { delete this.#foo } }", 'Deleting the private name "#foo" is forbidden');
|
||
|
|
expectParseError(
|
||
|
|
"class Foo { #foo; foo() { delete this?.#foo } }",
|
||
|
|
'Deleting the private name "#foo" is forbidden',
|
||
|
|
);
|
||
|
|
expectParseError("class Foo extends Bar { #foo; foo() { super.#foo } }", 'Expected identifier but found "#foo"');
|
||
|
|
expectParseError("class Foo { #foo = () => { for (#foo in this) ; } }", "Unexpected #foo");
|
||
|
|
expectParseError("class Foo { #foo = () => { for (x = #foo in this) ; } }", "Unexpected #foo");
|
||
|
|
expectPrinted_("class Foo { #foo }", "class Foo {\n #foo;\n}");
|
||
|
|
expectPrinted_("class Foo { #foo = 1 }", "class Foo {\n #foo = 1;\n}");
|
||
|
|
expectPrinted_("class Foo { #foo = #foo in this }", "class Foo {\n #foo = #foo in this;\n}");
|
||
|
|
expectPrinted_(
|
||
|
|
"class Foo { #foo = #foo in (#bar in this); #bar }",
|
||
|
|
"class Foo {\n #foo = #foo in (#bar in this);\n #bar;\n}",
|
||
|
|
);
|
||
|
|
expectPrinted_("class Foo { #foo() {} }", "class Foo {\n #foo() {}\n}");
|
||
|
|
expectPrinted_("class Foo { get #foo() {} }", "class Foo {\n get #foo() {}\n}");
|
||
|
|
expectPrinted_("class Foo { set #foo(x) {} }", "class Foo {\n set #foo(x) {}\n}");
|
||
|
|
expectPrinted_("class Foo { static #foo }", "class Foo {\n static #foo;\n}");
|
||
|
|
expectPrinted_("class Foo { static #foo = 1 }", "class Foo {\n static #foo = 1;\n}");
|
||
|
|
expectPrinted_("class Foo { static #foo() {} }", "class Foo {\n static #foo() {}\n}");
|
||
|
|
expectPrinted_("class Foo { static get #foo() {} }", "class Foo {\n static get #foo() {}\n}");
|
||
|
|
expectPrinted_("class Foo { static set #foo(x) {} }", "class Foo {\n static set #foo(x) {}\n}");
|
||
|
|
|
||
|
|
expectParseError("class Foo { #foo = #foo in #bar in this; #bar }", "Unexpected #bar");
|
||
|
|
|
||
|
|
expectParseError("class Foo { #constructor }", 'Invalid field name "#constructor"');
|
||
|
|
expectParseError("class Foo { #constructor() {} }", 'Invalid method name "#constructor"');
|
||
|
|
expectParseError("class Foo { static #constructor }", 'Invalid field name "#constructor"');
|
||
|
|
expectParseError("class Foo { static #constructor() {} }", 'Invalid method name "#constructor"');
|
||
|
|
expectParseError("class Foo { #\\u0063onstructor }", 'Invalid field name "#constructor"');
|
||
|
|
expectParseError("class Foo { #\\u0063onstructor() {} }", 'Invalid method name "#constructor"');
|
||
|
|
expectParseError("class Foo { static #\\u0063onstructor }", 'Invalid field name "#constructor"');
|
||
|
|
expectParseError("class Foo { static #\\u0063onstructor() {} }", 'Invalid method name "#constructor"');
|
||
|
|
const errorText = '"#foo" has already been declared';
|
||
|
|
expectParseError("class Foo { #foo; #foo }", errorText);
|
||
|
|
expectParseError("class Foo { #foo; static #foo }", errorText);
|
||
|
|
expectParseError("class Foo { static #foo; #foo }", errorText);
|
||
|
|
expectParseError("class Foo { #foo; #foo() {} }", errorText);
|
||
|
|
expectParseError("class Foo { #foo; get #foo() {} }", errorText);
|
||
|
|
expectParseError("class Foo { #foo; set #foo(x) {} }", errorText);
|
||
|
|
expectParseError("class Foo { #foo() {} #foo }", errorText);
|
||
|
|
expectParseError("class Foo { get #foo() {} #foo }", errorText);
|
||
|
|
expectParseError("class Foo { set #foo(x) {} #foo }", errorText);
|
||
|
|
expectParseError("class Foo { get #foo() {} get #foo() {} }", errorText);
|
||
|
|
expectParseError("class Foo { set #foo(x) {} set #foo(x) {} }", errorText);
|
||
|
|
expectParseError("class Foo { get #foo() {} set #foo(x) {} #foo }", errorText);
|
||
|
|
expectParseError("class Foo { set #foo(x) {} get #foo() {} #foo }", errorText);
|
||
|
|
|
||
|
|
expectPrinted_(
|
||
|
|
"class Foo { get #foo() {} set #foo(x) { this.#foo } }",
|
||
|
|
"class Foo {\n get #foo() {}\n set #foo(x) {\n this.#foo;\n }\n}",
|
||
|
|
);
|
||
|
|
expectPrinted_(
|
||
|
|
"class Foo { set #foo(x) { this.#foo } get #foo() {} }",
|
||
|
|
"class Foo {\n set #foo(x) {\n this.#foo;\n }\n get #foo() {}\n}",
|
||
|
|
);
|
||
|
|
expectPrinted_("class Foo { #foo } class Bar { #foo }", "class Foo {\n #foo;\n}\n\nclass Bar {\n #foo;\n}");
|
||
|
|
expectPrinted_("class Foo { foo = this.#foo; #foo }", "class Foo {\n foo = this.#foo;\n #foo;\n}");
|
||
|
|
expectPrinted_("class Foo { foo = this?.#foo; #foo }", "class Foo {\n foo = this?.#foo;\n #foo;\n}");
|
||
|
|
expectParseError(
|
||
|
|
"class Foo { #foo } class Bar { foo = this.#foo }",
|
||
|
|
'Private name "#foo" must be declared in an enclosing class',
|
||
|
|
);
|
||
|
|
expectParseError(
|
||
|
|
"class Foo { #foo } class Bar { foo = this?.#foo }",
|
||
|
|
'Private name "#foo" must be declared in an enclosing class',
|
||
|
|
);
|
||
|
|
expectParseError(
|
||
|
|
"class Foo { #foo } class Bar { foo = #foo in this }",
|
||
|
|
'Private name "#foo" must be declared in an enclosing class',
|
||
|
|
);
|
||
|
|
|
||
|
|
expectPrinted_(
|
||
|
|
`class Foo {
|
||
|
|
#if
|
||
|
|
#im() { return this.#im(this.#if) }
|
||
|
|
static #sf
|
||
|
|
static #sm() { return this.#sm(this.#sf) }
|
||
|
|
foo() {
|
||
|
|
return class {
|
||
|
|
#inner() {
|
||
|
|
return [this.#im, this?.#inner, this?.x.#if]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
`class Foo {
|
||
|
|
#if;
|
||
|
|
#im() {
|
||
|
|
return this.#im(this.#if);
|
||
|
|
}
|
||
|
|
static #sf;
|
||
|
|
static #sm() {
|
||
|
|
return this.#sm(this.#sf);
|
||
|
|
}
|
||
|
|
foo() {
|
||
|
|
return class {
|
||
|
|
#inner() {
|
||
|
|
return [this.#im, this?.#inner, this?.x.#if];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("type only exports", () => {
|
||
|
|
let { expectPrinted_, expectParseError } = ts;
|
||
|
|
expectPrinted_("export type {foo, bar as baz} from 'bar'", "");
|
||
|
|
expectPrinted_("export type {foo, bar as baz}", "");
|
||
|
|
expectPrinted_("export type {foo} from 'bar'; x", "x");
|
||
|
|
expectPrinted_("export type {foo} from 'bar'\nx", "x");
|
||
|
|
expectPrinted_("export type {default} from 'bar'", "");
|
||
|
|
expectPrinted_("export { type } from 'mod'; type", 'export { type } from "mod";\ntype');
|
||
|
|
expectPrinted_("export { type, as } from 'mod'", 'export { type, as } from "mod"');
|
||
|
|
expectPrinted_("export { x, type foo } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { x, type as } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { x, type foo as bar } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { x, type foo as as } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { type as as } from 'mod'; as", 'export { type as as } from "mod";\nas');
|
||
|
|
expectPrinted_("export { type as foo } from 'mod'; foo", 'export { type as foo } from "mod";\nfoo');
|
||
|
|
expectPrinted_("export { type as type } from 'mod'; type", 'export { type } from "mod";\ntype');
|
||
|
|
expectPrinted_("export { x, type as as foo } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { x, type as as as } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { x, type type as as } from 'mod'; x", 'export { x } from "mod";\nx');
|
||
|
|
expectPrinted_("export { x, \\u0074ype y }; let x, y", "export { x };\nlet x, y");
|
||
|
|
expectPrinted_("export { x, \\u0074ype y } from 'mod'", 'export { x } from "mod"');
|
||
|
|
expectPrinted_("export { x, type if } from 'mod'", 'export { x } from "mod"');
|
||
|
|
expectPrinted_("export { x, type y as if }; let x", "export { x };\nlet x");
|
||
|
|
expectPrinted_("export { type x };", "");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("delete + optional chain", () => {
|
||
|
|
expectPrinted_("delete foo.bar.baz", "delete foo.bar.baz");
|
||
|
|
expectPrinted_("delete foo?.bar.baz", "delete foo?.bar.baz");
|
||
|
|
expectPrinted_("delete foo?.bar?.baz", "delete foo?.bar?.baz");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("useDefineForConst TypeScript class initialization", () => {
|
||
|
|
var { expectPrinted_ } = ts;
|
||
|
|
expectPrinted_(
|
||
|
|
`
|
||
|
|
class Foo {
|
||
|
|
constructor(public x: string = "hey") {}
|
||
|
|
bar: number;
|
||
|
|
}
|
||
|
|
`.trim(),
|
||
|
|
`
|
||
|
|
class Foo {
|
||
|
|
x;
|
||
|
|
constructor(x = "hey") {
|
||
|
|
this.x = x;
|
||
|
|
}
|
||
|
|
bar;
|
||
|
|
}
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("class static blocks", () => {
|
||
|
|
expectPrinted_("class Foo { static {} }", "class Foo {\n static {}\n}");
|
||
|
|
expectPrinted_("class Foo { static {} x = 1 }", "class Foo {\n static {}\n x = 1;\n}");
|
||
|
|
expectPrinted_("class Foo { static { this.foo() } }", "class Foo {\n static {\n this.foo();\n }\n}");
|
||
|
|
|
||
|
|
expectParseError("class Foo { static { yield } }", '"yield" is a reserved word and cannot be used in strict mode');
|
||
|
|
expectParseError("class Foo { static { await } }", 'The keyword "await" cannot be used here');
|
||
|
|
expectParseError("class Foo { static { return } }", "A return statement cannot be used here");
|
||
|
|
expectParseError("class Foo { static { break } }", 'Cannot use "break" here');
|
||
|
|
expectParseError("class Foo { static { continue } }", 'Cannot use "continue" here');
|
||
|
|
expectParseError("x: { class Foo { static { break x } } }", 'There is no containing label named "x"');
|
||
|
|
expectParseError("x: { class Foo { static { continue x } } }", 'There is no containing label named "x"');
|
||
|
|
|
||
|
|
expectParseError("class Foo { get #x() { this.#x = 1 } }", 'Writing to getter-only property "#x" will throw');
|
||
|
|
expectParseError("class Foo { get #x() { this.#x += 1 } }", 'Writing to getter-only property "#x" will throw');
|
||
|
|
expectParseError("class Foo { set #x(x) { this.#x } }", 'Reading from setter-only property "#x" will throw');
|
||
|
|
expectParseError("class Foo { set #x(x) { this.#x += 1 } }", 'Reading from setter-only property "#x" will throw');
|
||
|
|
|
||
|
|
// Writing to method warnings
|
||
|
|
expectParseError("class Foo { #x() { this.#x = 1 } }", 'Writing to read-only method "#x" will throw');
|
||
|
|
expectParseError("class Foo { #x() { this.#x += 1 } }", 'Writing to read-only method "#x" will throw');
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("simplification", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader: "tsx",
|
||
|
|
define: {
|
||
|
|
"process.env.NODE_ENV": JSON.stringify("development"),
|
||
|
|
user_undefined: "undefined",
|
||
|
|
},
|
||
|
|
macro: {
|
||
|
|
react: {
|
||
|
|
bacon: `${import.meta.dir}/macro-check.js`,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
platform: "browser",
|
||
|
|
minify: { syntax: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
const parsed = (code, trim = true, autoExport = false, transpiler_ = transpiler) => {
|
||
|
|
if (autoExport) {
|
||
|
|
code = "export default (" + code + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
var out = transpiler_.transformSync(code, "js");
|
||
|
|
if (autoExport && out.startsWith("export default ")) {
|
||
|
|
out = out.substring("export default ".length);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (trim) {
|
||
|
|
out = out.trim();
|
||
|
|
|
||
|
|
if (out.endsWith(";")) {
|
||
|
|
out = out.substring(0, out.length - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
return out.trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrinted = (code, out) => {
|
||
|
|
expect(parsed(code, true, true)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrinted_ = (code, out) => {
|
||
|
|
expect(parsed(code, !out.endsWith(";\n"), false)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectPrintedNoTrim = (code, out) => {
|
||
|
|
expect(parsed(code, false, false)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const expectBunPrinted_ = (code, out) => {
|
||
|
|
expect(parsed(code, !out.endsWith(";\n"), false, bunTranspiler)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
it("unary operator", () => {
|
||
|
|
expectPrinted("a = !(b, c)", "a = (b, !c)");
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("const inlining", () => {
|
||
|
|
var transpiler = new Bun.Transpiler({
|
||
|
|
inline: true,
|
||
|
|
platform: "bun",
|
||
|
|
allowBunRuntime: false,
|
||
|
|
minify: { syntax: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
function check(input, output) {
|
||
|
|
expect(
|
||
|
|
transpiler
|
||
|
|
.transformSync("export function hello() {\n" + input + "\n}")
|
||
|
|
.trim()
|
||
|
|
.replaceAll(/^ /gm, ""),
|
||
|
|
).toBe("export function hello() {\n" + output + "\n}".replaceAll(/^ /gm, ""));
|
||
|
|
}
|
||
|
|
hideFromStackTrace(check);
|
||
|
|
|
||
|
|
check("const x = 1; return x", "return 1;");
|
||
|
|
check("const x = 1; return x + 1", "return 2;");
|
||
|
|
check("const x = 1; return x + x", "return 2;");
|
||
|
|
check("const x = 1; return x + x + 1", "return 3;");
|
||
|
|
check("const x = 1; return x + x + x", "return 3;");
|
||
|
|
check(`const foo = "foo"; const bar = "bar"; return foo + bar`, `return "foobar";`);
|
||
|
|
|
||
|
|
check(
|
||
|
|
`
|
||
|
|
const a = "a";
|
||
|
|
const c = "b" + a;
|
||
|
|
const b = c + a;
|
||
|
|
const d = b + a;
|
||
|
|
console.log(a, b, c, d);
|
||
|
|
`,
|
||
|
|
`
|
||
|
|
const c = "ba", b = c + "a", d = b + "a";
|
||
|
|
console.log("a", b, c, d);
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// check that it doesn't inline after "var"
|
||
|
|
check(
|
||
|
|
`
|
||
|
|
const x = 1;
|
||
|
|
const y = 2;
|
||
|
|
var hey = "yo";
|
||
|
|
const z = 3;
|
||
|
|
console.log(x + y + z);
|
||
|
|
`,
|
||
|
|
`
|
||
|
|
var hey = "yo";
|
||
|
|
const z = 3;
|
||
|
|
console.log(3 + z);
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// check that nested scopes can inline from parent scopes
|
||
|
|
check(
|
||
|
|
`
|
||
|
|
const x = 1;
|
||
|
|
const y = 2;
|
||
|
|
var hey = "yo";
|
||
|
|
const z = 3;
|
||
|
|
function hey() {
|
||
|
|
const boom = 3;
|
||
|
|
return x + y + boom + hey;
|
||
|
|
}
|
||
|
|
hey();
|
||
|
|
`,
|
||
|
|
`
|
||
|
|
var hey = "yo";
|
||
|
|
const z = 3;
|
||
|
|
function hey() {
|
||
|
|
return 6 + hey;
|
||
|
|
}
|
||
|
|
hey();
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// check that we don't inline objects or arrays that aren't from macros
|
||
|
|
check(
|
||
|
|
`
|
||
|
|
const foo = { bar: true };
|
||
|
|
const array = [1];
|
||
|
|
console.log(foo, array);
|
||
|
|
`,
|
||
|
|
`
|
||
|
|
const foo = { bar: !0 }, array = [1];
|
||
|
|
console.log(foo, array);
|
||
|
|
`.trim(),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("constant folding scopes", () => {
|
||
|
|
var transpiler = new Bun.Transpiler({
|
||
|
|
inline: true,
|
||
|
|
platform: "bun",
|
||
|
|
allowBunRuntime: false,
|
||
|
|
minify: { syntax: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
// Check that pushing/popping scopes doesn't cause a crash
|
||
|
|
// We panic at runtime if the scopes are unbalanced, so this test just checks we don't have any crashes
|
||
|
|
function check(input) {
|
||
|
|
transpiler.transformSync(input);
|
||
|
|
}
|
||
|
|
|
||
|
|
check("var x; 1 ? 0 : ()=>{}; (()=>{})()");
|
||
|
|
check("var x; 0 ? ()=>{} : 1; (()=>{})()");
|
||
|
|
check("var x; 0 && (()=>{}); (()=>{})()");
|
||
|
|
check("var x; 1 || (()=>{}); (()=>{})()");
|
||
|
|
check("if (1) 0; else ()=>{}; (()=>{})()");
|
||
|
|
check("if (0) ()=>{}; else 1; (()=>{})()");
|
||
|
|
check(`
|
||
|
|
var func = () => {};
|
||
|
|
var x;
|
||
|
|
1 ? 0 : func;
|
||
|
|
(() => {})();
|
||
|
|
switch (1) {
|
||
|
|
case 0: {
|
||
|
|
class Foo {
|
||
|
|
static {
|
||
|
|
function hey() {
|
||
|
|
return class {
|
||
|
|
static {
|
||
|
|
var foo = class {
|
||
|
|
hey(arg) {
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
new foo();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
new Foo();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("substitution", () => {
|
||
|
|
var transpiler = new Bun.Transpiler({
|
||
|
|
inline: true,
|
||
|
|
platform: "bun",
|
||
|
|
allowBunRuntime: false,
|
||
|
|
minify: { syntax: true },
|
||
|
|
});
|
||
|
|
function check(input, output) {
|
||
|
|
expect(
|
||
|
|
transpiler
|
||
|
|
.transformSync("export function hello() {\n" + input + "\n}")
|
||
|
|
.trim()
|
||
|
|
.replaceAll(/^ /gm, ""),
|
||
|
|
).toBe("export function hello() {\n" + output + "\n}".replaceAll(/^ /gm, ""));
|
||
|
|
}
|
||
|
|
check("var x = 1; return x", "var x = 1;\nreturn x;");
|
||
|
|
check("let x = 1; return x", "return 1;");
|
||
|
|
check("const x = 1; return x", "return 1;");
|
||
|
|
|
||
|
|
check("let x = 1; if (false) x++; return x", "return 1;");
|
||
|
|
// TODO: comma operator
|
||
|
|
// check("let x = 1; if (true) x++; return x", "let x = 1;\nreturn x++, x;");
|
||
|
|
check("let x = 1; return x + x", "let x = 1;\nreturn x + x;");
|
||
|
|
|
||
|
|
// Can substitute into normal unary operators
|
||
|
|
check("let x = 1; return +x", "return 1;");
|
||
|
|
check("let x = 1; return -x", "return -1;");
|
||
|
|
check("let x = 1; return !x", "return !1;");
|
||
|
|
check("let x = 1; return ~x", "return ~1;");
|
||
|
|
// TODO: remove needless return undefined;
|
||
|
|
// check("let x = 1; return void x", "let x = 1;");
|
||
|
|
|
||
|
|
// esbuild does this:
|
||
|
|
// check("let x = 1; return typeof x", "return typeof 1;");
|
||
|
|
// we do:
|
||
|
|
check("let x = 1; return typeof x", 'return "number";');
|
||
|
|
|
||
|
|
// Check substituting a side-effect free value into normal binary operators
|
||
|
|
// esbuild does this:
|
||
|
|
// check("let x = 1; return x + 2", "return 1 + 2;");
|
||
|
|
// we do:
|
||
|
|
check("let x = 1; return x + 2", "return 3;");
|
||
|
|
check("let x = 1; return 2 + x", "return 3;");
|
||
|
|
check("let x = 1; return x + arg0", "return 1 + arg0;");
|
||
|
|
// check("let x = 1; return arg0 + x", "return arg0 + 1;");
|
||
|
|
check("let x = 1; return x + fn()", "return 1 + fn();");
|
||
|
|
check("let x = 1; return fn() + x", "let x = 1;\nreturn fn() + x;");
|
||
|
|
check("let x = 1; return x + undef", "return 1 + undef;");
|
||
|
|
check("let x = 1; return undef + x", "let x = 1;\nreturn undef + x;");
|
||
|
|
|
||
|
|
// Check substituting a value with side-effects into normal binary operators
|
||
|
|
check("let x = fn(); return x + 2", "return fn() + 2;");
|
||
|
|
check("let x = fn(); return 2 + x", "return 2 + fn();");
|
||
|
|
check("let x = fn(); return x + arg0", "return fn() + arg0;");
|
||
|
|
check("let x = fn(); return arg0 + x", "let x = fn();\nreturn arg0 + x;");
|
||
|
|
check("let x = fn(); return x + fn2()", "return fn() + fn2();");
|
||
|
|
check("let x = fn(); return fn2() + x", "let x = fn();\nreturn fn2() + x;");
|
||
|
|
check("let x = fn(); return x + undef", "return fn() + undef;");
|
||
|
|
check("let x = fn(); return undef + x", "let x = fn();\nreturn undef + x;");
|
||
|
|
|
||
|
|
// Cannot substitute into mutating unary operators
|
||
|
|
check("let x = 1; ++x", "let x = 1;\n++x;");
|
||
|
|
check("let x = 1; --x", "let x = 1;\n--x;");
|
||
|
|
check("let x = 1; x++", "let x = 1;\nx++;");
|
||
|
|
check("let x = 1; x--", "let x = 1;\nx--;");
|
||
|
|
check("let x = 1; delete x", "let x = 1;\ndelete x;");
|
||
|
|
|
||
|
|
// Cannot substitute into mutating binary operators unless immediately after the assignment
|
||
|
|
check("let x = 1; let y; x = 2", "let x = 1, y;\nx = 2;");
|
||
|
|
check("let x = 1; let y; x += 2", "let x = 1, y;\nx += 2;");
|
||
|
|
check("let x = 1; let y; x ||= 2", "let x = 1, y;\nx ||= 2;");
|
||
|
|
|
||
|
|
// Can substitute past mutating binary operators when the left operand has no side effects
|
||
|
|
// check("let x = 1; arg0 = x", "arg0 = 1;");
|
||
|
|
// check("let x = 1; arg0 += x", "arg0 += 1;");
|
||
|
|
// check("let x = 1; arg0 ||= x", "arg0 ||= 1;");
|
||
|
|
// check("let x = fn(); arg0 = x", "arg0 = fn();");
|
||
|
|
// check("let x = fn(); arg0 += x", "let x = fn();\narg0 += x;");
|
||
|
|
// check("let x = fn(); arg0 ||= x", "let x = fn();\narg0 ||= x;");
|
||
|
|
|
||
|
|
// Cannot substitute past mutating binary operators when the left operand has side effects
|
||
|
|
check("let x = 1; y.z = x", "let x = 1;\ny.z = x;");
|
||
|
|
check("let x = 1; y.z += x", "let x = 1;\ny.z += x;");
|
||
|
|
check("let x = 1; y.z ||= x", "let x = 1;\ny.z ||= x;");
|
||
|
|
check("let x = fn(); y.z = x", "let x = fn();\ny.z = x;");
|
||
|
|
check("let x = fn(); y.z += x", "let x = fn();\ny.z += x;");
|
||
|
|
check("let x = fn(); y.z ||= x", "let x = fn();\ny.z ||= x;");
|
||
|
|
|
||
|
|
// TODO:
|
||
|
|
// Can substitute code without side effects into branches
|
||
|
|
// check("let x = arg0; return x ? y : z;", "return arg0 ? y : z;");
|
||
|
|
// check("let x = arg0; return arg1 ? x : y;", "return arg1 ? arg0 : y;");
|
||
|
|
// check("let x = arg0; return arg1 ? y : x;", "return arg1 ? y : arg0;");
|
||
|
|
// check("let x = arg0; return x || y;", "return arg0 || y;");
|
||
|
|
// check("let x = arg0; return x && y;", "return arg0 && y;");
|
||
|
|
// check("let x = arg0; return x ?? y;", "return arg0 ?? y;");
|
||
|
|
// check("let x = arg0; return arg1 || x;", "return arg1 || arg0;");
|
||
|
|
// check("let x = arg0; return arg1 && x;", "return arg1 && arg0;");
|
||
|
|
// check("let x = arg0; return arg1 ?? x;", "return arg1 ?? arg0;");
|
||
|
|
|
||
|
|
// Can substitute code without side effects into branches past an expression with side effects
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return y ? x : z;",
|
||
|
|
// "let x = arg0;\nreturn y ? x : z;",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return y ? z : x;",
|
||
|
|
// "let x = arg0;\nreturn y ? z : x;",
|
||
|
|
// );
|
||
|
|
// check("let x = arg0; return (arg1 ? 1 : 2) ? x : 3;", "return arg0;");
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return (arg1 ? 1 : 2) ? 3 : x;",
|
||
|
|
// "let x = arg0;\nreturn 3;",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return (arg1 ? y : 1) ? x : 2;",
|
||
|
|
// "let x = arg0;\nreturn !arg1 || y ? x : 2;",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return (arg1 ? 1 : y) ? x : 2;",
|
||
|
|
// "let x = arg0;\nreturn arg1 || y ? x : 2;",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return (arg1 ? y : 1) ? 2 : x;",
|
||
|
|
// "let x = arg0;\nreturn !arg1 || y ? 2 : x;",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return (arg1 ? 1 : y) ? 2 : x;",
|
||
|
|
// "let x = arg0;\nreturn arg1 || y ? 2 : x;",
|
||
|
|
// );
|
||
|
|
// check("let x = arg0; return y || x;", "let x = arg0;\nreturn y || x;");
|
||
|
|
// check("let x = arg0; return y && x;", "let x = arg0;\nreturn y && x;");
|
||
|
|
// check("let x = arg0; return y ?? x;", "let x = arg0;\nreturn y ?? x;");
|
||
|
|
|
||
|
|
// Cannot substitute code with side effects into branches
|
||
|
|
check("let x = fn(); return x ? arg0 : y;", "return fn() ? arg0 : y;");
|
||
|
|
check("let x = fn(); return arg0 ? x : y;", "let x = fn();\nreturn arg0 ? x : y;");
|
||
|
|
check("let x = fn(); return arg0 ? y : x;", "let x = fn();\nreturn arg0 ? y : x;");
|
||
|
|
check("let x = fn(); return x || arg0;", "return fn() || arg0;");
|
||
|
|
check("let x = fn(); return x && arg0;", "return fn() && arg0;");
|
||
|
|
check("let x = fn(); return x ?? arg0;", "return fn() ?? arg0;");
|
||
|
|
check("let x = fn(); return arg0 || x;", "let x = fn();\nreturn arg0 || x;");
|
||
|
|
check("let x = fn(); return arg0 && x;", "let x = fn();\nreturn arg0 && x;");
|
||
|
|
check("let x = fn(); return arg0 ?? x;", "let x = fn();\nreturn arg0 ?? x;");
|
||
|
|
|
||
|
|
// Test chaining
|
||
|
|
check("let x = fn(); let y = x[prop]; let z = y.val; throw z", "throw fn()[prop].val;");
|
||
|
|
check("let x = fn(), y = x[prop], z = y.val; throw z", "throw fn()[prop].val;");
|
||
|
|
|
||
|
|
// Can substitute an initializer with side effects
|
||
|
|
check("let x = 0; let y = ++x; return y", "let x = 0;\nreturn ++x;");
|
||
|
|
|
||
|
|
// Can substitute an initializer without side effects past an expression without side effects
|
||
|
|
check("let x = 0; let y = x; return [x, y]", "let x = 0;\nreturn [x, x];");
|
||
|
|
|
||
|
|
// TODO: merge s_local
|
||
|
|
// Cannot substitute an initializer with side effects past an expression without side effects
|
||
|
|
// check(
|
||
|
|
// "let x = 0; let y = ++x; return [x, y]",
|
||
|
|
// "let x = 0, y = ++x;\nreturn [x, y];",
|
||
|
|
// );
|
||
|
|
|
||
|
|
// Cannot substitute an initializer without side effects past an expression with side effects
|
||
|
|
// TODO: merge s_local
|
||
|
|
// check(
|
||
|
|
// "let x = 0; let y = {valueOf() { x = 1 }}; let z = x; return [y == 1, z]",
|
||
|
|
// "let x = 0, y = { valueOf() {\n x = 1;\n} }, z = x;\nreturn [y == 1, z];",
|
||
|
|
// );
|
||
|
|
|
||
|
|
// Cannot inline past a spread operator, since that evaluates code
|
||
|
|
check("let x = arg0; return [...x];", "return [...arg0];");
|
||
|
|
check("let x = arg0; return [x, ...arg1];", "return [arg0, ...arg1];");
|
||
|
|
check("let x = arg0; return [...arg1, x];", "let x = arg0;\nreturn [...arg1, x];");
|
||
|
|
// TODO: preserve call here
|
||
|
|
// check("let x = arg0; return arg1(...x);", "return arg1(...arg0);");
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return arg1(x, ...arg1);",
|
||
|
|
// "return arg1(arg0, ...arg1);",
|
||
|
|
// );
|
||
|
|
check("let x = arg0; return arg1(...arg1, x);", "let x = arg0;\nreturn arg1(...arg1, x);");
|
||
|
|
|
||
|
|
// Test various statement kinds
|
||
|
|
// TODO:
|
||
|
|
// check("let x = arg0; arg1(x);", "arg1(arg0);");
|
||
|
|
|
||
|
|
check("let x = arg0; throw x;", "throw arg0;");
|
||
|
|
check("let x = arg0; return x;", "return arg0;");
|
||
|
|
check("let x = arg0; if (x) return 1;", "if (arg0)\n return 1;");
|
||
|
|
check("let x = arg0; switch (x) { case 0: return 1; }", "switch (arg0) {\n case 0:\n return 1;\n}");
|
||
|
|
check("let x = arg0; let y = x; return y + y;", "let y = arg0;\nreturn y + y;");
|
||
|
|
|
||
|
|
// Loops must not be substituted into because they evaluate multiple times
|
||
|
|
check("let x = arg0; do {} while (x);", "let x = arg0;\ndo\n ;\nwhile (x);");
|
||
|
|
|
||
|
|
// TODO: convert while(x) to for (;x;)
|
||
|
|
check(
|
||
|
|
"let x = arg0; while (x) return 1;",
|
||
|
|
"let x = arg0;\nwhile (x)\n return 1;",
|
||
|
|
// "let x = arg0;\nfor (; x; )\n return 1;",
|
||
|
|
);
|
||
|
|
check("let x = arg0; for (; x; ) return 1;", "let x = arg0;\nfor (;x; )\n return 1;");
|
||
|
|
|
||
|
|
// Can substitute an expression without side effects into a branch due to optional chaining
|
||
|
|
// TODO:
|
||
|
|
// check("let x = arg0; return arg1?.[x];", "return arg1?.[arg0];");
|
||
|
|
// check("let x = arg0; return arg1?.(x);", "return arg1?.(arg0);");
|
||
|
|
|
||
|
|
// Cannot substitute an expression with side effects into a branch due to optional chaining,
|
||
|
|
// since that would change the expression with side effects from being unconditionally
|
||
|
|
// evaluated to being conditionally evaluated, which is a behavior change
|
||
|
|
check("let x = fn(); return arg1?.[x];", "let x = fn();\nreturn arg1?.[x];");
|
||
|
|
check("let x = fn(); return arg1?.(x);", "let x = fn();\nreturn arg1?.(x);");
|
||
|
|
|
||
|
|
// Can substitute an expression past an optional chaining operation, since it has side effects
|
||
|
|
check("let x = arg0; return arg1?.a === x;", "let x = arg0;\nreturn arg1?.a === x;");
|
||
|
|
check("let x = arg0; return arg1?.[0] === x;", "let x = arg0;\nreturn arg1?.[0] === x;");
|
||
|
|
check("let x = arg0; return arg1?.(0) === x;", "let x = arg0;\nreturn arg1?.(0) === x;");
|
||
|
|
check("let x = arg0; return arg1?.a[x];", "let x = arg0;\nreturn arg1?.a[x];");
|
||
|
|
check("let x = arg0; return arg1?.a(x);", "let x = arg0;\nreturn arg1?.a(x);");
|
||
|
|
// TODO:
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return arg1?.[a][x];",
|
||
|
|
// "let x = arg0;\nreturn arg1?.[a][x];",
|
||
|
|
// );
|
||
|
|
check("let x = arg0; return arg1?.[a](x);", "let x = arg0;\nreturn (arg1?.[a])(x);");
|
||
|
|
check("let x = arg0; return arg1?.(a)[x];", "let x = arg0;\nreturn (arg1?.(a))[x];");
|
||
|
|
check("let x = arg0; return arg1?.(a)(x);", "let x = arg0;\nreturn (arg1?.(a))(x);");
|
||
|
|
|
||
|
|
// Can substitute into an object as long as there are no side effects
|
||
|
|
// beforehand. Note that computed properties must call "toString()" which
|
||
|
|
// can have side effects.
|
||
|
|
check("let x = arg0; return {x};", "return { x: arg0 };");
|
||
|
|
check("let x = arg0; return {x: y, y: x};", "let x = arg0;\nreturn { x: y, y: x };");
|
||
|
|
// TODO:
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return {x: arg1, y: x};",
|
||
|
|
// "return { x: arg1, y: arg0 };",
|
||
|
|
// );
|
||
|
|
check("let x = arg0; return {[x]: 0};", "return { [arg0]: 0 };");
|
||
|
|
check("let x = arg0; return {[y]: x};", "let x = arg0;\nreturn { [y]: x };");
|
||
|
|
check("let x = arg0; return {[arg1]: x};", "let x = arg0;\nreturn { [arg1]: x };");
|
||
|
|
// TODO:
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return {y() {}, x};",
|
||
|
|
// "return { y() {\n}, x: arg0 };",
|
||
|
|
// );
|
||
|
|
check("let x = arg0; return {[y]() {}, x};", "let x = arg0;\nreturn { [y]() {\n}, x };");
|
||
|
|
check("let x = arg0; return {...x};", "return { ...arg0 };");
|
||
|
|
check("let x = arg0; return {...x, y};", "return { ...arg0, y };");
|
||
|
|
check("let x = arg0; return {x, ...y};", "return { x: arg0, ...y };");
|
||
|
|
check("let x = arg0; return {...y, x};", "let x = arg0;\nreturn { ...y, x };");
|
||
|
|
|
||
|
|
// TODO:
|
||
|
|
// Check substitutions into template literals
|
||
|
|
// check("let x = arg0; return `a${x}b${y}c`;", "return `a${arg0}b${y}c`;");
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return `a${y}b${x}c`;",
|
||
|
|
// "let x = arg0;\nreturn `a${y}b${x}c`;",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return `a${arg1}b${x}c`;",
|
||
|
|
// "return `a${arg1}b${arg0}c`;",
|
||
|
|
// );
|
||
|
|
// check("let x = arg0; return x`y`;", "return arg0`y`;");
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return y`a${x}b`;",
|
||
|
|
// "let x = arg0;\nreturn y`a${x}b`;",
|
||
|
|
// );
|
||
|
|
// check("let x = arg0; return arg1`a${x}b`;", "return arg1`a${arg0}b`;");
|
||
|
|
// check("let x = 'x'; return `a${x}b`;", "return `axb`;");
|
||
|
|
|
||
|
|
// Check substitutions into import expressions
|
||
|
|
// TODO:
|
||
|
|
// check("let x = arg0; return import(x);", "return import(arg0);");
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return [import(y), x];",
|
||
|
|
// "let x = arg0;\nreturn [import(y), x];",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "let x = arg0; return [import(arg1), x];",
|
||
|
|
// "return [import(arg1), arg0];",
|
||
|
|
// );
|
||
|
|
|
||
|
|
// Check substitutions into await expressions
|
||
|
|
check("return async () => { let x = arg0; await x; };", "return async () => {\n await arg0;\n};");
|
||
|
|
|
||
|
|
// TODO: combine with comma operator
|
||
|
|
// check(
|
||
|
|
// "return async () => { let x = arg0; await y; return x; };",
|
||
|
|
// "return async () => {\n let x = arg0;\n return await y, x;\n};",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "return async () => { let x = arg0; await arg1; return x; };",
|
||
|
|
// "return async () => {\n let x = arg0;\n return await arg1, x;\n};",
|
||
|
|
// );
|
||
|
|
|
||
|
|
// Check substitutions into yield expressions
|
||
|
|
check("return function* () { let x = arg0; yield x; };", "return function* () {\n yield arg0;\n};");
|
||
|
|
// TODO: combine with comma operator
|
||
|
|
// check(
|
||
|
|
// "return function* () { let x = arg0; yield; return x; };",
|
||
|
|
// "return function* () {\n let x = arg0;\n yield ; \n return x;\n};",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "return function* () { let x = arg0; yield y; return x; };",
|
||
|
|
// "return function* () {\n let x = arg0;\n return yield y, x;\n};",
|
||
|
|
// );
|
||
|
|
// check(
|
||
|
|
// "return function* () { let x = arg0; yield arg1; return x; };",
|
||
|
|
// "return function* () {\n let x = arg0;\n return yield arg1, x;\n};",
|
||
|
|
// );
|
||
|
|
|
||
|
|
// Cannot substitute into call targets when it would change "this"
|
||
|
|
check("let x = arg0; x()", "arg0();");
|
||
|
|
// check("let x = arg0; (0, x)()", "arg0();");
|
||
|
|
check("let x = arg0.foo; x.bar()", "arg0.foo.bar();");
|
||
|
|
check("let x = arg0.foo; x[bar]()", "arg0.foo[bar]();");
|
||
|
|
check("let x = arg0.foo; x()", "let x = arg0.foo;\nx();");
|
||
|
|
check("let x = arg0[foo]; x()", "let x = arg0[foo];\nx();");
|
||
|
|
check("let x = arg0?.foo; x()", "let x = arg0?.foo;\nx();");
|
||
|
|
check("let x = arg0?.[foo]; x()", "let x = arg0?.[foo];\nx();");
|
||
|
|
// check("let x = arg0.foo; (0, x)()", "let x = arg0.foo;\nx();");
|
||
|
|
// check("let x = arg0[foo]; (0, x)()", "let x = arg0[foo];\nx();");
|
||
|
|
// check("let x = arg0?.foo; (0, x)()", "let x = arg0?.foo;\nx();");
|
||
|
|
// check("let x = arg0?.[foo]; (0, x)()", "let x = arg0?.[foo];\nx();");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("comma operator transforms", () => {
|
||
|
|
const expectPrinted = (code, out) => {
|
||
|
|
expect(parsed(code, true, true, transpilerMinifySyntax)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Comma operator should be optimized when not used as call target
|
||
|
|
expectPrinted("(0, 1)", "1");
|
||
|
|
expectPrinted("(0, foo)", "foo");
|
||
|
|
expectPrinted("(sideEffect(), foo)", "(sideEffect(), foo)");
|
||
|
|
|
||
|
|
// Comma operator should preserve 'this' binding semantics when used as call target
|
||
|
|
expectPrinted("(0, obj.method)()", "(0, obj.method)()");
|
||
|
|
expectPrinted("(0, obj[key])()", "(0, obj[key])()");
|
||
|
|
expectPrinted("(0, obj?.method)()", "(0, obj?.method)()");
|
||
|
|
expectPrinted("(0, obj?.[key])()", "(0, obj?.[key])()");
|
||
|
|
|
||
|
|
// Side effects should still be preserved in call context
|
||
|
|
expectPrinted("(sideEffect(), obj.method)()", "(sideEffect(), obj.method)()");
|
||
|
|
|
||
|
|
// Non-method calls should still be optimized even in call context
|
||
|
|
expectPrinted("(0, func)()", "func()");
|
||
|
|
expectPrinted("(0, getValue())()", "getValue()()");
|
||
|
|
|
||
|
|
// Non-call target with function call as second value should be optimized
|
||
|
|
expectPrinted("(0, obj.method)", "obj.method");
|
||
|
|
expectPrinted("(0, obj[key])", "obj[key]");
|
||
|
|
expectPrinted("(0, func())", "func()");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("constant folding", () => {
|
||
|
|
const expectPrinted = (code, out) => {
|
||
|
|
expect(parsed(code, true, true, transpilerMinifySyntax)).toBe(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
// we have an optimization for numbers 0 - 100, -0 - -100 so we must test those specifically
|
||
|
|
// https://github.com/oven-sh/bun/issues/2810
|
||
|
|
for (let i = 1; i < 120; i++) {
|
||
|
|
const inner = "${" + i + " * 1}";
|
||
|
|
expectPrinted("console.log(`" + inner + "`)", 'console.log("' + i + '")');
|
||
|
|
|
||
|
|
const innerNeg = "${" + -i + " * 1}";
|
||
|
|
expectPrinted("console.log(`" + innerNeg + "`)", 'console.log("' + -i + '")');
|
||
|
|
}
|
||
|
|
|
||
|
|
expectPrinted("1 && 2", "2");
|
||
|
|
expectPrinted("1 || 2", "1");
|
||
|
|
expectPrinted("0 && 1", "0");
|
||
|
|
expectPrinted("0 || 1", "1");
|
||
|
|
|
||
|
|
expectPrinted("null ?? 1", "1");
|
||
|
|
expectPrinted("undefined ?? 1", "1");
|
||
|
|
expectPrinted("0 ?? 1", "0");
|
||
|
|
expectPrinted("false ?? 1", "!1");
|
||
|
|
expectPrinted('"" ?? 1', '""');
|
||
|
|
|
||
|
|
expectPrinted("typeof undefined", '"undefined"');
|
||
|
|
expectPrinted("typeof null", '"object"');
|
||
|
|
expectPrinted("typeof false", '"boolean"');
|
||
|
|
expectPrinted("typeof true", '"boolean"');
|
||
|
|
expectPrinted("typeof 123", '"number"');
|
||
|
|
expectPrinted("typeof 123n", '"bigint"');
|
||
|
|
expectPrinted("typeof 'abc'", '"string"');
|
||
|
|
expectPrinted("typeof function() {}", '"function"');
|
||
|
|
expectPrinted("typeof (() => {})", '"function"');
|
||
|
|
// Array/object/class literals may contain side effects in their
|
||
|
|
// elements/properties, so `typeof` is not folded for them.
|
||
|
|
expectPrinted("typeof {}", "typeof {}");
|
||
|
|
expectPrinted("typeof {foo: 123}", "typeof { foo: 123 }");
|
||
|
|
expectPrinted("typeof []", "typeof []");
|
||
|
|
expectPrinted("typeof [0]", "typeof [0]");
|
||
|
|
expectPrinted("typeof [null]", "typeof [null]");
|
||
|
|
expectPrinted("typeof ['boolean']", 'typeof ["boolean"]');
|
||
|
|
expectPrinted("typeof [sideEffect()]", "typeof [sideEffect()]");
|
||
|
|
expectPrinted("typeof {x: sideEffect()}", "typeof { x: sideEffect() }");
|
||
|
|
expectPrinted("typeof class { static x = sideEffect(); }", "typeof class {\n static x = sideEffect();\n}");
|
||
|
|
|
||
|
|
expectPrinted('typeof [] === "object"', 'typeof [] === "object"');
|
||
|
|
expectPrinted("typeof {foo: 123} === typeof {bar: 123}", "typeof { foo: 123 } === typeof { bar: 123 }");
|
||
|
|
expectPrinted("typeof {foo: 123} !== typeof 123", 'typeof { foo: 123 } !== "number"');
|
||
|
|
|
||
|
|
// `!` folds to a boolean only when the operand has no side effects or
|
||
|
|
// can be proven removable. Side-effecting operands are left intact.
|
||
|
|
expectPrinted("![]", "!1");
|
||
|
|
expectPrinted("!{}", "!1");
|
||
|
|
expectPrinted("![1, 2, 3]", "!1");
|
||
|
|
expectPrinted("!{ a: 1 }", "!1");
|
||
|
|
expectPrinted("![sideEffect()]", "![sideEffect()]");
|
||
|
|
expectPrinted("!{ x: sideEffect() }", "!{ x: sideEffect() }");
|
||
|
|
expectPrinted("!(class { static x = sideEffect(); })", "!class {\n static x = sideEffect();\n}");
|
||
|
|
expectPrinted("!void sideEffect()", "!void sideEffect()");
|
||
|
|
expectPrinted("!!void sideEffect()", "!!void sideEffect()");
|
||
|
|
expectPrinted("!![sideEffect()]", "!![sideEffect()]");
|
||
|
|
expectPrinted("!(sideEffect(), true)", "(sideEffect(), !1)");
|
||
|
|
expectPrinted("!(sideEffect() || 1)", "!(sideEffect() || 1)");
|
||
|
|
expectPrinted("!(sideEffect() && 0)", "!(sideEffect() && 0)");
|
||
|
|
expectPrinted("!typeof sideEffect()", "!typeof sideEffect()");
|
||
|
|
|
||
|
|
expectPrinted("undefined === undefined", "!0");
|
||
|
|
expectPrinted("undefined !== undefined", "!1");
|
||
|
|
expectPrinted("undefined == undefined", "!0");
|
||
|
|
expectPrinted("undefined != undefined", "!1");
|
||
|
|
|
||
|
|
expectPrinted("null === null", "!0");
|
||
|
|
expectPrinted("null !== null", "!1");
|
||
|
|
expectPrinted("null == null", "!0");
|
||
|
|
expectPrinted("null != null", "!1");
|
||
|
|
|
||
|
|
expectPrinted("undefined === null", "!1");
|
||
|
|
expectPrinted("undefined !== null", "!0");
|
||
|
|
expectPrinted("undefined == null", "!0");
|
||
|
|
expectPrinted("undefined != null", "!1");
|
||
|
|
|
||
|
|
expectPrinted("true === true", "!0");
|
||
|
|
expectPrinted("true === false", "!1");
|
||
|
|
expectPrinted("true !== true", "!1");
|
||
|
|
expectPrinted("true !== false", "!0");
|
||
|
|
expectPrinted("true == true", "!0");
|
||
|
|
expectPrinted("true == false", "!1");
|
||
|
|
expectPrinted("true != true", "!1");
|
||
|
|
expectPrinted("true != false", "!0");
|
||
|
|
|
||
|
|
expectPrinted("1 === 1", "!0");
|
||
|
|
expectPrinted("1 === 2", "!1");
|
||
|
|
expectPrinted("1 === '1'", '1 === "1"');
|
||
|
|
expectPrinted("1 == 1", "!0");
|
||
|
|
expectPrinted("1 == 2", "!1");
|
||
|
|
expectPrinted("1 == '1'", '1 == "1"');
|
||
|
|
|
||
|
|
expectPrinted("1 !== 1", "!1");
|
||
|
|
expectPrinted("1 !== 2", "!0");
|
||
|
|
expectPrinted("1 !== '1'", '1 !== "1"');
|
||
|
|
expectPrinted("1 != 1", "!1");
|
||
|
|
expectPrinted("1 != 2", "!0");
|
||
|
|
expectPrinted("1 != '1'", '1 != "1"');
|
||
|
|
|
||
|
|
expectPrinted('"" == 0', "!0");
|
||
|
|
expectPrinted("1n == 1n", "!0");
|
||
|
|
expectPrinted("1234n == 1234n", "!0");
|
||
|
|
expectPrinted("1n == 2n", "!1");
|
||
|
|
expectPrinted("!0n", "!0");
|
||
|
|
expectPrinted("!1n", "!1");
|
||
|
|
// Radix BigInt literals keep their source text, so folds that need a
|
||
|
|
// decimal string bail out instead of producing a wrong constant.
|
||
|
|
expectPrinted("0x00n == 0n", "0x00n == 0n");
|
||
|
|
expectPrinted("0x10n == 16n", "0x10n == 16n");
|
||
|
|
expectPrinted("0x10n == 0x10n", "!0");
|
||
|
|
expectPrinted("!0x0n", "!0x0n");
|
||
|
|
expectPrinted("!0x1n", "!0x1n");
|
||
|
|
expectPrinted("`${0x10n}`", "`${0x10n}`");
|
||
|
|
expectPrinted("`${0b1_0n}`", "`${0b10n}`");
|
||
|
|
expectPrinted("`${10n}`", '"10"');
|
||
|
|
|
||
|
|
expectPrinted("'a' === '\\x61'", "!0");
|
||
|
|
expectPrinted("'a' === '\\x62'", "!1");
|
||
|
|
expectPrinted("'a' === 'abc'", "!1");
|
||
|
|
expectPrinted("'a' !== '\\x61'", "!1");
|
||
|
|
expectPrinted("'a' !== '\\x62'", "!0");
|
||
|
|
expectPrinted("'a' !== 'abc'", "!0");
|
||
|
|
expectPrinted("'a' == '\\x61'", "!0");
|
||
|
|
expectPrinted("'a' == '\\x62'", "!1");
|
||
|
|
expectPrinted("'a' == 'abc'", "!1");
|
||
|
|
expectPrinted("'a' != '\\x61'", "!1");
|
||
|
|
expectPrinted("'a' != '\\x62'", "!0");
|
||
|
|
expectPrinted("'a' != 'abc'", "!0");
|
||
|
|
|
||
|
|
expectPrinted("'a' + 'b'", '"ab"');
|
||
|
|
expectPrinted("'a' + 'bc'", '"abc"');
|
||
|
|
expectPrinted("'ab' + 'c'", '"abc"');
|
||
|
|
expectPrinted("x + 'a' + 'b'", 'x + "ab"');
|
||
|
|
expectPrinted("x + 'a' + 'bc'", 'x + "abc"');
|
||
|
|
expectPrinted("x + 'ab' + 'c'", 'x + "abc"');
|
||
|
|
expectPrinted("'a' + 1", '"a1"');
|
||
|
|
expectPrinted("x * 'a' + 'b'", 'x * "a" + "b"');
|
||
|
|
|
||
|
|
// rope string push another rope string
|
||
|
|
expectPrinted("'a' + ('b' + 'c') + 'd'", '"abcd"');
|
||
|
|
expectPrinted("('a' + 'b') + 'c'", '"abc"');
|
||
|
|
expectPrinted("'a' + ('b' + 'c')", '"abc"');
|
||
|
|
expectPrinted("'a' + ('b' + ('c' + 'd')) + 'e'", '"abcde"');
|
||
|
|
expectPrinted("'a' + ('b' + ('c' + ('d' + 'e')))", '"abcde"');
|
||
|
|
expectPrinted("('a' + ('b' + ('c' + 'd'))) + 'e'", '"abcde"');
|
||
|
|
expectPrinted("('a' + ('b' + 'c')) + ('d' + 'e')", '"abcde"');
|
||
|
|
expectPrinted("('a' + 'b') + ('c' + 'd')", '"abcd"');
|
||
|
|
expectPrinted("'a' + ('b' + 'c') + 'd'", '"abcd"');
|
||
|
|
expectPrinted("'a' + ('b' + ('c' + 'd'))", '"abcd"');
|
||
|
|
|
||
|
|
function check(input, output) {
|
||
|
|
expect(transpiler.transformSync(input)).toEqual(output);
|
||
|
|
}
|
||
|
|
|
||
|
|
var output = `var boop = "bcd";\nconst ropy = "a" + boop + "d", ropy2 = "b" + boop;\n`;
|
||
|
|
check(`var boop = ('b' + 'c') + 'd'; const ropy = "a" + boop + 'd'; const ropy2 = 'b' + boop;`, output);
|
||
|
|
|
||
|
|
output = `var boop = "fbcd", ropy = "a" + boop + "d", ropy2 = "b" + (ropy + "d");\n`;
|
||
|
|
check(`var boop = "f" + ("b" + "c") + "d";var ropy = "a" + boop + "d";var ropy2 = "b" + (ropy + "d")`, output);
|
||
|
|
|
||
|
|
expectPrinted("'string' + `template`", `"stringtemplate"`);
|
||
|
|
|
||
|
|
expectPrinted("`template` + 'string'", '"templatestring"');
|
||
|
|
|
||
|
|
// TODO: string template simplification
|
||
|
|
// expectPrinted("'string' + `a${foo}b`", "`stringa${foo}b`");
|
||
|
|
// expectPrinted("'string' + tag`template`", '"string" + tag`template`;');
|
||
|
|
// expectPrinted("`a${foo}b` + 'string'", "`a${foo}bstring`");
|
||
|
|
// expectPrinted("tag`template` + 'string'", 'tag`template` + "string"');
|
||
|
|
// expectPrinted("`template` + `a${foo}b`", "`templatea${foo}b`");
|
||
|
|
// expectPrinted("`a${foo}b` + `template`", "`a${foo}btemplate`");
|
||
|
|
// expectPrinted("`a${foo}b` + `x${bar}y`", "`a${foo}bx${bar}y`");
|
||
|
|
// expectPrinted(
|
||
|
|
// "`a${i}${j}bb` + `xxx${bar}yyyy`",
|
||
|
|
// "`a${i}${j}bbxxx${bar}yyyy`"
|
||
|
|
// );
|
||
|
|
// expectPrinted(
|
||
|
|
// "`a${foo}bb` + `xxx${i}${j}yyyy`",
|
||
|
|
// "`a${foo}bbxxx${i}${j}yyyy`"
|
||
|
|
// );
|
||
|
|
// expectPrinted(
|
||
|
|
// "`template` + tag`template2`",
|
||
|
|
// "`template` + tag`template2`"
|
||
|
|
// );
|
||
|
|
// expectPrinted(
|
||
|
|
// "tag`template` + `template2`",
|
||
|
|
// "tag`template` + `template2`"
|
||
|
|
// );
|
||
|
|
|
||
|
|
expectPrinted("123", "123");
|
||
|
|
expectPrinted("123 .toString()", "123 .toString()");
|
||
|
|
expectPrinted("-123", "-123");
|
||
|
|
expectPrinted("(-123).toString()", "(-123).toString()");
|
||
|
|
expectPrinted("-0", "-0");
|
||
|
|
expectPrinted("(-0).toString()", "(-0).toString()");
|
||
|
|
expectPrinted("-0 === 0", "!0");
|
||
|
|
|
||
|
|
expectPrinted("NaN", "NaN");
|
||
|
|
expectPrinted("NaN.toString()", "NaN.toString()");
|
||
|
|
expectPrinted("NaN === NaN", "!1");
|
||
|
|
|
||
|
|
expectPrinted("Infinity", "1 / 0");
|
||
|
|
expectPrinted("Infinity.toString()", "(1 / 0).toString()");
|
||
|
|
expectPrinted("(-Infinity).toString()", "(-1 / 0).toString()");
|
||
|
|
expectPrinted("Infinity === Infinity", "!0");
|
||
|
|
expectPrinted("Infinity === -Infinity", "!1");
|
||
|
|
|
||
|
|
expectPrinted("123n === 1_2_3n", "!0");
|
||
|
|
});
|
||
|
|
describe("type coercions", () => {
|
||
|
|
const dead = `
|
||
|
|
if ("") {
|
||
|
|
TEST_FAIL
|
||
|
|
}
|
||
|
|
|
||
|
|
if (false) {
|
||
|
|
TEST_FAIL
|
||
|
|
}
|
||
|
|
|
||
|
|
if (0) {
|
||
|
|
TEST_FAIL
|
||
|
|
}
|
||
|
|
|
||
|
|
if (void 0) {
|
||
|
|
TEST_FAIL
|
||
|
|
}
|
||
|
|
|
||
|
|
if (null) {
|
||
|
|
TEST_FAIL
|
||
|
|
}
|
||
|
|
|
||
|
|
var should_be_true = typeof "" === "string" || false
|
||
|
|
var should_be_false = typeof "" !== "string" && TEST_FAIL;
|
||
|
|
var should_be_false_2 = typeof true === "string" && TEST_FAIL;
|
||
|
|
var should_be_false_3 = typeof false === "string" && TEST_FAIL;
|
||
|
|
var should_be_false_4 = typeof 123n === "string" && TEST_FAIL;
|
||
|
|
var should_be_false_5 = typeof function(){} === "string" && TEST_FAIL;
|
||
|
|
var should_be_kept = typeof globalThis.BACON === "string" && TEST_OK;
|
||
|
|
var should_be_kept_1 = typeof TEST_OK === "string";
|
||
|
|
|
||
|
|
var should_be_kept_2 = TEST_OK ?? true;
|
||
|
|
var should_be_kept_4 = { "TEST_OK": true } ?? TEST_FAIL;
|
||
|
|
var should_be_false_6 = false ?? TEST_FAIL;
|
||
|
|
var should_be_true_7 = true ?? TEST_FAIL;
|
||
|
|
`;
|
||
|
|
const out = transpiler.transformSync(dead);
|
||
|
|
|
||
|
|
for (let line of out.split("\n")) {
|
||
|
|
it(line, () => {
|
||
|
|
if (line.includes("should_be_kept")) {
|
||
|
|
expect(line.includes("TEST_OK")).toBe(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (line.includes("should_be_false")) {
|
||
|
|
if (!line.includes("= !1")) throw new Error(`Expected false in "${line}"`);
|
||
|
|
expect(line.includes("= !1")).toBe(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (line.includes("TEST_FAIL")) {
|
||
|
|
throw new Error(`"${line}"\n\tshould not contain TEST_FAIL`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("raw template literal contents", () => {
|
||
|
|
expectPrinted("String.raw`\r`", "String.raw`\n`");
|
||
|
|
expectPrinted("String.raw`\r\n`", "String.raw`\n`");
|
||
|
|
expectPrinted("String.raw`\n`", "String.raw`\n`");
|
||
|
|
expectPrinted("String.raw`\r\r\r\r\r\n\r`", "String.raw`\n\n\n\n\n\n`");
|
||
|
|
expectPrinted("String.raw`\n\r`", "String.raw`\n\n`");
|
||
|
|
var code = `String.raw\`
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8" />
|
||
|
|
<title>${"meow123"}</title>
|
||
|
|
<link rel="stylesheet" href="/css/style.css" />
|
||
|
|
</head>
|
||
|
|
\``;
|
||
|
|
var result = code;
|
||
|
|
expectPrinted(code, code);
|
||
|
|
|
||
|
|
code = `String.raw\`
|
||
|
|
<head>\r\n\n\r\r\r
|
||
|
|
<meta charset="UTF-8" />\r
|
||
|
|
<title>${"meow123"}</title>\n
|
||
|
|
|
||
|
|
\r
|
||
|
|
\r
|
||
|
|
\n\r
|
||
|
|
<link rel="stylesheet" href="/css/style.css" />
|
||
|
|
</head>
|
||
|
|
\``;
|
||
|
|
result = `String.raw\`
|
||
|
|
<head>\n\n\n\n
|
||
|
|
<meta charset="UTF-8" />
|
||
|
|
<title>${"meow123"}</title>\n
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
\n
|
||
|
|
<link rel="stylesheet" href="/css/style.css" />
|
||
|
|
</head>
|
||
|
|
\``;
|
||
|
|
expectPrinted(code, result);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("scan", () => {
|
||
|
|
it("reports all export names", () => {
|
||
|
|
const { imports, exports } = transpiler.scan(code);
|
||
|
|
|
||
|
|
expect(exports[0]).toBe("action");
|
||
|
|
expect(exports[2]).toBe("loader");
|
||
|
|
expect(exports[1]).toBe("default");
|
||
|
|
expect(exports).toHaveLength(3);
|
||
|
|
|
||
|
|
expect(imports.filter(({ path }) => path === "remix")).toHaveLength(1);
|
||
|
|
expect(imports.filter(({ path }) => path === "mod")).toHaveLength(0);
|
||
|
|
expect(imports.filter(({ path }) => path === "react")).toHaveLength(1);
|
||
|
|
expect(imports).toHaveLength(2);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("transform", () => {
|
||
|
|
// Async transform doesn't work in the test runner. Skipping for now.
|
||
|
|
// This might be caused by incorrectly using shared memory between the two files.
|
||
|
|
it.skip("supports macros", async () => {
|
||
|
|
const out = await transpiler.transform(`
|
||
|
|
import {keepSecondArgument} from 'macro:${require.resolve("./macro-check.js")}';
|
||
|
|
|
||
|
|
export default keepSecondArgument("Test failed", "Test passed");
|
||
|
|
export function otherNamesStillWork() {}
|
||
|
|
`);
|
||
|
|
expect(out.includes("Test failed")).toBe(false);
|
||
|
|
expect(out.includes("Test passed")).toBe(true);
|
||
|
|
|
||
|
|
// ensure both the import and the macro function call are removed
|
||
|
|
expect(out.includes("keepSecondArgument")).toBe(false);
|
||
|
|
expect(out.includes("otherNamesStillWork")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("sync supports macros", () => {
|
||
|
|
const out = transpiler.transformSync(`
|
||
|
|
import {keepSecondArgument} from 'macro:${import.meta.dir}/macro-check.js';
|
||
|
|
|
||
|
|
export default keepSecondArgument("Test failed", "Test passed");
|
||
|
|
export function otherNamesStillWork() {
|
||
|
|
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
expect(out.includes("Test failed")).toBe(false);
|
||
|
|
expect(out.includes("Test passed")).toBe(true);
|
||
|
|
|
||
|
|
expect(out.includes("keepSecondArgument")).toBe(false);
|
||
|
|
expect(out.includes("otherNamesStillWork")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("a macro that runs a nested transformSync macro and then requires a module leaves the importing file intact", async () => {
|
||
|
|
const otherLines = [];
|
||
|
|
for (let i = 0; i < 300; i++) {
|
||
|
|
otherLines.push(`const v${i} = { a: [${i}, "s${i}"], b: (${i} + 1) * 2, c: String(${i}).length };`);
|
||
|
|
}
|
||
|
|
otherLines.push(`module.exports = { value: v299.b + v0.c };`);
|
||
|
|
|
||
|
|
using dir = tempDir("macro-nested-transform-sync", {
|
||
|
|
"inner-macro.ts": `export function inner() { return "inner-value"; }`,
|
||
|
|
"outer-macro.ts": `
|
||
|
|
import { join } from "node:path";
|
||
|
|
export function outer() {
|
||
|
|
const source =
|
||
|
|
"import { inner } from " +
|
||
|
|
JSON.stringify(join(import.meta.dir, "inner-macro.ts")) +
|
||
|
|
' with { type: "macro" };\\nexport const v = inner();\\n';
|
||
|
|
const code = new Bun.Transpiler({ loader: "ts" }).transformSync(source);
|
||
|
|
const expanded = code.includes('"inner-value"') && !code.includes("inner(");
|
||
|
|
const other = import.meta.require("./other.cjs");
|
||
|
|
return "expanded=" + expanded + " other=" + other.value;
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
"other.cjs": otherLines.join("\n"),
|
||
|
|
"index.ts": `
|
||
|
|
import { writeFileSync } from "node:fs";
|
||
|
|
import { join } from "node:path";
|
||
|
|
import { inner } from "./inner-macro.ts" with { type: "macro" };
|
||
|
|
import { outer } from "./outer-macro.ts" with { type: "macro" };
|
||
|
|
const pre = inner();
|
||
|
|
const res = outer();
|
||
|
|
const tail = { list: [1, 2, 3].map(n => n * 2), label: ["a", "b"].join("-") };
|
||
|
|
writeFileSync(join(import.meta.dir, "out.json"), JSON.stringify({ pre, res, tail }));
|
||
|
|
`,
|
||
|
|
});
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "run", "index.ts"],
|
||
|
|
env: bunEnv,
|
||
|
|
cwd: String(dir),
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect({ stderr, exitCode }).toEqual({ stderr: "", exitCode: 0 });
|
||
|
|
expect(await Bun.file(join(String(dir), "out.json")).text()).toBe(
|
||
|
|
JSON.stringify({
|
||
|
|
pre: "inner-value",
|
||
|
|
res: "expanded=true other=601",
|
||
|
|
tail: { list: [2, 4, 6], label: "a-b" },
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("special identifier in import statement", () => {
|
||
|
|
const out = transpiler.transformSync(`
|
||
|
|
import {ɵtest} from 'foo'
|
||
|
|
`);
|
||
|
|
|
||
|
|
expect(out).toBe('import { ɵtest } from "foo";\n');
|
||
|
|
});
|
||
|
|
|
||
|
|
const importLines = ["import {createElement, bacon} from 'react';", "import {bacon, createElement} from 'react';"];
|
||
|
|
describe("sync supports macros remap", () => {
|
||
|
|
for (let importLine of importLines) {
|
||
|
|
it.todo(importLine, () => {
|
||
|
|
var thisCode = `
|
||
|
|
${importLine}
|
||
|
|
|
||
|
|
export default bacon("Test failed", "Test passed");
|
||
|
|
export function otherNamesStillWork() {
|
||
|
|
return createElement("div");
|
||
|
|
}
|
||
|
|
|
||
|
|
`;
|
||
|
|
var out = transpiler.transformSync(thisCode);
|
||
|
|
try {
|
||
|
|
expect(out.includes("Test failed")).toBe(false);
|
||
|
|
expect(out.includes("Test passed")).toBe(true);
|
||
|
|
|
||
|
|
expect(out.includes("bacon")).toBe(false);
|
||
|
|
expect(out.includes("createElement")).toBe(true);
|
||
|
|
} catch (e) {
|
||
|
|
console.log("Failing code:\n\n" + out + "\n");
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it.todo("macro remap removes import statement if its the only used one", () => {
|
||
|
|
const out = transpiler.transformSync(`
|
||
|
|
import {bacon} from 'react';
|
||
|
|
|
||
|
|
export default bacon("Test failed", "Test passed");
|
||
|
|
`);
|
||
|
|
|
||
|
|
expect(out.includes("Test failed")).toBe(false);
|
||
|
|
expect(out.includes("Test passed")).toBe(true);
|
||
|
|
|
||
|
|
expect(out.includes("bacon")).toBe(false);
|
||
|
|
expect(out.includes("import")).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("removes types", () => {
|
||
|
|
expect(code.includes("mod")).toBe(true);
|
||
|
|
expect(code.includes("xx")).toBe(true);
|
||
|
|
expect(code.includes("ActionFunction")).toBe(true);
|
||
|
|
expect(code.includes("LoaderFunction")).toBe(true);
|
||
|
|
expect(code.includes("ReactNode")).toBe(true);
|
||
|
|
expect(code.includes("React")).toBe(true);
|
||
|
|
expect(code.includes("Component")).toBe(true);
|
||
|
|
const out = transpiler.transformSync(code);
|
||
|
|
|
||
|
|
expect(out.includes("ActionFunction")).toBe(false);
|
||
|
|
expect(out.includes("LoaderFunction")).toBe(false);
|
||
|
|
expect(out.includes("mod")).toBe(false);
|
||
|
|
expect(out.includes("xx")).toBe(false);
|
||
|
|
expect(out.includes("ReactNode")).toBe(false);
|
||
|
|
const { exports } = transpiler.scan(out);
|
||
|
|
exports.sort();
|
||
|
|
|
||
|
|
expect(exports[0]).toBe("action");
|
||
|
|
expect(exports[2]).toBe("loader");
|
||
|
|
expect(exports[1]).toBe("default");
|
||
|
|
expect(exports).toHaveLength(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("#17961 - node target", async () => {
|
||
|
|
// Issue #17961: Transpiler encoding issue with UTF-8 characters
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader: "ts",
|
||
|
|
target: "node",
|
||
|
|
});
|
||
|
|
|
||
|
|
const input = `let list = ["•", "-", "◦", "▪", "▫"];`;
|
||
|
|
const result = await transpiler.transform(input);
|
||
|
|
expect(result).toBe(`let list = ["•", "-", "◦", "▪", "▫"];\n`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("#17961 - browser target", async () => {
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader: "ts",
|
||
|
|
target: "browser",
|
||
|
|
});
|
||
|
|
|
||
|
|
const input = `let list = ["•", "-", "◦", "▪", "▫"];`;
|
||
|
|
const result = await transpiler.transform(input);
|
||
|
|
expect(result).toBe(`let list = ["•", "-", "◦", "▪", "▫"];\n`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("#17961 - bun target", async () => {
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader: "ts",
|
||
|
|
target: "bun",
|
||
|
|
});
|
||
|
|
|
||
|
|
const input = `let list = ["•", "-", "◦", "▪", "▫"];\n`;
|
||
|
|
const result = await transpiler.transform(input);
|
||
|
|
expect(result).toBe(`let list = [\"\\u2022\", \"-\", \"\\u25E6\", \"\\u25AA\", \"\\u25AB\"];\n`);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("edge cases", () => {
|
||
|
|
it.todo("import statement with quoted specifier", () => {
|
||
|
|
expectPrinted_(`import { "x.y" as xy } from "bar";`, `import {"x.y" as xy} from "bar"`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('`str` + "``"', () => {
|
||
|
|
expectPrintedMin_('const x = `str` + "``";', 'const x = "str``"');
|
||
|
|
expectPrintedMin_('const x = `` + "`";', 'const x = "`"');
|
||
|
|
expectPrintedMin_('const x = `` + "``";', 'const x = "``"');
|
||
|
|
expectPrintedMin_('const x = "``" + ``;', 'const x = "``"');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("scan on empty file does not segfault", () => {
|
||
|
|
new Bun.Transpiler().scan("");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("scanImports on empty file does not segfault", () => {
|
||
|
|
new Bun.Transpiler().scanImports("");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("preserves exotic directives", () => {
|
||
|
|
expect(
|
||
|
|
new Bun.Transpiler().transformSync(`"use client";
|
||
|
|
console.log("boop");
|
||
|
|
`),
|
||
|
|
).toBe(
|
||
|
|
`"use client";
|
||
|
|
console.log("boop");
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
it("does not preserve use strict (for now)", () => {
|
||
|
|
expect(
|
||
|
|
new Bun.Transpiler().transformSync(`"use strict";
|
||
|
|
console.log("boop");
|
||
|
|
`),
|
||
|
|
).toBe(
|
||
|
|
`console.log("boop");
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("can parse 'a<b>' as typescript", () => {
|
||
|
|
ts.expectPrinted("a<b>", "a");
|
||
|
|
expect(new Bun.Transpiler({ loader: "ts" }).transformSync(`a<b>`)).toBe(`a;\n`);
|
||
|
|
});
|
||
|
|
|
||
|
|
const prepareForSnapshot = code => {
|
||
|
|
return code.replace(/(__using|__callDispose)_([a-z0-9]+)/g, "$1");
|
||
|
|
};
|
||
|
|
const expectCapturePrintedSnapshot = code => {
|
||
|
|
const result = parsed(`(async() => {${code}})()`, false, false);
|
||
|
|
expect(result).toEndWith("})();\n");
|
||
|
|
const of_relevance = result
|
||
|
|
.slice(result.indexOf("() => {") + 9, result.lastIndexOf("})();") - 1)
|
||
|
|
.trim()
|
||
|
|
.split("\n")
|
||
|
|
.map(x => x.trim())
|
||
|
|
.filter(x => x.length > 0)
|
||
|
|
.join("\n");
|
||
|
|
expect(prepareForSnapshot(of_relevance)).toMatchSnapshot();
|
||
|
|
};
|
||
|
|
const expectPrintedSnapshot = code => {
|
||
|
|
expect(prepareForSnapshot(parsed(`${code}`, false, false))).toMatchSnapshot();
|
||
|
|
};
|
||
|
|
|
||
|
|
it("using statements work right", () => {
|
||
|
|
expectCapturePrintedSnapshot(`using x = a;`);
|
||
|
|
expectCapturePrintedSnapshot(`await using x = a;`);
|
||
|
|
|
||
|
|
expectCapturePrintedSnapshot(`for (using a of b) c(a)`);
|
||
|
|
expectCapturePrintedSnapshot(`for await (using a of b) c(a)`);
|
||
|
|
expectCapturePrintedSnapshot(`for (await using a of b) c(a)`);
|
||
|
|
expectCapturePrintedSnapshot(`for await (await using a of b) c(a)`);
|
||
|
|
|
||
|
|
expectCapturePrintedSnapshot(`for (using a of b) { c(a); a(c) }`);
|
||
|
|
expectCapturePrintedSnapshot(`for await (using a of b) { c(a); a(c) }`);
|
||
|
|
expectCapturePrintedSnapshot(`for (await using a of b) { c(a); a(c) }`);
|
||
|
|
expectCapturePrintedSnapshot(`for await (await using a of b) { c(a); a(c) }`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("await of the identifier 'using' is not an await using declaration", () => {
|
||
|
|
// "await using" only starts a declaration when followed by an identifier on
|
||
|
|
// the same line. Otherwise it's an "await" expression of the identifier "using".
|
||
|
|
expectPrinted_(
|
||
|
|
"async function f() { await using instanceof o }",
|
||
|
|
"async function f() {\n await using instanceof o;\n}",
|
||
|
|
);
|
||
|
|
expectPrinted_("async function f() { await using }", "async function f() {\n await using;\n}");
|
||
|
|
expectPrinted_("async function f() { await using\n x = 1 }", "async function f() {\n await using;\n x = 1;\n}");
|
||
|
|
expectPrinted_("async function f() { await using.foo() }", "async function f() {\n await using.foo();\n}");
|
||
|
|
expectPrinted_(
|
||
|
|
"async function f() { for (await using instanceof o;;); }",
|
||
|
|
"async function f() {\n for (await using instanceof o;; )\n ;\n}",
|
||
|
|
);
|
||
|
|
expectBunPrinted_("await using instanceof o", "await using instanceof o");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("using top level", () => {
|
||
|
|
expectPrintedSnapshot(`
|
||
|
|
using a = b;
|
||
|
|
export function c(e) {
|
||
|
|
using f = g(a);
|
||
|
|
return f.h;
|
||
|
|
}
|
||
|
|
await using j = c(i);
|
||
|
|
using k = l(m);
|
||
|
|
export { k };
|
||
|
|
import { using } from 'n';
|
||
|
|
using o = using;
|
||
|
|
await using p = await using;
|
||
|
|
export var q = r;
|
||
|
|
`);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("await can only be used inside an async function message", () => {
|
||
|
|
var transpiler = new Bun.Transpiler({
|
||
|
|
logLevel: "debug",
|
||
|
|
});
|
||
|
|
|
||
|
|
function assertError(code, hasNote = false) {
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(code);
|
||
|
|
expect.unreachable();
|
||
|
|
} catch (e) {
|
||
|
|
function handle(error) {
|
||
|
|
expect(error.message).toBe('"await" can only be used inside an "async" function');
|
||
|
|
|
||
|
|
if (hasNote) {
|
||
|
|
expect(error.notes).toHaveLength(1);
|
||
|
|
expect(error.notes[0].message).toBe('Consider adding the "async" keyword here');
|
||
|
|
expect(error.notes[0].position.lineText).toContain("foo");
|
||
|
|
} else {
|
||
|
|
expect(error.notes).toHaveLength(0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (e instanceof AggregateError) {
|
||
|
|
handle(e.errors[0]);
|
||
|
|
} else {
|
||
|
|
expect.unreachable();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
it("in object method", () => {
|
||
|
|
assertError(
|
||
|
|
`const x = {
|
||
|
|
foo() {
|
||
|
|
await bar();
|
||
|
|
}
|
||
|
|
}`,
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("in class method", () => {
|
||
|
|
assertError(
|
||
|
|
`class X {
|
||
|
|
foo() {
|
||
|
|
await bar();
|
||
|
|
}
|
||
|
|
}`,
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("in function statement", () => {
|
||
|
|
assertError(
|
||
|
|
`function foo() {
|
||
|
|
await bar();
|
||
|
|
}`,
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("in function expression", () => {
|
||
|
|
assertError(
|
||
|
|
`const foo = function() {
|
||
|
|
await bar();
|
||
|
|
}`,
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("in arrow function", () => {
|
||
|
|
assertError(
|
||
|
|
`const foo = () => {
|
||
|
|
await bar();
|
||
|
|
}`,
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("in arrow function with block body", () => {
|
||
|
|
assertError(
|
||
|
|
`const foo = () => {
|
||
|
|
await bar();
|
||
|
|
}`,
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("in arrow function with expression body", () => {
|
||
|
|
assertError(`const foo = () => await bar();`, false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("malformed function definition does not crash due to invalid scope initialization", () => {
|
||
|
|
it("fails with a parse error and exits cleanly", async () => {
|
||
|
|
const tests = ["function:", "function a() {function:}"];
|
||
|
|
for (const code of tests) {
|
||
|
|
for (const loader of ["js", "ts"]) {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader });
|
||
|
|
expect(() => transpiler.transformSync(code)).toThrow("Parse error");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not crash with 9 comments and typescript type skipping", () => {
|
||
|
|
const cmd = [bunExe(), "build", "--minify-identifiers", join(import.meta.dir, "fixtures", "9-comments.ts")];
|
||
|
|
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||
|
|
cmd,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
env: bunEnv,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(stderr.toString()).toBe("");
|
||
|
|
expect(stdout.toString()).toContain("success!");
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not crash with --minify-syntax and revisiting dot expressions", () => {
|
||
|
|
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||
|
|
cmd: [bunExe(), "-p", "[(()=>{})()][''+'c']"],
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
env: bunEnv,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(stderr.toString()).toBe("");
|
||
|
|
expect(stdout.toString()).toBe("undefined\n");
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("runtime transpiler stack overflows", async () => {
|
||
|
|
expect(async () => await import("./fixtures/lots-of-for-loop.js")).toThrow(`Maximum call stack size exceeded`);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("Bun.Transpiler.transformSync stack overflows", async () => {
|
||
|
|
const code = await Bun.file(join(import.meta.dir, "fixtures", "lots-of-for-loop.js")).text();
|
||
|
|
const transpiler = new Bun.Transpiler();
|
||
|
|
expect(() => transpiler.transformSync(code)).toThrow(`Maximum call stack size exceeded`);
|
||
|
|
}, 60_000);
|
||
|
|
|
||
|
|
it("Bun.Transpiler.transform stack overflows", async () => {
|
||
|
|
const code = await Bun.file(join(import.meta.dir, "fixtures", "lots-of-for-loop.js")).text();
|
||
|
|
const transpiler = new Bun.Transpiler();
|
||
|
|
expect(async () => await transpiler.transform(code)).toThrow(`Maximum call stack size exceeded`);
|
||
|
|
}, 60_000);
|
||
|
|
|
||
|
|
it("deeply nested expressions error instead of crashing the process", () => {
|
||
|
|
const script = `
|
||
|
|
const repeat = (fill, count) => Buffer.alloc(fill.length * count, fill).toString();
|
||
|
|
const shapes = [
|
||
|
|
n => repeat("- ", n) + "1",
|
||
|
|
n => repeat("f(", n) + "1" + repeat(")", n),
|
||
|
|
n => repeat("[", n) + "1" + repeat("]", n),
|
||
|
|
n => "void " + repeat("- ", n) + "1",
|
||
|
|
n => repeat("[", n) + "() => 1" + repeat("]", n) + ";{ let x; }",
|
||
|
|
n => repeat("[", n) + "1" + repeat("]", n) + "; someLongIdentifier + anotherIdentifier;",
|
||
|
|
n => "void ((x" + repeat(" ?? x", n) + ") < 1)",
|
||
|
|
n => "(a" + repeat(" && a", n) + ") == 1;",
|
||
|
|
n => "f() ? 1 : g()" + repeat(" || g()", n) + ";",
|
||
|
|
n => "let " + repeat("[", n) + "x" + repeat("]", n) + " = y;",
|
||
|
|
];
|
||
|
|
const minifyShapes = [
|
||
|
|
n => "function f(){let x = 1; return a" + repeat(" && a", n) + " && x}",
|
||
|
|
n =>
|
||
|
|
"function f(){function g(){return x}" +
|
||
|
|
repeat("[", n) +
|
||
|
|
"1" +
|
||
|
|
repeat("]", n) +
|
||
|
|
";let x = 1;return " +
|
||
|
|
repeat("a", 500) +
|
||
|
|
";}",
|
||
|
|
];
|
||
|
|
const check = (transpiler, src) => {
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(src);
|
||
|
|
} catch (e) {
|
||
|
|
if (!/Maximum call stack size exceeded|StackOverflow/.test(String(e?.message))) throw e;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
for (const shape of shapes) {
|
||
|
|
for (const n of [4000, 20000, 100000]) {
|
||
|
|
check(new Bun.Transpiler({ loader: "js" }), shape(n));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const shape of minifyShapes) {
|
||
|
|
for (const n of [4000, 20000, 100000]) {
|
||
|
|
check(new Bun.Transpiler({ loader: "js", minify: true }), shape(n));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log("depth-ok");
|
||
|
|
`;
|
||
|
|
const { stdout, exitCode, signalCode } = Bun.spawnSync({
|
||
|
|
cmd: [bunExe(), "-e", script],
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
env: bunEnv,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(stdout.toString()).toBe("depth-ok\n");
|
||
|
|
expect([exitCode, signalCode ?? undefined]).toEqual([0, undefined]);
|
||
|
|
}, 60_000);
|
||
|
|
|
||
|
|
it("deeply nested TypeScript types error instead of crashing the process", () => {
|
||
|
|
const script = `
|
||
|
|
const repeat = (fill, count) => Buffer.alloc(fill.length * count, fill).toString();
|
||
|
|
const shapes = [
|
||
|
|
// TSX generic arrow function whose "extends" constraint is a deeply nested tuple type
|
||
|
|
{ loader: "tsx", code: n => "<T extends " + repeat("[", n) + "0" + repeat("]", n) + ">(x: T) => x" },
|
||
|
|
// same shape nested inside array literals (matches the fuzzer-found input)
|
||
|
|
{
|
||
|
|
loader: "tsx",
|
||
|
|
code: n =>
|
||
|
|
repeat("[", 1000) + "<T extends " + repeat("[", n) + "0" + repeat("]", n) + ">(x: T) => x" + repeat("]", 1000),
|
||
|
|
},
|
||
|
|
// deeply nested tuple type in a type alias
|
||
|
|
{ loader: "ts", code: n => "type A = " + repeat("[", n) + "0" + repeat("]", n) + ";" },
|
||
|
|
// deeply nested destructuring pattern in a function type's arguments
|
||
|
|
{ loader: "ts", code: n => "type A = (" + repeat("[", n) + "a" + repeat("]", n) + ": any) => void;" },
|
||
|
|
];
|
||
|
|
for (const { loader, code } of shapes) {
|
||
|
|
for (const n of [4000, 20000, 100000]) {
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader,
|
||
|
|
target: "bun",
|
||
|
|
minifyWhitespace: true,
|
||
|
|
deadCodeElimination: true,
|
||
|
|
});
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(code(n));
|
||
|
|
} catch (e) {
|
||
|
|
if (!/Maximum call stack size exceeded|StackOverflow/.test(String(e?.message))) throw e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log("type-depth-ok");
|
||
|
|
`;
|
||
|
|
const { stdout, exitCode, signalCode } = Bun.spawnSync({
|
||
|
|
cmd: [bunExe(), "-e", script],
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
env: bunEnv,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(stdout.toString()).toBe("type-depth-ok\n");
|
||
|
|
expect([exitCode, signalCode ?? undefined]).toEqual([0, undefined]);
|
||
|
|
}, 60_000);
|
||
|
|
|
||
|
|
it("deeply nested statement blocks error instead of crashing the process", () => {
|
||
|
|
const script = `
|
||
|
|
const repeat = (fill, count) => Buffer.alloc(fill.length * count, fill).toString();
|
||
|
|
const shapes = [
|
||
|
|
n => repeat("{", n) + 'class Test1 { static "prop1" = 0; }' + repeat("}", n),
|
||
|
|
n => repeat("{", n) + "let x = 1;" + repeat("}", n),
|
||
|
|
n => repeat("if (x) {", n) + "y();" + repeat("}", n),
|
||
|
|
n => "if (x) { y(); }" + repeat(" else if (x) { y(); }", n),
|
||
|
|
];
|
||
|
|
const check = (transpiler, src) => {
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(src);
|
||
|
|
} catch (e) {
|
||
|
|
if (!/Maximum call stack size exceeded|StackOverflow/.test(String(e?.message))) throw e;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
for (const shape of shapes) {
|
||
|
|
for (const n of [600, 800, 990]) {
|
||
|
|
check(
|
||
|
|
new Bun.Transpiler({ loader: "tsx", target: "bun", minifyWhitespace: true, deadCodeElimination: true }),
|
||
|
|
shape(n),
|
||
|
|
);
|
||
|
|
check(new Bun.Transpiler({ loader: "js" }), shape(n));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log("depth-ok");
|
||
|
|
`;
|
||
|
|
const { stdout, exitCode, signalCode } = Bun.spawnSync({
|
||
|
|
cmd: [bunExe(), "-e", script],
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
env: bunEnv,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(stdout.toString()).toBe("depth-ok\n");
|
||
|
|
expect([exitCode, signalCode ?? undefined]).toEqual([0, undefined]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("running a file with deeply nested unary operators does not crash the process", () => {
|
||
|
|
const code = Buffer.alloc(2 * 4000, "- ").toString() + "1";
|
||
|
|
const { exitCode, signalCode } = Bun.spawnSync({
|
||
|
|
cmd: [bunExe(), "-e", code],
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
env: bunEnv,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(signalCode ?? undefined).toBeUndefined();
|
||
|
|
expect([0, 1]).toContain(exitCode);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not duplicate the branch when simplifying an unused ternary with a comma test", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "js" });
|
||
|
|
expect(transpiler.transformSync("(f(), g()) ? 1 : h();").trim()).toBe("f(), g() || h();");
|
||
|
|
expect(transpiler.transformSync("(f(), g()) ? h() : 1;").trim()).toBe("f(), g() && h();");
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("arrow function parsing after const declaration (scope mismatch bug)", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "tsx" });
|
||
|
|
|
||
|
|
it("reproduces the original scope mismatch bug with JSX", () => {
|
||
|
|
// This is the exact pattern that caused the scope mismatch panic
|
||
|
|
const code = `
|
||
|
|
const Layout = () => {
|
||
|
|
return (
|
||
|
|
<html>
|
||
|
|
</html>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
['1', 'p'].forEach(i =>
|
||
|
|
app.get(\`/\${i === 'home' ? '' : i}\`, c => c.html(
|
||
|
|
<Layout selected={i}>
|
||
|
|
Hello {i}
|
||
|
|
</Layout>
|
||
|
|
))
|
||
|
|
)`;
|
||
|
|
|
||
|
|
// Without the fix, this would parse the array as indexing into the arrow function
|
||
|
|
// causing a scope mismatch panic when visiting the AST
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
|
||
|
|
// The correct parse should have the array literal as a separate statement
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
// The bug would incorrectly parse as: })["1", "p"]
|
||
|
|
expect(result).not.toContain(')["');
|
||
|
|
expect(result).not.toContain('}["');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("correctly parses array literal on next line after block body arrow function", () => {
|
||
|
|
const code = `const Layout = () => {
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
['1', 'p'].forEach(i => console.log(i))`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
// The bug would cause the array to be parsed as indexing: Layout[...
|
||
|
|
expect(result).not.toContain(')["');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("correctly parses JSX arrow function followed by array literal", () => {
|
||
|
|
const code = `const Layout = () => {
|
||
|
|
return (
|
||
|
|
<html>
|
||
|
|
</html>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
['1', 'p'].forEach(i => console.log(i))`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
expect(result).not.toContain("Layout[");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects indexing directly into block body arrow function without parens", () => {
|
||
|
|
const code = `const Layout = () => {return 1}['x']`;
|
||
|
|
|
||
|
|
// Should throw a parse error - either "Parse error" or the more specific message
|
||
|
|
expect(() => transpiler.transformSync(code)).toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("allows indexing into parenthesized arrow function", () => {
|
||
|
|
const code = `const x = (() => {return {a: 1}})['a']`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain('["a"]');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("correctly handles expression body arrow functions", () => {
|
||
|
|
const code = `const Layout = () => 1
|
||
|
|
['1', 'p'].forEach(i => console.log(i))`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("correctly handles arrow function with comma operator", () => {
|
||
|
|
const code = `const a = () => {return 1}, b = 2`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("b = 2");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("correctly handles multiple arrow functions in const declaration", () => {
|
||
|
|
const code = `const a = () => {return 1}, b = () => {return 2}
|
||
|
|
['1', '2'].forEach(x => console.log(x))`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
expect(result).not.toContain("b[");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("preserves intentional array access with explicit semicolon", () => {
|
||
|
|
const code = `const Layout = () => {return 1};
|
||
|
|
['1', 'p'].forEach(i => console.log(i))`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
expect(result).not.toContain("Layout[");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles nested arrow functions correctly", () => {
|
||
|
|
const code = `const outer = () => {
|
||
|
|
const inner = () => {
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
return inner
|
||
|
|
}
|
||
|
|
['test'].forEach(x => x)`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("forEach");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles arrow function followed by object literal", () => {
|
||
|
|
const code = `const fn = () => {return 1}
|
||
|
|
({a: 1, b: 2}).a`;
|
||
|
|
|
||
|
|
const result = transpiler.transformSync(code);
|
||
|
|
expect(result).toContain("a: 1");
|
||
|
|
expect(result).not.toContain("fn(");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("export of a block-scoped function declaration", () => {
|
||
|
|
const code = "{\n function encrypt() {}\n}\nexport { encrypt }";
|
||
|
|
|
||
|
|
function expectNotDeclaredError(loader) {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader });
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(code);
|
||
|
|
expect.unreachable();
|
||
|
|
} catch (e) {
|
||
|
|
const error = e instanceof AggregateError ? e.errors[0] : e;
|
||
|
|
expect(error.message).toBe('"encrypt" is not declared in this file');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
it("is a parse error for JavaScript", () => {
|
||
|
|
expectNotDeclaredError("js");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("is a parse error for JSX", () => {
|
||
|
|
expectNotDeclaredError("jsx");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("is stripped like a type-only export for TypeScript", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
||
|
|
const out = transpiler.transformSync(code);
|
||
|
|
expect(out).not.toContain("export { encrypt }");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("still allows exporting a top-level function declaration", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "js" });
|
||
|
|
const out = transpiler.transformSync("function encrypt() {}\nexport { encrypt }");
|
||
|
|
expect(out).toContain("export { encrypt }");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("still allows exporting a var declared inside a block", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "js" });
|
||
|
|
const out = transpiler.transformSync("{\n var encrypt = 1;\n}\nexport { encrypt }");
|
||
|
|
expect(out).toContain("export { encrypt }");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not affect block-level function declarations in sloppy mode", () => {
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "js" });
|
||
|
|
const out = transpiler.transformSync("{\n function f() {}\n}\nmodule.exports = f;");
|
||
|
|
expect(out).toContain("let f = function");
|
||
|
|
expect(out).toContain("module.exports = f");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("reports the error when running a module with this pattern", async () => {
|
||
|
|
using dir = tempDir("block-scoped-fn-export", {
|
||
|
|
"mod.mjs": code + "\n",
|
||
|
|
});
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "mod.mjs"],
|
||
|
|
env: bunEnv,
|
||
|
|
cwd: String(dir),
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toContain('"encrypt" is not declared in this file');
|
||
|
|
expect(exitCode).toBe(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("using declarations in switch statements", () => {
|
||
|
|
const reparse = out => new Bun.Transpiler({ loader: "js" }).transformSync(out);
|
||
|
|
|
||
|
|
it("lowers by wrapping the entire switch in a single try/finally", () => {
|
||
|
|
const input =
|
||
|
|
"switch (dom()) {\n case 0:\n using d23 = { [Se]() {} };\n default:\n using d24 = { [ose]() {} };\n }";
|
||
|
|
|
||
|
|
for (const minifyWhitespace of [false, true]) {
|
||
|
|
const out = new Bun.Transpiler({ loader: "jsx", target: "node", minifyWhitespace }).transformSync(input);
|
||
|
|
expect(() => reparse(out)).not.toThrow();
|
||
|
|
expect(out).toMatch(/try\s*\{\s*switch\s*\(dom\(\)\)/);
|
||
|
|
expect(out.match(/finally/g)).toHaveLength(1);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("lowers `await using` in switch cases the same way", () => {
|
||
|
|
const input = `async function f(x) {
|
||
|
|
switch (x()) {
|
||
|
|
case 0:
|
||
|
|
await using a = y();
|
||
|
|
default:
|
||
|
|
await using b = z();
|
||
|
|
}
|
||
|
|
}`;
|
||
|
|
const out = new Bun.Transpiler({ loader: "js", target: "node" }).transformSync(input);
|
||
|
|
expect(() => reparse(out)).not.toThrow();
|
||
|
|
expect(out).toMatch(/try\s*\{\s*switch\s*\(x\(\)\)/);
|
||
|
|
expect(out.match(/finally/g)).toHaveLength(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keeps generated temp refs unique across sibling switches in the same scope", () => {
|
||
|
|
const input = `
|
||
|
|
switch (a()) { case 0: using x = { [s]() {} }; }
|
||
|
|
switch (b()) { case 1: using y = { [t]() {} }; }
|
||
|
|
`;
|
||
|
|
const out = new Bun.Transpiler({ loader: "js", target: "node", minifyWhitespace: true }).transformSync(input);
|
||
|
|
expect(() => reparse(out)).not.toThrow();
|
||
|
|
expect(out.match(/finally/g)).toHaveLength(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keeps case bindings const when combined with top-level using declarations", () => {
|
||
|
|
const input = `
|
||
|
|
using top = r();
|
||
|
|
switch (a()) {
|
||
|
|
case 0:
|
||
|
|
using x = { [s]() {} };
|
||
|
|
default:
|
||
|
|
using y = { [t]() {} };
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
const out = new Bun.Transpiler({ loader: "js", target: "node", minifyWhitespace: true }).transformSync(input);
|
||
|
|
expect(() => reparse(out)).not.toThrow();
|
||
|
|
expect(out).toMatch(/const x\s*=\s*__using/);
|
||
|
|
expect(out).toMatch(/const y\s*=\s*__using/);
|
||
|
|
expect(out).not.toMatch(/var [xy]\b/);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("disposes at switch exit in reverse order and keeps bindings visible across cases", async () => {
|
||
|
|
const source = `
|
||
|
|
const order = [];
|
||
|
|
function resource(name) {
|
||
|
|
return { [Symbol.dispose]() { order.push("dispose " + name); } };
|
||
|
|
}
|
||
|
|
function run(value) {
|
||
|
|
switch (value) {
|
||
|
|
case 0:
|
||
|
|
using a = resource("a");
|
||
|
|
order.push("case 0");
|
||
|
|
default:
|
||
|
|
using b = resource("b");
|
||
|
|
order.push("default sees a: " + (a !== undefined));
|
||
|
|
}
|
||
|
|
order.push("after switch");
|
||
|
|
}
|
||
|
|
run(0);
|
||
|
|
console.log(JSON.stringify(order));
|
||
|
|
`;
|
||
|
|
|
||
|
|
const lowered = new Bun.Transpiler({ loader: "js", target: "node" }).transformSync(source);
|
||
|
|
expect(lowered).toContain("__using");
|
||
|
|
|
||
|
|
using dir = tempDir("using-switch-lowering", { "lowered.mjs": lowered });
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "lowered.mjs"],
|
||
|
|
env: bunEnv,
|
||
|
|
cwd: String(dir),
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(JSON.parse(stdout)).toEqual(["case 0", "default sees a: true", "dispose b", "dispose a", "after switch"]);
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("minifyWhitespace keeps the space before keyword operators", () => {
|
||
|
|
const minifier = new Bun.Transpiler({ loader: "js", minifyWhitespace: true });
|
||
|
|
|
||
|
|
it("between a single-character identifier and 'instanceof'", () => {
|
||
|
|
expect(minifier.transformSync("x instanceof y")).toBe("x instanceof y;");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("between a single-character identifier and 'in'", () => {
|
||
|
|
expect(minifier.transformSync("x in y")).toBe("x in y;");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("between a numeric literal and 'in'", () => {
|
||
|
|
expect(minifier.transformSync("1 in y")).toBe("1 in y;");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("transform() result is unaffected by detaching the input ArrayBuffer while the task is in flight", async () => {
|
||
|
|
// The async transform parses the input on a work-pool thread. The input bytes
|
||
|
|
// must be copied before the thread hop so that detaching the ArrayBuffer from
|
||
|
|
// the JS thread mid-parse cannot change or free the memory being read.
|
||
|
|
const script = `
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "js" });
|
||
|
|
const size = 1 << 20;
|
||
|
|
const source = "export const original = 12345;";
|
||
|
|
const bytes = new Uint8Array(size).fill(0x20);
|
||
|
|
new TextEncoder().encodeInto(source, bytes);
|
||
|
|
const expected = transpiler.transformSync(new TextDecoder().decode(bytes), "js");
|
||
|
|
|
||
|
|
const promise = transpiler.transform(bytes, "js");
|
||
|
|
// Detach the backing store while the worker thread may still be parsing it.
|
||
|
|
bytes.buffer.transfer(0);
|
||
|
|
// Recycle similarly-sized allocations holding different valid JS so a stale
|
||
|
|
// read of the old backing store would produce observably different output.
|
||
|
|
const decoys = [];
|
||
|
|
for (let i = 0; i < 8; i++) {
|
||
|
|
const decoy = new Uint8Array(size).fill(0x20);
|
||
|
|
new TextEncoder().encodeInto("export const replaced" + i + " = " + i + ";", decoy);
|
||
|
|
decoys.push(decoy);
|
||
|
|
}
|
||
|
|
const out = await promise;
|
||
|
|
if (out === expected && !out.includes("replaced")) {
|
||
|
|
console.log("OK");
|
||
|
|
} else {
|
||
|
|
console.log("MISMATCH " + JSON.stringify(out.slice(0, 200)));
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [bunExe(), "-e", script],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout.trim()).toBe("OK");
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
// A numeric literal property name like `1e999` overflows to the number Infinity, which the
|
||
|
|
// printer emits as "1/0" / "1 / 0". That is not valid syntax in property-name position, so such
|
||
|
|
// keys must be printed as computed properties instead.
|
||
|
|
describe("numeric property keys that overflow to Infinity", () => {
|
||
|
|
const minifier = new Bun.Transpiler({ loader: "ts", minifyWhitespace: true });
|
||
|
|
const plain = new Bun.Transpiler({ loader: "ts" });
|
||
|
|
|
||
|
|
it("are printed as computed properties when minifying whitespace", () => {
|
||
|
|
expect(minifier.transformSync("x = { 1e999: 1 };")).toBe("x={[1/0]:1};");
|
||
|
|
expect(minifier.transformSync("x = { 1e999() {} };")).toBe("x={[1/0](){}};");
|
||
|
|
expect(minifier.transformSync("x = { get 1e999() {} };")).toBe("x={get[1/0](){}};");
|
||
|
|
expect(minifier.transformSync("x = { set 1e999(v) {} };")).toBe("x={set[1/0](v){}};");
|
||
|
|
expect(minifier.transformSync("x = class { 1e999() {} };")).toBe("x=class{[1/0](){}};");
|
||
|
|
expect(minifier.transformSync("x = class { static 1e999() {} };")).toBe("x=class{static[1/0](){}};");
|
||
|
|
expect(minifier.transformSync("x = class { 1e999 = 1 };")).toBe("x=class{[1/0]=1};");
|
||
|
|
expect(minifier.transformSync("x = class { static 1e999 = 1 };")).toBe("x=class{static[1/0]=1};");
|
||
|
|
expect(minifier.transformSync("const { 1e999: y } = x;")).toBe("const{[1/0]:y}=x;");
|
||
|
|
expect(minifier.transformSync("({ 1e999: x.y } = z);")).toBe("({[1/0]:x.y}=z);");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("are printed as computed properties without minification", () => {
|
||
|
|
expect(plain.transformSync("x = { 1e999: 1 };")).toBe("x = { [1 / 0]: 1 };\n");
|
||
|
|
expect(plain.transformSync("x = class { 1e999() {} };")).toBe("x = class {\n [1 / 0]() {}\n};\n");
|
||
|
|
expect(plain.transformSync("const { 1e999: y } = x;")).toBe("const { [1 / 0]: y } = x;\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles a method name with hundreds of digits", () => {
|
||
|
|
const digits = Buffer.alloc(325, "9").toString();
|
||
|
|
expect(minifier.transformSync(`(class { ${digits}() {} });`)).toBe("(class{[1/0](){}});");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("still refers to the same property at runtime", () => {
|
||
|
|
const out = minifier.transformSync(`
|
||
|
|
const obj = { 1e999: "object" };
|
||
|
|
const { 1e999: destructured } = { 1e999: "destructured" };
|
||
|
|
class C {
|
||
|
|
1e999() { return "method"; }
|
||
|
|
static 1e999 = "static";
|
||
|
|
}
|
||
|
|
var result = [obj[Infinity], destructured, new C()[Infinity](), C[Infinity]];
|
||
|
|
`);
|
||
|
|
expect(new Function(`${out}; return result;`)()).toEqual(["object", "destructured", "method", "static"]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("parse error flood", () => {
|
||
|
|
it("reports duplicate-binding floods in linear time", async () => {
|
||
|
|
await using proc = Bun.spawn({
|
||
|
|
cmd: [
|
||
|
|
bunExe(),
|
||
|
|
"-e",
|
||
|
|
`
|
||
|
|
const transpiler = new Bun.Transpiler({
|
||
|
|
loader: "js",
|
||
|
|
target: "browser",
|
||
|
|
minifyWhitespace: true,
|
||
|
|
deadCodeElimination: true,
|
||
|
|
});
|
||
|
|
const check = (label, statement, repeats) => {
|
||
|
|
const input = Buffer.alloc(statement.length * repeats, statement).toString();
|
||
|
|
let threw;
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(input);
|
||
|
|
} catch (e) {
|
||
|
|
threw = e;
|
||
|
|
}
|
||
|
|
if (threw?.name !== "AggregateError") throw new Error("expected AggregateError, got " + threw);
|
||
|
|
if (!threw.errors.some(e => String(e.message).includes("has already been declared"))) {
|
||
|
|
throw new Error("expected duplicate-declaration errors");
|
||
|
|
}
|
||
|
|
console.log("OK " + label);
|
||
|
|
};
|
||
|
|
const bindings = Buffer.alloc(420, "a,").toString();
|
||
|
|
check("template catch flood", "try {} catch ([" + bindings + "a, \`]) {}\\n", 800);
|
||
|
|
check("duplicate catch flood", "try {} catch ([" + bindings + "a]) {}\\n", 400);
|
||
|
|
console.log("DONE");
|
||
|
|
`,
|
||
|
|
],
|
||
|
|
env: bunEnv,
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
timeout: 60_000,
|
||
|
|
killSignal: "SIGKILL",
|
||
|
|
});
|
||
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||
|
|
expect(stderr).toBe("");
|
||
|
|
expect(stdout).toContain("OK template catch flood");
|
||
|
|
expect(stdout).toContain("OK duplicate catch flood");
|
||
|
|
expect(stdout).toContain("DONE");
|
||
|
|
expect(exitCode).toBe(0);
|
||
|
|
}, 90_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("multi-line comment scanning", () => {
|
||
|
|
// The lexer's block-comment scanner switches to a SIMD skip
|
||
|
|
// (bun_highway::index_of_interesting_character_in_multiline_comment) when at
|
||
|
|
// least 512 bytes of input remain, so cover sizes on both sides of that
|
||
|
|
// threshold and around vector-width boundaries, plus every byte class the
|
||
|
|
// skip has to stop at ('*', '\r', '\n', non-ASCII).
|
||
|
|
const transpiler = new Bun.Transpiler({ loader: "js" });
|
||
|
|
const xPad = Buffer.alloc(8193, "x").toString();
|
||
|
|
const pad600 = xPad.slice(0, 600);
|
||
|
|
|
||
|
|
const expectParseError = (code, message) => {
|
||
|
|
try {
|
||
|
|
transpiler.transformSync(code);
|
||
|
|
} catch (er) {
|
||
|
|
let err = er;
|
||
|
|
if (er instanceof AggregateError) {
|
||
|
|
err = er.errors[0];
|
||
|
|
}
|
||
|
|
expect(err.message).toBe(message);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
throw new Error("Expected parse error for code\n\t" + code);
|
||
|
|
};
|
||
|
|
|
||
|
|
it("strips block comments of every size around the SIMD threshold", () => {
|
||
|
|
const sizes = [];
|
||
|
|
for (let size = 480; size <= 576; size++) sizes.push(size);
|
||
|
|
sizes.push(1000, 4095, 4096, 4097, 8193);
|
||
|
|
for (const size of sizes) {
|
||
|
|
const out = transpiler.transformSync(`/*${xPad.slice(0, size)}*/ pass();`);
|
||
|
|
expect({ size, out }).toEqual({ size, out: "pass();\n" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles a lone '*' at every offset inside a large comment", () => {
|
||
|
|
const aPad = Buffer.alloc(80, "a").toString();
|
||
|
|
const bPad = Buffer.alloc(700, "b").toString();
|
||
|
|
for (let offset = 0; offset < 80; offset++) {
|
||
|
|
const body = aPad.slice(0, offset) + "*" + bPad.slice(0, 700 - offset);
|
||
|
|
const out = transpiler.transformSync(`/*${body}*/ pass();`);
|
||
|
|
expect({ offset, out }).toEqual({ offset, out: "pass();\n" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles comments made entirely of '*'", () => {
|
||
|
|
const stars = Buffer.alloc(600, "*").toString();
|
||
|
|
expect(transpiler.transformSync(`/*${stars}*/ pass();`)).toBe("pass();\n");
|
||
|
|
expect(transpiler.transformSync(`/*${stars}/ pass();`)).toBe("pass();\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("treats newlines inside a large block comment as line terminators (ASI)", () => {
|
||
|
|
for (const newline of ["\n", "\r", "\r\n", "\u2028", "\u2029"]) {
|
||
|
|
const out = transpiler.transformSync(`function f() { return /*${pad600}${newline}${pad600}*/ 1 }`);
|
||
|
|
expect({ newline, out }).toEqual({ newline, out: "function f() {\n return;\n}\n" });
|
||
|
|
}
|
||
|
|
// control: no newline anywhere inside the comment, so no ASI
|
||
|
|
expect(transpiler.transformSync(`function f() { return /*${pad600}${pad600}*/ 1 }`)).toBe(
|
||
|
|
"function f() {\n return 1;\n}\n",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("scans large comments containing non-ASCII text", () => {
|
||
|
|
expect(transpiler.transformSync(`/*${"é".repeat(400)}*/ pass();`)).toBe("pass();\n");
|
||
|
|
expect(transpiler.transformSync(`/*${"🦊".repeat(200)}*/ pass();`)).toBe("pass();\n");
|
||
|
|
expect(transpiler.transformSync(`/* ${pad600} 日本語のコメント ${pad600} */ pass();`)).toBe("pass();\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not corrupt code around a large comment", () => {
|
||
|
|
const dashes = Buffer.alloc(700, "-").toString();
|
||
|
|
const out = transpiler.transformSync(`const a = "before";/*${dashes}*/const b = "after"; console.log(a, b);`);
|
||
|
|
expect(out).toBe('const a = "before";\nconst b = "after";\nconsole.log(a, b);\n');
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles a large comment that ends exactly at EOF", () => {
|
||
|
|
expect(transpiler.transformSync(`pass(); /*${pad600}*/`)).toBe("pass();\n");
|
||
|
|
expect(transpiler.transformSync(`pass(); /*${pad600}**/`)).toBe("pass();\n");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("reports unterminated large block comments", () => {
|
||
|
|
const message = 'Expected "*/" to terminate multi-line comment';
|
||
|
|
expectParseError(`/*${pad600}`, message);
|
||
|
|
expectParseError(`/*${pad600}*`, message);
|
||
|
|
expectParseError(`/*${Buffer.alloc(600, "*").toString()}`, message);
|
||
|
|
expectParseError(`/*${pad600}🦊`, message);
|
||
|
|
});
|
||
|
|
});
|