Skip to content

parser: honor the type attribute on export ... from statements - #38407

Closed
robobun wants to merge 1 commit into
mainfrom
farm/6bcb99f6/export-from-type-attribute
Closed

robobun wants to merge 1 commit into
mainfrom
farm/6bcb99f6/export-from-type-attribute

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • export { default as text } from "./data.bin" with { type: "text" }, export * as ns from ... with { type } and export * from ... with { type } ignore the attribute: the file is loaded with the loader its extension implies (file for .bin, so the re-export is a path string). import text from "./data.bin" with { type: "text" } honors it. Affects bun run, bun build, bun build --no-bundle and the dev server; with { type: "sqlite" } on a re-export bundles the database as a file asset and exports its path.
  • parse_path() (src/js_parser/parse/mod.rs:1396) stores the type attribute in ParsedPath.loader. Import statements copy it onto the import record (validate_and_set_import_type, src/js_parser/p.rs). The two export-from branches in src/js_parser/parse/parse_stmt.rs (export * at ~1248, export { .. } from at ~1303) never look at ParsedPath.loader: they only check ParsedPath.import_tag, which since d502df3 (Support import with { type: "json" } and others #16624, v1.2.5) is set for bunBakeGraph alone. Before that commit the attribute lived in import_tag and these branches rejected it; after it the attribute falls through.
  • The printer (src/js_printer/lib.rs) only prints the with { type } clause back out for S::Import, so even with the record fixed, bun run (which transpiles and lets JSC's module loader read the attribute) would still drop it on re-exports.

Fix

  • parse_stmt.rs: both export-from branches set the record's loader from ParsedPath.loader, through a new set_import_record_loader shared with import statements (p.rs). It also keeps the existing name validation for sqlite (default/db) and text/file (default); for export { a as b } from the name imported from the module is ClauseItem.original_name, for imports it is alias. The remaining import_tag checks only ever fire for bunBakeGraph, so their messages now say that instead of claiming type is not allowed.
  • js_printer: S::ExportStar and S::ExportFrom print the same with { type: "..." } clause as S::Import and record the same FetchParameters in the ModuleInfo export records. The three statement kinds now share print_import_record_type_attribute / module_info_fetch_parameters / import_attribute_type_name, which replace the two per-loader match blocks that were inlined in the S::Import branch (output is byte-identical for imports, including the minified form).
  • RuntimeTranspilerCache: version bump, since the printed output of re-exports changed.
  • Why honoring is right rather than restoring the old hard error: the import attributes spec allows attributes on re-exports, Node requires with { type: "json" } on JSON re-exports, and Bun has accepted export { default } from "./x.json" with { type: "json" } since v1.2.5; a hard error would break that code. Everything downstream of the parser is already per import record (the bundler resolves, parses and externalizes records by record.loader regardless of which statement created them, and converts export-from into imports before printing), so a re-export record with a loader behaves exactly like an import record with one.
  • Why the printer and ModuleInfo parts are needed: at runtime the transpiled source is what JSC analyzes, and the host loader picks the loader from the fetch parameters JSC passes for each module request, so the attribute has to survive printing on re-export statements too. Under bun test --isolate (and bytecode builds) the module record is built from ModuleInfo instead of JSC's own parse, so the export records need the matching fetch parameters; debug builds diff the two and reject the module if they disagree, which the --isolate test exercises.
  • Verified with (all fail on the released 1.4.0 / without the src change, pass with it):
    • test/js/bun/import-attributes/import-attributes.test.ts: runtime export { default as x } from, export * as ns from, export * from, sqlite default/db re-export, the two validation errors, and a bun test --isolate run covering the ModuleInfo path (the JSON-with-matching-attribute case passes both ways and guards the decision above).
    • test/bundler/bundler_loader.test.ts: bundled re-exports with text/json/js attributes for bun and node targets, and --no-bundle output keeping the clause on all three statement kinds.
    • test/bundler/bundler_bun.test.ts: embedded and external sqlite through a re-export (.db extension, since .sqlite already maps to the sqlite loader on its own).
    • test/bake/dev/bundle.test.ts: dev server re-export of an .html file with the text loader.
    • Also ran bundler_loader, bundler_bun, transpiler, import-defer, isolation, type-export, webkit-upgrade-3722912f, 30887 and the source-lint suites; cargo clippy on bun_js_parser / bun_js_printer is clean.

Background

  • Import attributes: the with { type: "..." } clause on import and export ... from statements (ES2025). Bun uses the type value to pick a loader for the target file instead of the one implied by its extension; Bun's parser records that choice as ImportRecord.loader on the statement's import record. (type: "macro" and bunBakeGraph are separate attributes handled by ParsedPath.is_macro / ParsedPath.import_tag.)
  • Import record: one entry per path referenced by a file (import, re-export, require, import()), holding the path, how it was referenced and the loader to use. The bundler resolves and parses files from these records, so a loader set on a record is honored no matter which statement produced it.
  • Runtime transpile: bun run transpiles each file and hands the printed source to JSC. JSC collects the attributes of every module request and passes them as fetch parameters to Bun's module loader (moduleLoaderFetch in ZigGlobalObject.cpp), which is where the runtime reads the type attribute. Anything the printer drops is therefore lost at runtime.
  • ModuleInfo: a description of a module's imports/exports/requested modules that the printer emits alongside the source. When it is present (bun test --isolate, bytecode builds) JSC builds the module record from it instead of re-analyzing the source, and each requested module and export entry carries fetch parameters that have to match what analyzing the printed source would give.

The type attribute used to be carried in ParsedPath.import_tag, which the
two export-from branches checked to reject it. Since it moved to
ParsedPath.loader those branches no longer saw it, so
`export { default as x } from "./a.bin" with { type: "text" }` (and the
export * forms) silently loaded the file with its extension's loader, both
at runtime and in the bundler.

Set the record's loader for export-from statements the same way import
statements do (validating the names imported from sqlite/text/file modules),
and print the attribute back out for export-from statements on bun targets,
including the ModuleInfo fetch parameters used by bun test --isolate and
bytecode builds, so the runtime module loader sees it too. The remaining
import_tag check only ever matched bunBakeGraph; its messages now say so.

Bumps the runtime transpiler cache version since the printed output of
re-exports changed.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 740d5c96-3a21-46e5-8bf9-a4d7ebe819af

📥 Commits

Reviewing files that changed from the base of the PR and between 01aa7cd and 8c590f8.

📒 Files selected for processing (8)
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_stmt.rs
  • src/js_printer/lib.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • test/bake/dev/bundle.test.ts
  • test/bundler/bundler_bun.test.ts
  • test/bundler/bundler_loader.test.ts
  • test/js/bun/import-attributes/import-attributes.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed, self-review in progress.

Reproduced on the released 1.4.0 and on main: with data.bin containing hello text, export { default as text } from "./data.bin" with { type: "text" } re-exports the path string (file loader) under both bun main.mjs and bun build --target bun, while the import form yields the contents. bun build --no-bundle --target bun also drops the with clause on re-exports and keeps it on imports.

Fail-before / pass-after (released binary vs this branch's debug build):

  • test/js/bun/import-attributes/import-attributes.test.ts: 7 of the 8 new tests fail before, all pass after (the JSON-with-matching-attribute case passes both ways by design).
  • test/bundler/bundler_loader.test.ts: 3 new cases fail before, pass after.
  • test/bundler/bundler_bun.test.ts: 2 new sqlite re-export cases fail before, pass after.
  • test/bake/dev/bundle.test.ts: the new dev-server case fails before, passes after.

@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.

I reviewed this PR and didn't find any bugs. Since it changes parser/printer semantics for every export ... from statement and the ModuleInfo fetch-parameters that JSC consumes, a human sign-off would still be worthwhile.

What was reviewed:

  • set_import_record_loader refactor is behavior-preserving for S::Import (same error strings, same alias/loc iteration); original_name is populated by parse_export_clause so the export-from validation checks the imported name, not the re-exported one.
  • Split ws! output in print_import_record_type_attribute is byte-identical to the old inline match in both normal and minified modes (_ws_minify just strips spaces).
  • import_record() returns &'a ImportRecord detached from &self, so holding record across the new &mut self printer/module_info calls is sound.
  • Moving add_import_record above the is_macro/import_tag checks in the export { } from branch doesn't change the type-only early-return path (still before the record is created).
Extended reasoning...

Overview

The PR makes export ... from "x" with { type: "..." } honor the type attribute the same way import ... from already does. It touches the JS parser (p.rs, parse_stmt.rs) to copy ParsedPath.loader onto the import record for both export-from forms and to share the name-validation logic with imports; the JS printer (lib.rs) to print the with { type } clause and record matching FetchParameters in ModuleInfo for S::ExportStar and S::ExportFrom; and bumps the RuntimeTranspilerCache version. Tests cover runtime, bundler (bun and node targets), --no-bundle, the dev server, and the bun test --isolate ModuleInfo path.

Security risks

None identified. The change routes an already-parsed attribute to the same loader machinery imports already use; no new untrusted-input parsing, no filesystem/network surface.

Level of scrutiny

High. The parser and printer sit on the hot path of every module Bun runs or bundles, and ModuleInfo feeds JSC's module record directly. The refactor also collapses two per-loader match blocks into shared helpers, so the byte-identity claim for existing S::Import output matters (verified: _ws_minify strips all spaces, so the split ws! calls produce the same normal and minified bytes). The design decision — honor rather than reject — is well-argued (spec allows it, Node requires it for JSON, Bun has silently accepted it since v1.2.5), but it is a decision a maintainer should ratify.

Other factors

Test coverage is unusually thorough: each of the three re-export forms is exercised at runtime, in the bundler for both targets, in transpile-only output, in the dev server, and under --isolate (which diffs ModuleInfo against JSC's own parse in debug builds). The two error messages that changed ("type" → "bunBakeGraph") are more accurate given import_tag is only ever set for that attribute since #16624. The add_import_record reorder in the export { } from branch was needed so the record index exists before set_import_record_loader is called; the type-only early return still precedes it, and the macro/bake-graph checks only add errors without returning, so no observable ordering change.

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:05 AM PT - Aug 14th, 2026

❌ @robobun, your commit 8c590f8 has 2 failures in Build #95639 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 38407

That installs a local version of the PR into your bun-38407 executable, so you can run:

bun-38407 --bun

@robobun

robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #40836, which carries the with { ... } clause on every import and re-export, derives the loader from the type attribute on export { } from, export * as ns from and export * from, and covers the case fixed here. Its tests include these cases.

Checked against a debug build of #40836: 12 of the 14 cases added here pass as written (the runtime re-exports, bun build for the bun and node targets, the --no-bundle output, the embedded and plain sqlite re-exports, the dev server re-export, and bun test --isolate). The sqlite re-export, --isolate and dev server cases are now part of #40836's tests.

The two remaining cases only check the parser message for a named re-export that a text or sqlite module does not provide, such as export { notDefault as text } from "./x" with { type: "text" }. With #40836 that import still fails, with JSC's export 'x' not found error, the same as the released bun today.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant