Skip to content

loader: document the empty --loader extension, accept it in bunfig too - #36904

Closed
elibarzilay wants to merge 1 commit into
oven-sh:mainfrom
elibarzilay:loader-extensionless
Closed

elibarzilay wants to merge 1 commit into
oven-sh:mainfrom
elibarzilay:loader-extensionless

Conversation

@elibarzilay

Copy link
Copy Markdown

Mostly a documentation gap.

--loader already accepts an empty extension, mapping files that have none —
which matters because Bun reads extensionless files with the tsx loader, where
a generic arrow (<T>(x: T) => x) is an unclosed tag. colon_list_type
deliberately allows the empty key, but --help and the docs both imply an
extension is required, so the capability is undiscoverable and untested.

Briefly, how it threw me off: I concluded the mapping didn't exist at all — the
flag docs implied it, and the bunfig spelling failed silently rather than saying
anything.

Included here:

  • --help (both copies) and the bun run / bunfig docs pages mention the empty
    extension
  • bunfig [loader] accepts an empty key, matching the CLI. The zig parser's
    continue for that key also left an uninitialized entry in the loader map, so
    a bunfig containing one sent extensionless imports through the file loader
  • tests for both spellings, and for an extensionless file reached by an import

Repro of the bunfig behavior on 1.3.14:

$ cat bunfig.toml
[loader]
"" = "ts"
".foo" = "ts"
$ echo 'export const x = 1;' > mod
$ bun -e 'console.log(await import("./mod"))'
Module {
  __esModule: true,
  default: "/tmp/repro/mod",
}

I haven't built Bun locally: the four --loader tests pass against a released
1.3.14, while the bunfig test needs the parser change compiled, so CI is its
first real run.

The zig → rust migration is clearly in flight — this touches both copies of the
parser and of the help text. Please take it as a sketch of one way it could land
rather than a finished patch; happy to reshape it to whatever fits the
transition.

`--loader` takes an empty extension to map files that have none, but `--help`
and the docs imply an extension is required, and bunfig's `[loader]` drops such
a key instead of honoring it.

Document it in `--help` (rust and zig copies) and in the run/bunfig docs pages,
and accept an empty key in bunfig's `[loader]`, matching the CLI. In the zig
parser the `continue` that skipped the key also left an uninitialized entry in
the loader map, so a bunfig containing one sent extensionless imports through
the `file` loader with no error.

@claude claude Bot left a comment

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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Bun now accepts empty loader keys for files without extensions. The --loader :ts syntax and [loader] configuration are documented. Tests cover CLI, configuration-file, and imported extensionless files.

Changes

Extensionless loader mapping

Layer / File(s) Summary
Loader parsing and CLI documentation
src/bunfig/bunfig.rs, src/runtime/cli/bunfig.zig, src/runtime/cli/Arguments.*, docs/runtime/bunfig.mdx, docs/snippets/cli/run.mdx
Loader parsing retains empty keys. The CLI and documentation describe mapping extensionless files with :ts.
Extensionless loader tests
test/cli/run/run-extensionless.test.ts
Tests cover --loader, bunfig.toml, and imported extensionless files with TypeScript syntax.

Possibly related PRs

  • oven-sh/bun#36194: Both changes modify temporary-directory setup in the extensionless loader tests.

Suggested reviewers: robobun

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description explains what the PR does and includes rationale and test details, but does not explicitly state how verification was performed against the template's two required sections. Add a dedicated section titled 'How did you verify your code works?' that explicitly describes verification steps, even though testing details are mentioned in the description body.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: documenting and accepting empty --loader extensions in bunfig, which directly corresponds to the changeset across documentation and implementation files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch loader-extensionless

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/run/run-extensionless.test.ts`:
- Around line 43-50: Update all five subprocess tests to capture
proc.stdout.text() and proc.exited concurrently, then assert the expected stdout
and verify that the resolved exitCode equals 0. Apply this consistently to each
Bun.spawn invocation in the extensionless subprocess test suite.
- Line 3: Remove tmpdirSync from the import statement at line 3 and ensure
tempDir is imported from harness. Replace all manual calls to tmpdirSync()
throughout the test file (including the locations around lines 40-41 and 69-70)
with the tempDir helper to align with the test harness coding guidelines that
require using tempDir for temporary directory creation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d7984e8c-6b62-4a56-b0cf-058413359e8d

📥 Commits

Reviewing files that changed from the base of the PR and between b66764f and 425e83b.

📒 Files selected for processing (7)
  • docs/runtime/bunfig.mdx
  • docs/snippets/cli/run.mdx
  • src/bunfig/bunfig.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/Arguments.zig
  • src/runtime/cli/bunfig.zig
  • test/cli/run/run-extensionless.test.ts

import { describe, expect, test } from "bun:test";
import { mkdirSync, writeFileSync } from "fs";
import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness";
import { bunEnv, bunExe, isWindows, tempDirWithFiles, tmpdirSync } from "harness";

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
wc -l test/cli/run/run-extensionless.test.ts 2>/dev/null || true
sed -n '1,110p' test/cli/run/run-extensionless.test.ts 2>/dev/null || true

echo
echo "== harness exports for tempDir/tmpdirSync =="
rg -n "function tempDir|const tempDir|export .*tempDir|tmpdirSync" packages test -g '*.{ts,tsx,js,jsx,mjs,cjs}' | head -80

echo
echo "== imports/usages in target file =="
rg -n "tmpdirSync|tempDir|mkdirSync|exec|proc|exitCode|exited" test/cli/run/run-extensionless.test.ts

Repository: oven-sh/bun

Length of output: 11122


Use tempDir for the new temporary directories.

The new tests call tmpdirSync() and create directories manually. Use the tempDir helper from harness instead.

Proposed change
-import { bunEnv, bunExe, isWindows, tempDirWithFiles, tmpdirSync } from "harness";
+import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles, tmpdirSync } from "harness";

-    const dir = tmpdirSync();
-    mkdirSync(dir, { recursive: true });
+    using dir = tempDir("run-extensionless-cli", {});

-    const dir = tmpdirSync();
-    mkdirSync(dir, { recursive: true });
+    using dir = tempDir("run-extensionless-import", {});

As per coding guidelines, test files must use tempDir from harness and must not use tmpdirSync.

Also applies to: 40-41, 69-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/run/run-extensionless.test.ts` at line 3, Remove tmpdirSync from the
import statement at line 3 and ensure tempDir is imported from harness. Replace
all manual calls to tmpdirSync() throughout the test file (including the
locations around lines 40-41 and 69-70) with the tempDir helper to align with
the test harness coding guidelines that require using tempDir for temporary
directory creation.

Source: Coding guidelines

Comment on lines +43 to +50
await using proc = Bun.spawn({
cmd: [bunExe(), "--loader=:ts", join(dir, "./cool")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '^test/cli/run/run-extensionless\.test\.ts$' || true

echo "== file excerpt =="
if [ -f test/cli/run/run-extensionless.test.ts ]; then
  nl -ba test/cli/run/run-extensionless.test.ts | sed -n '1,120p'
fi

echo "== subprocess tests in file =="
rg -n 'Bun\.spawn|await proc\.exited|exitCode|stdout\.text|expect\(stdout\)' test/cli/run/run-extensionless.test.ts || true

echo "== git status/stat (context only) =="
git status --short || true
git diff --stat || true

Repository: oven-sh/bun

Length of output: 265


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== file excerpt =="
if [ -f test/cli/run/run-extensionless.test.ts ]; then
  awk '{ printf "%5d  %s\n", NR, $0 }' test/cli/run/run-extensionless.test.ts | sed -n '1,120p'
fi

echo "== subprocess patterns in file =="
grep -nE 'Bun\.spawn|await proc\.exited|exitCode|stdout\.text|expect\(stdout\)' test/cli/run/run-extensionless.test.ts || true

Repository: oven-sh/bun

Length of output: 4565


Assert the subprocess exit code in the new subprocess tests.

Each test reads only proc.stdout. Capture proc.stdout.text() and proc.exited concurrently, assert the output, then assert exitCode is 0. Apply this to all five subprocess tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/run/run-extensionless.test.ts` around lines 43 - 50, Update all five
subprocess tests to capture proc.stdout.text() and proc.exited concurrently,
then assert the expected stdout and verify that the resolved exitCode equals 0.
Apply this consistently to each Bun.spawn invocation in the extensionless
subprocess test suite.

Source: Coding guidelines

robobun added a commit that referenced this pull request Sep 8, 2026
…ntry point

`--loader :ts` and bunfig `"" = "ts"` must reach a file with no extension
that is run directly, not only one that is imported. `<T>(x: T) => x`
parses under the ts loader and not under tsx, so the test tells the two
apart. These are the entry-point tests from #36904.

Co-authored-by: Eli Barzilay <eli@barzilay.org>
@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Thank you for this, and for the clear write-up. The note on how the silent bunfig drop hid the feature was useful.

Since this was opened, the CLI and bunfig code finished moving from Zig to Rust. Arguments.zig and bunfig.zig are gone, so this branch cannot merge as it stands. Everything it intended is now in #41996, which also makes bun build and Bun.build treat a file with no extension as tsx, like the runtime does:

  • bunfig [loader] accepts the "" key (your bunfig.rs hunk, unchanged).
  • --help, the bun run flags page, and the bunfig docs describe the empty extension.
  • Your tests: the two entry-point ones (--loader=:ts and "" = "ts") are in test/cli/run/run-extensionless.test.ts on that branch, and the import case is covered next to them. All three pass there, and the bunfig one fails on 1.4.3 as you described.

You are credited as co-author on the commits that carry your changes (0f24262 and 5634b44). The uninitialized map entry you found in the Zig parser does not exist in the Rust one, which pushes to a Vec.

Closing this as superseded by #41996. If anything from here is missing there, please comment and I will pick it up.

@robobun robobun closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants