Skip to content
21 changes: 15 additions & 6 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4438,13 +4438,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
&mut self,
loc: bun_ast::Loc,
) -> Result<js_ast::LocRef, bun_core::Error> {
// PORT NOTE: Zig `try p.source.path.name.nonUniqueNameString(arena)` allocates the
// sanitized identifier, then `allocPrint` formats `{s}_default`. bun_paths::fs::PathName<'static>
// exposes the same sanitizer as a Display formatter (`fmt_identifier()`), so format once
// and copy into the bump arena.
// Zig `createDefaultName` formats `{s}_default` from `nonUniqueNameString`, which is
// `MutableString.ensureValidIdentifier(nonUniqueNameStringBase())`. That sanitizer
// prepends `_` when the name starts with a non-identifier-start char (e.g. a digit from
// `1.ts`) per https://github.com/oven-sh/bun/issues/2946 — the non-allocating
// `fmt_identifier()` formatter does not, so it would emit an invalid identifier like
// `1_default` on the no-renamer (transpile / `bun run`) path.
let identifier: &'a [u8] = {
let s = format!("{}_default", self.source.path.name().fmt_identifier());
self.arena.alloc_slice_copy(s.as_bytes())
let base = self.source.path.name().non_unique_name_string_base();
let sanitized = bun_core::MutableString::ensure_valid_identifier(base)?;
Comment thread
robobun marked this conversation as resolved.
Outdated
const SUFFIX: &[u8] = b"_default";
let out = self
.arena
.alloc_slice_fill_copy(sanitized.len() + SUFFIX.len(), 0u8);
out[..sanitized.len()].copy_from_slice(&sanitized);
out[sanitized.len()..].copy_from_slice(SUFFIX);
out
};

let name = js_ast::LocRef {
Expand Down
73 changes: 73 additions & 0 deletions test/regression/issue/31401.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
import { join } from "node:path";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// https://github.com/oven-sh/bun/issues/31401
//
// An anonymous `export default function () {}` gets an auto-generated name
// derived from the module filename (`<name>_default`). When the filename
// starts with a digit (e.g. `1.ts`), the transpile / `bun run` path (which
// does not run the renamer) emitted `function 1_default()` — an invalid
// identifier that JSC's lexer rejected with
// "No identifiers allowed directly after numeric literal".
// The generated name must be sanitized up-front (→ `_1_default`).
describe.concurrent("issue 31401: anonymous default export from digit-named module", () => {
test("run a digit-named module with an anonymous default function", async () => {
using dir = tempDir("issue-31401-run", {
"1.ts": `export default function () {}\nconsole.log("ok");\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(String(dir), "1.ts")],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).not.toContain("No identifiers allowed directly after numeric literal");
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`"ok"`);
expect(exitCode).toBe(0);
});

test("import a digit-named module with an anonymous default function", async () => {
using dir = tempDir("issue-31401-import", {
"9mod.ts": `export default function () {}\n`,
"index.ts": `import f from "./9mod.ts";\nconsole.log(typeof f);\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(String(dir), "index.ts")],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).not.toContain("No identifiers allowed directly after numeric literal");
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`"function"`);
expect(exitCode).toBe(0);
});

test("transpile-only output uses a valid identifier for the generated default name", async () => {
using dir = tempDir("issue-31401-transpile", {
"1.ts": `export default function () {}\n`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", join(String(dir), "1.ts")],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).not.toContain("No identifiers allowed directly after numeric literal");
// Must be a valid identifier: the leading digit gets an underscore prefix.
expect(normalizeBunSnapshot(stdout)).toContain("export default function _1_default() {}");
expect(exitCode).toBe(0);
});
});
Loading