Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
c2a432c9f5 Address code review: remove boolean field, use sentinel value instead
Changes based on code review feedback:

1. Removed `is_at_module_scope` boolean field from lexer
   - Instead, use `fn_or_arrow_start_loc` as a sentinel
   - When at module scope, set it to `await_keyword_loc`
   - Compare locations to detect module scope: `fn_or_arrow_start_loc.start == await_keyword_loc.start`
   - This avoids adding persistent state to the lexer

2. Fixed test for async function in CJS format
   - Changed from `main().catch(console.error)` to IIFE: `(async function main() { ... })().catch(console.error)`
   - This ensures the promise is created and executed immediately
   - Prevents the CJS process from exiting before async work completes

The sentinel approach is cleaner as it reuses existing fields rather than
adding new state. The lexer already tracks both `await_keyword_loc` and
`fn_or_arrow_start_loc`, so we can encode the module scope information by
setting them to the same value when appropriate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 02:36:42 +00:00
Claude Bot
b42cde31b1 Fix transpiler tests: properly detect module scope for await errors
The previous implementation incorrectly used `fn_or_arrow_data_parse.is_top_level`
to determine if await was at module scope. This flag indicates whether top-level
await is *enabled* (true for ESM, false for CJS/IIFE), not whether we're actually
at module scope.

This caused two issues:
1. In CJS/IIFE builds, top-level await showed the wrong error message
2. In transpiler tests, await in arrow functions was incorrectly treated as top-level

The fix:
- Added `is_at_module_scope` field to the lexer to track actual scope position
- Set it by comparing `current_scope == module_scope` in the parser
- Updated error logic to handle three cases:
  - Module scope: "Top-level await can only be used when output format is ESM"
  - Non-async function (with location): old error + note about adding async
  - Arrow function (no location): old error without note

All tests now pass including:
- test/bundler/bundler_top_level_await_error.test.ts
- test/bundler/transpiler/transpiler.test.js (await error tests)
- test/bundler/bundler_edgecase.test.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 01:31:42 +00:00
Claude Bot
871c543a21 Fix test: handle promise from main() call
Added .catch() handler to the main() invocation to ensure the
promise is properly handled and the async work completes before
the process exits.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 22:55:57 +00:00
Claude Bot
1dd972b388 Improve error message for top-level await in non-ESM output formats
When using `bun build --format=cjs` (or --format=iife) with top-level
await, the error message was misleading:

Before:
  error: "await" can only be used inside an "async" function

After:
  error: Top-level await can only be used when output format is ESM

The lexer now detects when await is used at the module scope (when
fn_or_arrow_start_loc is empty) and provides a more accurate error
message that directs users to use --format=esm.

Added comprehensive tests to verify the improved error message works
correctly for CJS and IIFE formats, while ESM format continues to
work as expected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 22:44:37 +00:00
3 changed files with 112 additions and 15 deletions

View File

@@ -136,7 +136,14 @@ pub fn ParsePrefix(
.allow_ident => {
p.lexer.prev_token_was_await_keyword = true;
p.lexer.await_keyword_loc = name_range.loc;
p.lexer.fn_or_arrow_start_loc = p.fn_or_arrow_data_parse.needs_async_loc;
// Use fn_or_arrow_start_loc as a signal:
// - If at module scope, set it to await_keyword_loc (sentinel)
// - Otherwise, set it to needs_async_loc (may be empty for arrow functions)
if (p.current_scope == p.module_scope) {
p.lexer.fn_or_arrow_start_loc = name_range.loc;
} else {
p.lexer.fn_or_arrow_start_loc = p.fn_or_arrow_data_parse.needs_async_loc;
}
},
}
},

View File

@@ -1820,8 +1820,23 @@ fn NewLexer_(
pub fn expectedString(self: *LexerType, text: string) !void {
if (self.prev_token_was_await_keyword) {
var notes: [1]logger.Data = undefined;
if (!self.fn_or_arrow_start_loc.isEmpty()) {
// Use fn_or_arrow_start_loc as a signal:
// - If it equals await_keyword_loc, we're at module scope (sentinel value)
// - If it's not empty and different, we're in a function with a location for "async"
// - If it's empty, we're in a function without a clear location (e.g., arrow function)
const is_at_module_scope = self.fn_or_arrow_start_loc.start == self.await_keyword_loc.start;
if (is_at_module_scope) {
// Top-level await - provide better error message
try self.addRangeErrorWithNotes(
self.range(),
"Top-level await can only be used when output format is ESM",
.{},
&.{},
);
} else if (!self.fn_or_arrow_start_loc.isEmpty()) {
// Inside a non-async function with a clear location for adding "async"
var notes: [1]logger.Data = undefined;
notes[0] = logger.rangeData(
&self.source,
rangeOfIdentifier(
@@ -1830,19 +1845,22 @@ fn NewLexer_(
),
"Consider adding the \"async\" keyword here",
);
try self.addRangeErrorWithNotes(
self.range(),
"\"await\" can only be used inside an \"async\" function",
.{},
&notes,
);
} else {
// Inside a function (e.g., arrow function) but no clear location for "async"
try self.addRangeErrorWithNotes(
self.range(),
"\"await\" can only be used inside an \"async\" function",
.{},
&.{},
);
}
const notes_ptr: []const logger.Data = notes[0..@as(
usize,
@intFromBool(!self.fn_or_arrow_start_loc.isEmpty()),
)];
try self.addRangeErrorWithNotes(
self.range(),
"\"await\" can only be used inside an \"async\" function",
.{},
notes_ptr,
);
return;
}
if (self.source.contents.len != self.start) {

View File

@@ -0,0 +1,72 @@
import { describe } from "bun:test";
import { itBundled } from "./expectBundled";
describe("bundler", () => {
itBundled("top_level_await_error/TopLevelAwaitInCJSFormat", {
files: {
"/entry.ts": /* ts */ `
async function sum(a: number, b: number) {
return a + b;
}
await sum(5, 5);
`,
},
format: "cjs",
bundleErrors: {
"/entry.ts": ["Top-level await can only be used when output format is ESM"],
},
});
itBundled("top_level_await_error/TopLevelAwaitInIIFEFormat", {
files: {
"/entry.ts": /* ts */ `
async function getData() {
return "data";
}
await getData();
`,
},
format: "iife",
bundleErrors: {
"/entry.ts": ["Top-level await can only be used when output format is ESM", 'Expected "=>" but found ";"'],
},
});
itBundled("top_level_await_error/TopLevelAwaitInESMFormatShouldWork", {
files: {
"/entry.ts": /* ts */ `
async function sum(a: number, b: number) {
return a + b;
}
const result = await sum(5, 5);
console.log(result);
`,
},
format: "esm",
run: {
stdout: "10",
},
});
itBundled("top_level_await_error/AwaitInAsyncFunctionShouldStillWork", {
files: {
"/entry.ts": /* ts */ `
(async function main() {
async function sum(a: number, b: number) {
return a + b;
}
const result = await sum(5, 5);
console.log(result);
})().catch(console.error);
`,
},
format: "cjs",
run: {
stdout: "10",
},
});
});