Skip to content

bun build --no-bundle: keep css url() references instead of failing to parse - #38528

Open
robobun wants to merge 1 commit into
mainfrom
farm/ca5ddaa0/no-bundle-css-url
Open

robobun wants to merge 1 commit into
mainfrom
farm/ca5ddaa0/no-bundle-css-url

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --no-bundle x.css exits 1 with error: Unexpected token: ./img.png parsing as soon as the stylesheet contains a url(), quoted, unquoted or data: alike. bun build x.css (bundled) works, and a --no-bundle stylesheet that only has @import works.
  • The transpile-only CSS path is Transpiler::build_css_output (src/bundler/transpiler.rs). It called StyleSheet::parse with no import record list. bun_css stores every url() as an index into that list, and when there is no list Parser::add_import_record (src/css/css_parser.rs) returns an unexpected-token error for the url, which fails the whole parse.
  • This has never worked: the Zig version of this function passed null as well.

Fix

  • build_css_output collects the records into a local Vec<ImportRecord> while parsing and passes ImportInfo::init_outside_of_bundler(&records) to to_css.
  • Why this is the right output: nothing on the --no-bundle path resolves anything, so the records keep an invalid source_index, and for such records the printer's get_import_record_url returns record.path.text, the url exactly as written. That is the behavior @import already has on this path, and it is the same parse/print pairing the in-tree CSS test binding uses (src/css_jsc/css_internals.rs).
  • Verified with the new bun build --no-bundle keeps css url() references as written block in test/bundler/cli.test.ts (9 cases: quoted, unquoted and data: urls, a #fragment, @font-face src, a custom property, image-set(), a file that also has @import, and --minify). All 9 fail on bun 1.4.0 with the error above and pass with this change; the rest of cli.test.ts still passes.
  • Not covered here: --no-bundle on a .module.css file still panics in Printer::lookup_symbol. That happens with or without a url() and is a separate problem in the same function.

Background

  • Import record: bun_ast::ImportRecord, one entry per thing a file references (the path as written, the kind, and after resolution a source_index). The CSS parser creates one per url(), and CSS values such as Url keep only the record index, so the printer needs the same list to print them back.
  • ImportInfo (src/css/printer.rs) is what the CSS printer reads urls from. Inside the bundler it also carries the tables used to rewrite records that resolved to an inlined data: URL or a copied asset. init_outside_of_bundler leaves those tables empty; they are only consulted for records with a valid source_index, which this path never produces.
  • --no-bundle (transform_only) makes bun build transpile each entry point on its own through Transpiler::transform instead of BundleV2, leaving imports for whoever consumes the output to resolve.

…o parse

The transpile-only CSS path parsed without an import record list, so the
parser rejected every url() token with "Unexpected token". Collect the
records during parse and hand them to the printer so url() values are
printed back as written, the same way @import already is.
@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: 2 minutes

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: 5902f0e0-59ea-4bcd-8f85-48e432fa707b

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and 35774fb.

📒 Files selected for processing (2)
  • src/bundler/transpiler.rs
  • test/bundler/cli.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced on bun 1.4.0 and current main with a one-line stylesheet:

printf '.x { background: url("./img.png") }\n' > e.css
bun build --no-bundle e.css
# error: Unexpected token: ./img.png parsing   (exit 1)

Same result for unquoted and data: urls; the bundled bun build e.css works. The 9 cases added to test/bundler/cli.test.ts all fail this way on 1.4.0 and pass with this branch.

Fix is in this PR (#38528): build_css_output now collects the import records while parsing and passes them to the printer, so url() values print back as written.

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

LGTM — small, well-scoped fix that wires the CSS url() import-record list through the --no-bundle transpile path.

What was reviewed:

  • build_css_output now passes a local Vec<ImportRecord> to StyleSheet::parse and ImportInfo::init_outside_of_bundler(&records) to to_css, matching the established pattern in src/css_jsc/css_internals.rs.
  • Confirmed get_import_record_url falls through to record.path.text when source_index is invalid, so URLs round-trip as written; the empty ast_urls_for_css/ast_unique_key_for_additional_file slices are only indexed inside the is_valid() branch.
  • The 9 new tests cover quoted/unquoted/data:/fragment/@font-face/custom-property/image-set()/@import-alongside/--minify, use tempDir/bunEnv, drain pipes concurrently, and assert exact output before exit code.
Extended reasoning...

Overview

Two files: a ~6-line functional change in src/bundler/transpiler.rs::build_css_output (the --no-bundle CSS path) plus a 9-case describe.concurrent block in test/bundler/cli.test.ts. Before, StyleSheet::parse was called with None for the import-record list, so Parser::add_import_record returned an unexpected-token error on the first url(). Now a local Vec<ImportRecord> collects the records and is handed to the printer via ImportInfo::init_outside_of_bundler, exactly as src/css_jsc/css_internals.rs already does for the internal CSS test binding.

Security risks

None. This is a parse/print plumbing fix inside the offline --no-bundle transpiler path — no I/O, network, auth, or filesystem writes are introduced. The URL strings are round-tripped verbatim from the input stylesheet; nothing is resolved or opened.

Level of scrutiny

Low-to-medium. The change is mechanical: replace two None arguments with the record list, mirroring an existing in-tree call site. I verified in src/css/printer.rs that get_import_record_url only indexes into the (empty) ast_urls_for_css / ast_unique_key_for_additional_file tables when record.source_index.is_valid(), which never holds here (parse is called with Index::INVALID and nothing resolves), so init_outside_of_bundler's empty tables are safe. The Vec and the source it borrows (entry.contents(), backed by the arena per the existing SAFETY comment at the call site) both remain in scope through to_css.

Other factors

Tests follow repo conventions well: describe.concurrent, tempDir with using, Bun.spawn with bunEnv/bunExe, Promise.all over stdout/stderr/exited, exact toBe on stdout, exit-code asserted last. The variant matrix is thorough for a bug that previously failed on any url(). The PR description is precise about mechanism and explicitly scopes out the pre-existing .module.css panic, which is unrelated to this change. No prior human review comments to address and no CODEOWNERS on these paths.

This branch has not been deployed

No deployments
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