Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
340393592f fix(transpiler): hoist directives above JSX imports and preserve during minification
"use client" and "use server" directives were being placed after
auto-generated JSX runtime imports in --no-bundle mode, breaking
frameworks like Next.js that require directives before any other
expressions. Additionally, --minify-syntax was dropping all directives
entirely.

Fixes #6854

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 04:56:44 +00:00
3 changed files with 157 additions and 2 deletions

View File

@@ -1192,8 +1192,11 @@ pub fn Visit(
switch (stmt.data) {
.s_empty => continue,
// skip directives for now
.s_directive => continue,
// Directives like "use client" / "use server" must be preserved.
.s_directive => {
output.append(stmt) catch unreachable;
continue;
},
.s_local => |local| {
// Merge adjacent local statements

View File

@@ -6044,8 +6044,24 @@ pub fn printAst(
}
}
// Hoist directive prologue ("use client", "use server", etc.) before
// all other statements so that auto-generated imports (e.g. JSX runtime)
// don't push directives out of the prologue position.
// https://github.com/oven-sh/bun/issues/6854
for (tree.parts.slice()) |part| {
for (part.stmts) |stmt| {
if (stmt.data != .s_directive) continue;
try printer.printStmt(stmt, PrinterType.TopLevel.init(.yes));
if (printer.writer.getError()) {} else |err| {
return err;
}
printer.printSemicolonIfNeeded();
}
}
for (tree.parts.slice()) |part| {
for (part.stmts) |stmt| {
if (stmt.data == .s_directive) continue;
try printer.printStmt(stmt, PrinterType.TopLevel.init(.yes));
if (printer.writer.getError()) {} else |err| {
return err;
@@ -6308,8 +6324,24 @@ pub fn printCommonJS(
printer.binary_expression_stack = std.array_list.Managed(PrinterType.BinaryExpressionVisitor).init(bin_stack_heap.get());
defer printer.binary_expression_stack.clearAndFree();
// Hoist directive prologue ("use client", "use server", etc.) before
// all other statements so that auto-generated imports (e.g. JSX runtime)
// don't push directives out of the prologue position.
// https://github.com/oven-sh/bun/issues/6854
for (tree.parts.slice()) |part| {
for (part.stmts) |stmt| {
if (stmt.data != .s_directive) continue;
try printer.printStmt(stmt, PrinterType.TopLevel.init(.yes));
if (printer.writer.getError()) {} else |err| {
return err;
}
printer.printSemicolonIfNeeded();
}
}
for (tree.parts.slice()) |part| {
for (part.stmts) |stmt| {
if (stmt.data == .s_directive) continue;
try printer.printStmt(stmt, PrinterType.TopLevel.init(.yes));
if (printer.writer.getError()) {} else |err| {
return err;

View File

@@ -0,0 +1,120 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/6854
// "use client" / "use server" directives must be hoisted above auto-generated
// imports (e.g. JSX runtime) and must be preserved during minification.
test('"use client" appears before JSX runtime import in --no-bundle', async () => {
using dir = tempDir("issue-6854", {
"input.jsx": `"use client";\nexport function Button() { return <div>Click</div>; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "input.jsx"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toStartWith('"use client";\n');
expect(exitCode).toBe(0);
});
test('"use server" appears before JSX runtime import in --no-bundle', async () => {
using dir = tempDir("issue-6854", {
"input.jsx": `"use server";\nexport function action() { return <div/>; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "input.jsx"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toStartWith('"use server";\n');
expect(exitCode).toBe(0);
});
test('"use client" preserved with --minify', async () => {
using dir = tempDir("issue-6854", {
"input.js": `"use client";\nexport function Component() { return "hello"; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "--minify", "input.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toStartWith('"use client";');
expect(exitCode).toBe(0);
});
test('"use client" preserved with --minify and JSX', async () => {
using dir = tempDir("issue-6854", {
"input.jsx": `"use client";\nexport function Button() { return <div>Click</div>; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "--minify", "input.jsx"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toStartWith('"use client";');
expect(exitCode).toBe(0);
});
test('"use server" preserved with --minify', async () => {
using dir = tempDir("issue-6854", {
"input.js": `"use server";\nexport async function submitForm(data) { return data; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "--minify", "input.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toStartWith('"use server";');
expect(exitCode).toBe(0);
});
test("directive without JSX still works in --no-bundle", async () => {
using dir = tempDir("issue-6854", {
"input.js": `"use client";\nexport function Component() { return "hello"; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "input.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toStartWith('"use client";\n');
expect(exitCode).toBe(0);
});