Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/js_printer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,10 @@ fn NewPrinter(
return printClauseItemAs(p, item, .@"export");
}

fn printExportFromClauseItem(p: *Printer, item: js_ast.ClauseItem) void {
return printClauseItemAs(p, item, .export_from);
}

fn printClauseItemAs(p: *Printer, item: js_ast.ClauseItem, comptime as: @Type(.enum_literal)) void {
const name = p.renamer.nameForSymbol(item.name.ref.?);

Expand Down Expand Up @@ -1955,6 +1959,23 @@ fn NewPrinter(
p.addSourceMapping(item.alias_loc);
p.printClauseAlias(item.alias);
}
} else if (comptime as == .export_from) {
// In `export { x } from 'mod'`, the "name" on the left of `as`
// refers to an export of the other module, not a local binding.
// It's stored as the raw source text on `item.original_name`
// (ECMAScript allows this to be a string literal like `"a b c"`)
// and the item's ref points to a synthesized intermediate symbol
// whose display name may be mangled by a minifier. We must print
// `original_name` via `printClauseAlias` so string literals stay
// quoted and mangling can't corrupt the foreign-module name.
const from_name = if (item.original_name.len > 0) item.original_name else name;
p.printClauseAlias(from_name);

if (!strings.eql(from_name, item.alias)) {
p.print(" as ");
p.addSourceMapping(item.alias_loc);
p.printClauseAlias(item.alias);
}
} else {
@compileError("Unknown as");
}
Expand Down Expand Up @@ -4216,7 +4237,7 @@ fn NewPrinter(
p.printNewline();
p.printIndent();
}
p.printExportClauseItem(item);
p.printExportFromClauseItem(item);
}

if (!s.is_single_line) {
Expand Down
128 changes: 128 additions & 0 deletions test/regression/issue/29242.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// https://github.com/oven-sh/bun/issues/29242
//
// The parser handles string-literal names in `export { ... } from 'mod'`
// clauses, but when transpiling without bundling the printer dropped the
// quotes around the local name, producing invalid syntax that JSC then
// rejected:
//
// export { "a b c" } from './b.mjs'; // input
// export { a b c } from './b.mjs'; // old output — SyntaxError
// export { "a b c" } from './b.mjs'; // fixed output
Comment on lines +2 to +10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Trim the top-of-file bug-context prose to keep regression tests concise.

Consider keeping just the issue URL and removing the long reproduction/output narrative block; this context already lives in the PR/issue and makes the test noisier to scan.

Based on learnings: In oven-sh/bun regression test files, do not suggest adding inline comments that explain bug context or issue number being tested; PR descriptions cover that context and test code is kept clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/29242.test.ts` around lines 2 - 10, Remove the long
explanatory prose at the top of the regression test and replace it with a
concise single-line reference to the issue (e.g., the issue URL or number);
specifically edit the header block in test/regression/issue/29242.test.ts that
contains the multi-line reproduction/output narrative so it only includes the
issue link or short identifier and no extra commentary, leaving the actual test
code (the export string-literal case) unchanged.

import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

Comment thread
robobun marked this conversation as resolved.
test.concurrent("re-export with string literal local name (export { 'x y z' } from 'mod')", async () => {
using dir = tempDir("issue-29242-bare", {
"a.mjs": `export { "a b c" } from './b.mjs';`,
"b.mjs": `const a = 1;\nexport { a as "a b c" };`,
"main.mjs": `import { "a b c" as a } from './a.mjs';\nconsole.log(a);`,
});

Comment thread
claude[bot] marked this conversation as resolved.
await using proc = Bun.spawn({
cmd: [bunExe(), "main.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).not.toContain("SyntaxError");
expect(stdout).toBe("1\n");
expect(exitCode).toBe(0);
});

test.concurrent("re-export aliasing from string literal to identifier", async () => {
using dir = tempDir("issue-29242-alias", {
"a.mjs": `export { "a b c" as a } from './b.mjs';`,
"b.mjs": `const a = 1;\nexport { a as "a b c" };`,
"main.mjs": `import { a } from './a.mjs';\nconsole.log(a);`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "main.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).not.toContain("SyntaxError");
expect(stdout).toBe("1\n");
expect(exitCode).toBe(0);
});

test.concurrent("re-export aliasing string literal to string literal", async () => {
using dir = tempDir("issue-29242-both", {
"a.mjs": `export { "a b c" as "x y z" } from './b.mjs';`,
"b.mjs": `const a = 1;\nexport { a as "a b c" };`,
"main.mjs": `import { "x y z" as a } from './a.mjs';\nconsole.log(a);`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "main.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).not.toContain("SyntaxError");
expect(stdout).toBe("1\n");
expect(exitCode).toBe(0);
});

test.concurrent.each([
[`export { "a b c" } from './mod';`, [`"a b c"`]],
[`export { "a b c" as a } from './mod';`, [`"a b c"`]],
[`export { "a b c" as "x y z" } from './mod';`, [`"a b c"`, `"x y z"`]],
[`export { plain, "a b c" as aliased } from './mod';`, [`"a b c"`, `plain`]],
])("transpiler preserves string literal names in export-from clauses: %s", async (source, mustContain) => {
// Direct test of the printer: transpile without bundling and confirm the
// quotes around the local names are preserved.
using dir = tempDir("issue-29242-printer", {
"input.ts": source,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "input.ts", "--target=bun", "--no-bundle"],
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).not.toContain("SyntaxError");
for (const frag of mustContain) {
expect(stdout).toContain(frag);
}
// No unquoted `a b c` or `x y z` anywhere.
expect(stdout).not.toMatch(/(^|[^"])a b c(?!")/);
expect(stdout).not.toMatch(/(^|[^"])x y z(?!")/);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
expect(exitCode).toBe(0);
});

test.concurrent("transpiler preserves string literal names under --minify-identifiers", async () => {
// Regression for a subtlety: the export-from clause's left-side symbol
// is a synthesized intermediate that a minifier may rename. Printing
// `original_name` (the raw source text) keeps re-exports correct.
using dir = tempDir("issue-29242-minify", {
"input.ts": [
`export { "a b c" as aliased } from './mod';`,
`export { foo as bar } from './mod';`,
].join("\n"),
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "input.ts", "--target=bun", "--no-bundle", "--minify-identifiers"],
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).not.toContain("SyntaxError");
expect(stdout).toContain(`"a b c" as aliased`);
expect(stdout).toContain(`foo as bar`);
expect(stdout).not.toMatch(/(^|[^"])a b c(?!")/);
expect(exitCode).toBe(0);
});
Loading