mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
esm bytecode (#26402)
### What does this PR do? ### How did you verify your code works? --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { rmSync } from "fs";
|
||||
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
|
||||
import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
import { itBundled } from "./expectBundled";
|
||||
|
||||
describe("bundler", () => {
|
||||
@@ -89,6 +90,135 @@ describe("bundler", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
// ESM bytecode test matrix: each scenario × {default, minified} = 2 tests per scenario.
|
||||
// With --compile, static imports are inlined into one chunk, but dynamic imports
|
||||
// create separate modules in the standalone graph — each with its own bytecode + ModuleInfo.
|
||||
const esmBytecodeScenarios: Array<{
|
||||
name: string;
|
||||
files: Record<string, string>;
|
||||
stdout: string;
|
||||
}> = [
|
||||
{
|
||||
name: "HelloWorld",
|
||||
files: {
|
||||
"/entry.ts": `console.log("Hello, world!");`,
|
||||
},
|
||||
stdout: "Hello, world!",
|
||||
},
|
||||
{
|
||||
// top-level await is ESM-only; if ModuleInfo or bytecode generation
|
||||
// mishandles async modules, this breaks.
|
||||
name: "TopLevelAwait",
|
||||
files: {
|
||||
"/entry.ts": `
|
||||
const result = await Promise.resolve("tla works");
|
||||
console.log(result);
|
||||
`,
|
||||
},
|
||||
stdout: "tla works",
|
||||
},
|
||||
{
|
||||
// import.meta is ESM-only.
|
||||
name: "ImportMeta",
|
||||
files: {
|
||||
"/entry.ts": `
|
||||
console.log(typeof import.meta.url === "string" ? "ok" : "fail");
|
||||
console.log(typeof import.meta.dir === "string" ? "ok" : "fail");
|
||||
`,
|
||||
},
|
||||
stdout: "ok\nok",
|
||||
},
|
||||
{
|
||||
// Dynamic import creates a separate module in the standalone graph,
|
||||
// exercising per-module bytecode + ModuleInfo.
|
||||
name: "DynamicImport",
|
||||
files: {
|
||||
"/entry.ts": `
|
||||
const { value } = await import("./lazy.ts");
|
||||
console.log("lazy:", value);
|
||||
`,
|
||||
"/lazy.ts": `export const value = 42;`,
|
||||
},
|
||||
stdout: "lazy: 42",
|
||||
},
|
||||
{
|
||||
// Dynamic import of a module that itself uses top-level await.
|
||||
// The dynamically imported module is a separate chunk with async
|
||||
// evaluation — stresses both ModuleInfo and async bytecode loading.
|
||||
name: "DynamicImportTLA",
|
||||
files: {
|
||||
"/entry.ts": `
|
||||
const mod = await import("./async-mod.ts");
|
||||
console.log("value:", mod.value);
|
||||
`,
|
||||
"/async-mod.ts": `export const value = await Promise.resolve(99);`,
|
||||
},
|
||||
stdout: "value: 99",
|
||||
},
|
||||
{
|
||||
// Multiple dynamic imports: several separate modules in the graph,
|
||||
// each with its own bytecode + ModuleInfo.
|
||||
name: "MultipleDynamicImports",
|
||||
files: {
|
||||
"/entry.ts": `
|
||||
const [a, b] = await Promise.all([
|
||||
import("./mod-a.ts"),
|
||||
import("./mod-b.ts"),
|
||||
]);
|
||||
console.log(a.value, b.value);
|
||||
`,
|
||||
"/mod-a.ts": `export const value = "a";`,
|
||||
"/mod-b.ts": `export const value = "b";`,
|
||||
},
|
||||
stdout: "a b",
|
||||
},
|
||||
];
|
||||
|
||||
for (const scenario of esmBytecodeScenarios) {
|
||||
for (const minify of [false, true]) {
|
||||
itBundled(`compile/ESMBytecode+${scenario.name}${minify ? "+minify" : ""}`, {
|
||||
compile: true,
|
||||
bytecode: true,
|
||||
format: "esm",
|
||||
...(minify && {
|
||||
minifySyntax: true,
|
||||
minifyIdentifiers: true,
|
||||
minifyWhitespace: true,
|
||||
}),
|
||||
files: scenario.files,
|
||||
run: { stdout: scenario.stdout },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-entry ESM bytecode with Worker (can't be in the matrix — needs
|
||||
// entryPointsRaw, outfile, setCwd). Each entry becomes a separate module
|
||||
// in the standalone graph with its own bytecode + ModuleInfo.
|
||||
itBundled("compile/WorkerBytecodeESM", {
|
||||
backend: "cli",
|
||||
compile: true,
|
||||
bytecode: true,
|
||||
format: "esm",
|
||||
files: {
|
||||
"/entry.ts": /* js */ `
|
||||
import {rmSync} from 'fs';
|
||||
// Verify we're not just importing from the filesystem
|
||||
rmSync("./worker.ts", {force: true});
|
||||
console.log("Hello, world!");
|
||||
new Worker("./worker.ts");
|
||||
`,
|
||||
"/worker.ts": /* js */ `
|
||||
console.log("Worker loaded!");
|
||||
`.trim(),
|
||||
},
|
||||
entryPointsRaw: ["./entry.ts", "./worker.ts"],
|
||||
outfile: "dist/out",
|
||||
run: {
|
||||
stdout: "Hello, world!\nWorker loaded!\n",
|
||||
file: "dist/out",
|
||||
setCwd: true,
|
||||
},
|
||||
});
|
||||
// https://github.com/oven-sh/bun/issues/8697
|
||||
itBundled("compile/EmbeddedFileOutfile", {
|
||||
compile: true,
|
||||
@@ -311,6 +441,8 @@ describe("bundler", () => {
|
||||
format: "cjs" | "esm";
|
||||
}> = [
|
||||
{ bytecode: true, minify: true, format: "cjs" },
|
||||
{ bytecode: true, format: "esm" },
|
||||
{ bytecode: true, minify: true, format: "esm" },
|
||||
{ format: "cjs" },
|
||||
{ format: "cjs", minify: true },
|
||||
{ format: "esm" },
|
||||
@@ -736,6 +868,54 @@ const server = serve({
|
||||
.throws(true);
|
||||
});
|
||||
|
||||
// Verify ESM bytecode is actually loaded from the cache at runtime, not just generated.
|
||||
// Uses regex matching on stderr (not itBundled) since we don't know the exact
|
||||
// number of cache hit/miss lines for ESM standalone.
|
||||
test("ESM bytecode cache is used at runtime", async () => {
|
||||
const ext = isWindows ? ".exe" : "";
|
||||
using dir = tempDir("esm-bytecode-cache", {
|
||||
"entry.js": `console.log("esm bytecode loaded");`,
|
||||
});
|
||||
|
||||
const outfile = join(String(dir), `app${ext}`);
|
||||
|
||||
// Build with ESM + bytecode
|
||||
await using build = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"build",
|
||||
"--compile",
|
||||
"--bytecode",
|
||||
"--format=esm",
|
||||
join(String(dir), "entry.js"),
|
||||
"--outfile",
|
||||
outfile,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]);
|
||||
|
||||
expect(buildStderr).toBe("");
|
||||
expect(buildExitCode).toBe(0);
|
||||
|
||||
// Run with verbose disk cache to verify bytecode is loaded
|
||||
await using exe = Bun.spawn({
|
||||
cmd: [outfile],
|
||||
env: { ...bunEnv, BUN_JSC_verboseDiskCache: "1" },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [exeStdout, exeStderr, exeExitCode] = await Promise.all([exe.stdout.text(), exe.stderr.text(), exe.exited]);
|
||||
|
||||
expect(exeStdout).toContain("esm bytecode loaded");
|
||||
expect(exeStderr).toMatch(/\[Disk Cache\].*Cache hit/i);
|
||||
expect(exeExitCode).toBe(0);
|
||||
});
|
||||
|
||||
// When compiling with 8+ entry points, the main entry point should still run correctly.
|
||||
test("compile with 8+ entry points runs main entry correctly", async () => {
|
||||
const dir = tempDirWithFiles("compile-many-entries", {
|
||||
|
||||
@@ -36,5 +36,30 @@ describe("bundler", () => {
|
||||
stdout: "app entry\nheader rendering\nmenu showing\nitems: home,about,contact",
|
||||
},
|
||||
});
|
||||
|
||||
for (const minify of [false, true]) {
|
||||
itBundled(`compile/splitting/ImportMetaInSplitChunk${minify ? "+minify" : ""}`, {
|
||||
compile: true,
|
||||
splitting: true,
|
||||
bytecode: true,
|
||||
format: "esm",
|
||||
...(minify ? { minifySyntax: true, minifyIdentifiers: true, minifyWhitespace: true } : {}),
|
||||
files: {
|
||||
"/entry.ts": /* js */ `
|
||||
const mod = await import("./worker.ts");
|
||||
mod.run();
|
||||
`,
|
||||
"/worker.ts": /* js */ `
|
||||
export function run() {
|
||||
console.log(typeof import.meta.url === "string" ? "ok" : "fail");
|
||||
console.log(typeof import.meta.dir === "string" ? "ok" : "fail");
|
||||
}
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
stdout: "ok\nok",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,8 +163,8 @@ describe.skipIf(!isWindows).concurrent("Windows compile metadata", () => {
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
// When cross-compiling to non-Windows, it tries to download the target but fails
|
||||
expect(stderr.toLowerCase()).toContain("target platform");
|
||||
// Windows flags require a Windows compile target
|
||||
expect(stderr.toLowerCase()).toContain("windows compile target");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "bun:test" with { todo: "true" };
|
||||
import { expect, test } from "bun:test";
|
||||
import "reflect-metadata";
|
||||
function Abc() {
|
||||
return (target: any, field: string) => {};
|
||||
|
||||
499
test/js/bun/typescript/type-export.test.ts
Normal file
499
test/js/bun/typescript/type-export.test.ts
Normal file
@@ -0,0 +1,499 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
|
||||
|
||||
const ext = isWindows ? ".exe" : "";
|
||||
|
||||
function compileAndRun(dir: string, entrypoint: string) {
|
||||
const outfile = dir + `/compiled${ext}`;
|
||||
const buildResult = Bun.spawnSync({
|
||||
cmd: [bunExe(), "build", "--compile", "--bytecode", "--format=esm", entrypoint, "--outfile", outfile],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
expect(buildResult.stderr.toString()).toBe("");
|
||||
expect(buildResult.exitCode).toBe(0);
|
||||
|
||||
return Bun.spawnSync({
|
||||
cmd: [outfile],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
const a_file = `
|
||||
export type my_string = "1";
|
||||
|
||||
export type my_value = "2";
|
||||
export const my_value = "2";
|
||||
|
||||
export const my_only = "3";
|
||||
`;
|
||||
|
||||
const a_no_value = `
|
||||
export type my_string = "1";
|
||||
export type my_value = "2";
|
||||
export const my_only = "3";
|
||||
`;
|
||||
|
||||
const a_with_value = `
|
||||
export type my_string = "1";
|
||||
export const my_value = "2";
|
||||
`;
|
||||
|
||||
const b_files = [
|
||||
{
|
||||
name: "export from",
|
||||
value: `export { my_string, my_value, my_only } from "./a.ts";`,
|
||||
},
|
||||
{
|
||||
name: "import then export",
|
||||
value: `
|
||||
import { my_string, my_value, my_only } from "./a.ts";
|
||||
export { my_string, my_value, my_only };
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "export star",
|
||||
value: `export * from "./a.ts";`,
|
||||
},
|
||||
{
|
||||
name: "export merge",
|
||||
value: `export * from "./a_no_value.ts"; export * from "./a_with_value.ts"`,
|
||||
},
|
||||
];
|
||||
|
||||
const c_files = [
|
||||
{ name: "require", value: `console.log(JSON.stringify(require("./b")));` },
|
||||
{ name: "import star", value: `import * as b from "./b"; console.log(JSON.stringify(b));` },
|
||||
{ name: "await import", value: `console.log(JSON.stringify(await import("./b")));` },
|
||||
{
|
||||
name: "import individual",
|
||||
value: `
|
||||
import { my_string, my_value, my_only } from "./b";
|
||||
console.log(JSON.stringify({ my_only, my_value }));
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const b_file of b_files) {
|
||||
describe(`re-export with ${b_file.name}`, () => {
|
||||
for (const c_file of c_files) {
|
||||
describe(`import with ${c_file.name}`, () => {
|
||||
const dir = tempDirWithFiles("type-export", {
|
||||
"a.ts": a_file,
|
||||
"b.ts": b_file.value,
|
||||
"c.ts": c_file.value,
|
||||
"a_no_value.ts": a_no_value,
|
||||
"a_with_value.ts": a_with_value,
|
||||
});
|
||||
|
||||
describe.each(["run", "compile", "build"])("%s", mode => {
|
||||
// TODO: "run" is skipped until ESM module_info is enabled in the runtime transpiler.
|
||||
// Currently module_info is only generated for standalone ESM bytecode (--compile).
|
||||
// Once enabled, flip this to include "run".
|
||||
test.skipIf(mode === "run")("works", async () => {
|
||||
let result: Bun.SyncSubprocess<"pipe", "inherit"> | Bun.SyncSubprocess<"pipe", "pipe">;
|
||||
if (mode === "compile") {
|
||||
result = compileAndRun(dir, dir + "/c.ts");
|
||||
} else if (mode === "build") {
|
||||
const build_result = await Bun.build({
|
||||
entrypoints: [dir + "/c.ts"],
|
||||
outdir: dir + "/dist",
|
||||
});
|
||||
expect(build_result.success).toBe(true);
|
||||
result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "run", dir + "/dist/c.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
} else {
|
||||
result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "run", "c.ts"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
});
|
||||
}
|
||||
|
||||
const parsedOutput = JSON.parse(result.stdout.toString().trim());
|
||||
expect(parsedOutput).toEqual({ my_value: "2", my_only: "3" });
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe("import not found", () => {
|
||||
for (const [ccase, target_value, name] of [
|
||||
[``, /SyntaxError: Export named 'not_found' not found in module '[^']+?'\./, "none"],
|
||||
[
|
||||
`export default function not_found() {};`,
|
||||
/SyntaxError: Export named 'not_found' not found in module '[^']+?'\. Did you mean to import default\?/,
|
||||
"default with same name",
|
||||
],
|
||||
[
|
||||
`export type not_found = "not_found";`,
|
||||
/SyntaxError: Export named 'not_found' not found in module '[^']+?'\./,
|
||||
"type",
|
||||
],
|
||||
] as const)
|
||||
test(`${name}`, () => {
|
||||
const dir = tempDirWithFiles("type-export", {
|
||||
"a.ts": ccase,
|
||||
"b.ts": /*js*/ `
|
||||
import { not_found } from "./a";
|
||||
console.log(not_found);
|
||||
`,
|
||||
"nf.ts": "",
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "run", "b.ts"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toMatch(target_value);
|
||||
expect({
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout?.toString().trim(),
|
||||
}).toEqual({
|
||||
exitCode: 1,
|
||||
stdout: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("js file type export", () => {
|
||||
const dir = tempDirWithFiles("type-export", {
|
||||
"a.js": "export {not_found};",
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "a.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toInclude('error: "not_found" is not declared in this file');
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("js file type import", () => {
|
||||
const dir = tempDirWithFiles("type-import", {
|
||||
"b.js": "import {type_only} from './ts.ts';",
|
||||
"ts.ts": "export type type_only = 'type_only';",
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "b.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toInclude("Export named 'type_only' not found in module '");
|
||||
expect(result.stderr?.toString().trim()).not.toInclude("Did you mean to import default?");
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("js file type import with default export", () => {
|
||||
const dir = tempDirWithFiles("type-import", {
|
||||
"b.js": "import {type_only} from './ts.ts';",
|
||||
"ts.ts": "export type type_only = 'type_only'; export default function type_only() {};",
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "b.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toInclude("Export named 'type_only' not found in module '");
|
||||
expect(result.stderr?.toString().trim()).toInclude("Did you mean to import default?");
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("js file with through export", () => {
|
||||
const dir = tempDirWithFiles("type-import", {
|
||||
"b.js": "export {type_only} from './ts.ts';",
|
||||
"ts.ts": "export type type_only = 'type_only'; export default function type_only() {};",
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "b.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toInclude("SyntaxError: export 'type_only' not found in './ts.ts'");
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("js file with through export 2", () => {
|
||||
const dir = tempDirWithFiles("type-import", {
|
||||
"b.js": "import {type_only} from './ts.ts'; export {type_only};",
|
||||
"ts.ts": "export type type_only = 'type_only'; export default function type_only() {};",
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "b.js"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toInclude("SyntaxError: export 'type_only' not found in './ts.ts'");
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
describe("through export merge", () => {
|
||||
// this isn't allowed, even in typescript (tsc emits "Duplicate identifier 'value'.")
|
||||
for (const fmt of ["js", "ts"]) {
|
||||
describe(fmt, () => {
|
||||
for (const [name, mode] of [
|
||||
["through", "export {value} from './b'; export {value} from './c';"],
|
||||
["direct", "export {value} from './b'; export const value = 'abc';"],
|
||||
["direct2", "export const value = 'abc'; export {value};"],
|
||||
["ns", "export * as value from './c'; export * as value from './c';"],
|
||||
]) {
|
||||
describe(name, () => {
|
||||
const dir = tempDirWithFiles("type-import", {
|
||||
["main." + fmt]: "import {value} from './a'; console.log(value);",
|
||||
["a." + fmt]: mode,
|
||||
["b." + fmt]: fmt === "ts" ? "export type value = 'b';" : "",
|
||||
["c." + fmt]: "export const value = 'c';",
|
||||
});
|
||||
|
||||
for (const file of ["main." + fmt, "a." + fmt]) {
|
||||
test(file, () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), file],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toInclude(
|
||||
file === "a." + fmt
|
||||
? 'error: Multiple exports with the same name "value"\n' // bun's syntax error
|
||||
: "SyntaxError: Cannot export a duplicate name 'value'.\n", // jsc's syntax error
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("check ownkeys from a star import", () => {
|
||||
const dir = tempDirWithFiles("ownkeys-star-import", {
|
||||
["main.ts"]: `
|
||||
import * as ns from './a';
|
||||
console.log(JSON.stringify({
|
||||
keys: Object.keys(ns).sort(),
|
||||
ns,
|
||||
has_sometype: Object.hasOwn(ns, 'sometype'),
|
||||
}));
|
||||
`,
|
||||
["a.ts"]: "export * from './b'; export {sometype} from './b';",
|
||||
["b.ts"]: "export const value = 'b'; export const anotherValue = 'another'; export type sometype = 'sometype';",
|
||||
});
|
||||
|
||||
const expected = {
|
||||
keys: ["anotherValue", "value"],
|
||||
ns: {
|
||||
anotherValue: "another",
|
||||
value: "b",
|
||||
},
|
||||
has_sometype: false,
|
||||
};
|
||||
|
||||
describe.each(["run", "compile"] as const)("%s", mode => {
|
||||
const testFn = mode === "run" ? test.skip : test;
|
||||
|
||||
testFn("works", () => {
|
||||
const result =
|
||||
mode === "compile"
|
||||
? compileAndRun(dir, dir + "/main.ts")
|
||||
: Bun.spawnSync({
|
||||
cmd: [bunExe(), "main.ts"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toBe("");
|
||||
expect(JSON.parse(result.stdout?.toString().trim())).toEqual(expected);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("check commonjs", () => {
|
||||
const dir = tempDirWithFiles("commonjs", {
|
||||
["main.ts"]: "const {my_value, my_type} = require('./a'); console.log(my_value, my_type);",
|
||||
["a.ts"]: "module.exports = require('./b');",
|
||||
["b.ts"]: "export const my_value = 'my_value'; export type my_type = 'my_type';",
|
||||
});
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "main.ts"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
expect(result.stderr?.toString().trim()).toBe("");
|
||||
expect(result.stdout?.toString().trim()).toBe("my_value undefined");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("check merge", () => {
|
||||
const dir = tempDirWithFiles("merge", {
|
||||
["main.ts"]: "import {value} from './a'; console.log(value);",
|
||||
["a.ts"]: "export * from './b'; export * from './c';",
|
||||
["b.ts"]: "export const value = 'b';",
|
||||
["c.ts"]: "export const value = 'c';",
|
||||
});
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "main.ts"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
expect(result.stderr?.toString().trim()).toInclude(
|
||||
"SyntaxError: Export named 'value' cannot be resolved due to ambiguous multiple bindings in module",
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
describe("export * from './module'", () => {
|
||||
for (const fmt of ["js", "ts"]) {
|
||||
describe(fmt, () => {
|
||||
const dir = tempDirWithFiles("export-star", {
|
||||
["main." + fmt]: "import {value} from './a'; console.log(value);",
|
||||
["a." + fmt]: "export * from './b';",
|
||||
["b." + fmt]: "export const value = 'b';",
|
||||
});
|
||||
for (const file of ["main." + fmt, "a." + fmt]) {
|
||||
test(file, () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), file],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
expect(result.stderr?.toString().trim()).toBe("");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("export * as ns from './module'", () => {
|
||||
for (const fmt of ["js", "ts"]) {
|
||||
describe(fmt, () => {
|
||||
const dir = tempDirWithFiles("export-star-as", {
|
||||
["main." + fmt]: "import {ns} from './a'; console.log(ns.value);",
|
||||
["a." + fmt]: "export * as ns from './b';",
|
||||
["b." + fmt]: "export const value = 'b';",
|
||||
});
|
||||
for (const file of ["main." + fmt, "a." + fmt]) {
|
||||
test(file, () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), file],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
expect(result.stderr?.toString().trim()).toBe("");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("export type {Type} from './module'", () => {
|
||||
for (const fmt of ["ts"]) {
|
||||
describe(fmt, () => {
|
||||
const dir = tempDirWithFiles("export-type", {
|
||||
["main." + fmt]: "import {Type} from './a'; const x: Type = 'test'; console.log(x);",
|
||||
["a." + fmt]: "export type {Type} from './b';",
|
||||
["b." + fmt]: "export type Type = string;",
|
||||
});
|
||||
for (const file of ["main." + fmt, "a." + fmt]) {
|
||||
test(file, () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), file],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
expect(result.stderr?.toString().trim()).toBe("");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("import only used in decorator (#8439)", () => {
|
||||
const dir = tempDirWithFiles("import-only-used-in-decorator", {
|
||||
["index.ts"]: /*js*/ `
|
||||
import { TestInterface } from "./interface.ts";
|
||||
|
||||
function Decorator(): PropertyDecorator {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
class TestClass {
|
||||
@Decorator()
|
||||
test?: TestInterface;
|
||||
}
|
||||
class OtherClass {
|
||||
other?: TestInterface;
|
||||
}
|
||||
|
||||
export {TestInterface};
|
||||
`,
|
||||
["interface.ts"]: "export interface TestInterface {};",
|
||||
"tsconfig.json": JSON.stringify({
|
||||
compilerOptions: {
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
describe.each(["run", "compile"] as const)("%s", mode => {
|
||||
const testFn = mode === "run" ? test.skip : test;
|
||||
|
||||
testFn("works", () => {
|
||||
const result =
|
||||
mode === "compile"
|
||||
? compileAndRun(dir, dir + "/index.ts")
|
||||
: Bun.spawnSync({
|
||||
cmd: [bunExe(), "index.ts"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
expect(result.stderr?.toString().trim()).toBe("");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user