Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions docs/runtime/bunfig.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Configure how Bun maps file extensions to loaders. This is useful for loading fi
[loader]
# when a .bagel file is imported, treat it like a tsx file
".bagel" = "tsx"
# an empty extension maps files that have none, which Bun otherwise treats as tsx
"" = "ts"
```

Bun supports the following loaders:
Expand Down
3 changes: 2 additions & 1 deletion docs/snippets/cli/run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ bun run <file or script>
</ParamField>

<ParamField path="--loader" type="string">
Parse files with <code>.ext:loader</code>, e.g. <code>--loader .js:jsx</code>. Valid loaders: <code>js</code>,{" "}
Parse files with <code>.ext:loader</code>, e.g. <code>--loader .js:jsx</code>. An empty extension maps
files that have none, e.g. <code>--loader :ts</code>. Valid loaders: <code>js</code>,{" "}
<code>jsx</code>, <code>ts</code>, <code>tsx</code>, <code>json</code>, <code>toml</code>, <code>text</code>,{" "}
<code>file</code>, <code>wasm</code>, <code>napi</code>. Alias: <code>-l</code>
</ParamField>
Expand Down
6 changes: 2 additions & 4 deletions src/bunfig/bunfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,10 +1065,8 @@ impl<'a> Parser<'a> {
let key = key_expr
.as_string(self.bump)
.expect("infallible: type checked");
if key.is_empty() {
continue;
}
if key[0] != b'.' {
// An empty key maps files with no extension, as `--loader :ts` does.
if !key.is_empty() && key[0] != b'.' {
self.add_error(
key_expr.loc,
b"file extension for loader must start with a '.'",
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub(crate) const TRANSPILER_PARAMS_: &[ParamType] = &[
"--feature <STR>... Enable a feature flag for dead-code elimination, e.g. --feature=SUPER_SECRET"
),
parse_param!(
"-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi"
"-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx, or :ts for extensionless files. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi"
),
parse_param!(
"--no-macros Disable macros from being executed in the bundler, transpiler and runtime"
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/cli/Arguments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub const transpiler_params_ = [_]ParamType{
clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:\"development\". Values are parsed as JSON.") catch unreachable,
clap.parseParam("--drop <STR>... Remove function calls, e.g. --drop=console removes all console.* calls.") catch unreachable,
clap.parseParam("--feature <STR>... Enable a feature flag for dead-code elimination, e.g. --feature=SUPER_SECRET") catch unreachable,
clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi") catch unreachable,
clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx, or :ts for extensionless files. Valid loaders: js, jsx, ts, tsx, json, toml, text, file, wasm, napi") catch unreachable,
clap.parseParam("--no-macros Disable macros from being executed in the bundler, transpiler and runtime") catch unreachable,
clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable,
clap.parseParam("--jsx-fragment <STR> Changes the function called when compiling JSX fragments") catch unreachable,
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/cli/bunfig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1188,8 +1188,8 @@ pub const Bunfig = struct {

for (properties, 0..) |item, i| {
const key = item.key.?.asString(allocator).?;
if (key.len == 0) continue;
if (key[0] != '.') {
// An empty key maps files with no extension, as `--loader :ts` does.
if (key.len > 0 and key[0] != '.') {
try this.addError(item.key.?.loc, "file extension for loader must start with a '.'");
}
var value = item.value.?;
Expand Down
49 changes: 48 additions & 1 deletion test/cli/run/run-extensionless.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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

import { join } from "path";

describe.concurrent("run-extensionless", () => {
Expand Down Expand Up @@ -32,4 +32,51 @@ describe.concurrent("run-extensionless", () => {
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});

// `--loader` takes an empty extension, which maps files that have none. `<T>(x: T) => x`
// is a generic arrow to the ts loader and an unclosed tag to the tsx loader, so it parses
// only when the mapping took effect.
test("--loader with an empty extension sets the loader for extensionless files", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(join(dir, "cool"), "const id = <T>(x: T) => x; console.log(id('hello world'));");
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");
Comment on lines +43 to +50

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

});

test("[loader] with an empty extension sets the loader for extensionless files", async () => {
const dir = tempDirWithFiles("bunfig-loader-extensionless", {
cool: "const id = <T>(x: T) => x; console.log(id('hello world'));",
"bunfig.toml": '[loader]\n"" = "ts"\n',
});
await using proc = Bun.spawn({
cmd: [bunExe(), join(dir, "./cool")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});

test("--loader with an empty extension applies to imported extensionless files", async () => {
const dir = tmpdirSync();
mkdirSync(dir, { recursive: true });
await Bun.write(join(dir, "dep"), "export const id = <T>(x: T) => x;");
await Bun.write(join(dir, "main.ts"), "import { id } from './dep'; console.log(id('hello world'));");
await using proc = Bun.spawn({
cmd: [bunExe(), "--loader=:ts", join(dir, "./main.ts")],
cwd: dir,
env: bunEnv,
stdout: "pipe",
});
const stdout = await proc.stdout.text();
expect(stdout).toEqual("hello world\n");
});
});