Skip to content

bundler: resolve a percent-encoded CSS url() or @import by its decoded path - #46511

Open
robobun wants to merge 1 commit into
mainfrom
robobun/90031491/css-url-percent-decode
Open

robobun wants to merge 1 commit into
mainfrom
robobun/90031491/css-url-percent-decode

Conversation

@robobun

@robobun robobun commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun build fails on a CSS url() or @import that a browser loads: url(./my%20photo.png) beside my photo.png gives error: Could not resolve: "./my%20photo.png". No user reported this.
  • The resolver looks up the text as written (src/resolver/resolver.rs:1427). It decodes only a package exports target.

Fix

  • percent_decoded_css_url decodes the path. Both bundler resolve sites try it after the text as written fails, so a literal a%20b.png still wins. CSS importers only.
  • An escape stays as written when resolve or a browser reads its byte another way: %23, %3F, %25, %22, %2F, %5C, control bytes.
  • A watcher re-reads and watches the decoded directory.
  • Verified: test/bundler/esbuild/css.test.ts (16 tests, 11 fail on main), test/bake/dev/css.test.ts (4, all fail). Self-reviewed: 20 concerns raised, 18 addressed.

Background

  • The sites are resolve_import_records and run_resolver (records an onResolve plugin declines).
  • esbuild does not decode. Vite and Parcel decode first. Here the literal name is first, because it resolves today.
  • Considered a third try inside resolve_and_auto_install: it cannot tell a CSS importer from an HTML one, and every runtime import enters it.

Downsides

Notes

Repro (1.4.2 and main 491aaad, linux x64):

head -c 140000 /dev/zero | tr '\0' x > 'my photo.png'; mkdir fonts; cp 'my photo.png' 'fonts/Geist[wght].woff2'
printf x > 'small icon.svg'; printf 'b{color:red}\n' > 'my theme.css'
printf '@import "./my%%20theme.css";\na { background: url(./my%%20photo.png) }\n@font-face { font-family: G; src: url(fonts/Geist%%5Bwght%%5D.woff2) }\nc { mask: url("./small%%20icon.svg#m") }\n' > enc.css
bun build ./enc.css --outdir o
# error: Could not resolve: "./my%20theme.css"
# error: Could not resolve: "./my%20photo.png"
# error: Could not resolve: "fonts/Geist%5Bwght%5D.woff2". Maybe you need to "bun install"?
# error: Could not resolve: "./small%20icon.svg#m"

With this change the build writes url("./my photo-8j4pkn61.png"), url("./Geist[wght]-8j4pkn61.woff2") and url("data:image/svg+xml;base64,eA==#m").

Order of the tries. For a record of a CSS file:

  1. The text as written.
  2. The text without its ?query#fragment. Steps 1 and 2 are on main, inside the resolver.
  3. The path part with its escapes decoded.

Step 3 is a full second resolve. So --external, tsconfig paths, the browser map and a package without exports treat the decoded path as they treat the raw spelling. A build can pass with no file on disk when the decoded path is external (css/URLPercentEncodedExternal). The suffix is not put back for step 3. The printer appends it from the text as written.

What stays as written.

  • %2F and %5C, by the rule that ESModule::finalize applies to an exports target (INVALID_PERCENT_CHARS). The resolver reads \ as a separator on every platform.
  • A decoded #, ? or %. resolve cuts at # and ? when the name is not a file, and an exports map decodes % once more. A browser does the same with the raw output name.
  • A decoded ", control byte, trailing space, or byte sequence that is not UTF-8. The output is the raw file name. css: quote and escape the asset path spliced into url() #41557 escapes " and control bytes in the CSS string. The others need the output path to be percent-encoded, which no PR does yet for CSS.
  • A : in the first path segment, which reads as a scheme or a drive letter. So C:/dir/my%20photo.png stays as written on Windows.
  • A malformed escape (100%.png).

Dot segments are decoded: %2e%2e/ is the parent directory, as for a browser.

Dev server and --watch. Each pass tries the literal text and then the decoded path. If both miss, the directories of both spellings are dropped from the cache once, and the pass runs again. Then both spellings go to track_resolution_failure, so a file that is created later at the decoded path rebuilds the stylesheet. The failure log is now fetched after the retries, because log_for_resolution_failures marks the file stale in the dev server. A decoded hit drops no directory from the cache: 3 rebuilds of a stylesheet with 5 encoded URLs gave 0 drops, as for the raw spelling.

Loop result binding. Both loops now bind their result with let mut. With the old second binding (let mut resolve_result = resolve_result;), the new break made the compiler copy the 296-byte Result once more for each record that resolves. The numbers are below.

ENAMETOOLONG. 29 CJK letters are a 261-byte path segment when written as escapes. The probe of that text logged Cannot read directory "...": ENAMETOOLONG, and a logged error fails the build even when the decoded name resolves. Both read sites now treat ENAMETOOLONG like ENOENT. For import "./<300 a>.js", bun build printed that line and Could not resolve. It now prints Could not resolve only. require() of such a path gives MODULE_NOT_FOUND before and after.

Measurements. Release builds of main 491aaad without and with this diff, linux x64. Tools: gdb hit counts and single-step counts, llvm-size, llvm-nm, llvm-objdump. perf, strace, valgrind and bloaty are not installed here, so there is no heap-block count.

  • Binary: .text 107,392,716 to 107,394,764 bytes (+2,048). data and bss are equal. The stripped file is 107,566,816 bytes for both. The new function is 775 bytes. resolve_import_records +472, on_resolve +319, load_as_file +30, dir_info_cached_miss +5.
  • resolve_and_auto_install, resolve, resolve_with_framework, resolve_without_symlinks, check_relative_path and ESModule::finalize: same size, 0 instructions differ.
  • A stylesheet with 200 url() records that resolve, counted inside resolve_import_records: calls 2,803 to 2,803, instructions 72,234 to 73,436 (+6 per record, register reloads). With the old second binding: 3,003 calls, one more 296-byte memmove per record.
  • The same records through an onResolve plugin that declines, counted inside on_resolve: memmove calls 402 to 402. The compiler no longer inlines path_with_pretty_initialized there: +2 calls and 37 fewer instructions inside the function per record.
  • 1,000 unresolved JS imports, and 1,000 unresolved url() without %: resolver calls 1,001 to 1,001. percent_decoded_css_url runs once per failed record and returns before any allocation.
  • 100 encoded records against the raw spelling of the same files: resolver calls 101 to 201, file syscalls 151 to 251 (351 with a #suffix). Raw, literal-% and plain names: 101 calls and 151 syscalls before and after.
  • Arena: one allocation of the path length per failed CSS record that has a % (16, 16, 27 and 18 bytes for the repro).

Tests.

  • On a release build of main, 11 of the 16 new tests in css.test.ts fail. The other 5 pin what must not change: the refused escapes, composes, and HTML src/href. Each gate and each refusal was removed in a separate build, and at least one test failed each time. The same holds for the order of the tries at each site, the suffix, both ENAMETOOLONG sites, and the decoded cache drop and watch at each site.
  • css/URLPercentEncodedEveryByte (Linux) builds one file per byte with both hex cases of its escape. It pins the accepted set, and checks with new URL that a URL parser gives each accepted byte back.
  • css/URLPercentEncodedFileLoaderMinified pins today's unquoted output for a name with a space. css: quote and escape the asset path spliced into url() #41557 changes that expectation.
  • The two HTML tests pin that src="./my%20photo.png" keeps its error. bundler(html): decode character references and percent escapes in src/href URLs #41616 changes that expectation.
  • Also ran: all of test/bundler/esbuild/css.test.ts and test/bake/dev/{css,html,bundle,esm,hot}.test.ts, bundler_html, css-modules, bundler_plugin, test/js/bun/resolve/resolve.test.ts, resolve-error.test.ts, the long-path tests of bun-build-api.test.ts, the byte-search and comment-cop source lints, cargo clippy for bun_resolver and bun_bundler, and cargo check of both crates for x86_64-pc-windows-msvc and aarch64-apple-darwin.

Other open PRs on the same lines or rules. This PR does not depend on any of them.

Not fixed here.

  • In-memory files of Bun.build match by exact key. They do not have the ?query#fragment retry either.
  • Names that need %23, %3F, %25 or %22.
  • HTML src/href and composes ... from.

Self-review. 20 concerns raised, 18 addressed. Not taken:

  • Keep the second let mut resolve_result binding. Without the change each resolved record copies the Result once more.
  • Split the decode into a second function without the CSS gate, for HTML to share. It has one caller in this PR.

…d path

For a browser, `url(./my%20photo.png)` in a stylesheet names the file
`my photo.png`. The bundler looked up the text as written and the build
failed with "Could not resolve".

When a `url()` or `@import` of a CSS file does not resolve as written,
both bundler resolve sites now try the path with its percent escapes
decoded (`percent_decoded_css_url`). A file with the literal name still
wins. An escape stays as written when the resolver or a browser reads
its byte another way: `%2F`, `%5C`, a decoded `#`, `?`, `%`, `"`,
control byte or trailing space, bytes that are not UTF-8, and a `:` in
the first path segment. HTML `src`/`href` and `composes` are not
decoded.

Under a watcher, the directory of the decoded path is dropped from the
cache and watched like the directory of the literal text. The failure
log is fetched after the retries, because the dev server marks the file
stale when it hands out that log.

The resolver treats ENAMETOOLONG as a missing file when it reads a
directory. A long name written as escapes logged "Cannot read
directory", and that error failed the build.

The two resolve loops bind their result with `let mut` directly. With
the second binding, the new `break` added one copy of the Result for
each record that resolves.
@robobun

robobun commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

How I reproduced it, on 1.4.2 and on main (38afee6):

head -c 140000 /dev/zero | tr '\0' x > 'my photo.png'
printf 'a { background: url(./my%%20photo.png) }\n' > enc.css
bun build ./enc.css --outdir o
# error: Could not resolve: "./my%20photo.png"

With this branch the build writes url("./my photo-8j4pkn61.png"). The same url() under bun build --minify prints the unquoted url(./my photo-8j4pkn61.png), as url("./my photo.png") does on main (#41557).

@robobun

robobun commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:12 AM PT - Oct 22nd, 2026

⏳ @robobun, your commit 2613436 is still building in Build #130894, but has 1 failures so far (All Failures):

@coderabbitai

coderabbitai Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 38afee6 and 2613436.

📒 Files selected for processing: 6
  • src/bundler/bundle_v2.rs
  • src/resolver/lib.rs
  • src/resolver/package_json.rs
  • src/resolver/resolver.rs
  • test/bake/dev/css.test.ts
  • test/bundler/esbuild/css.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Walkthrough

The resolver now decodes eligible percent-encoded CSS URL paths after literal-path resolution fails. The bundler retries resolution and updates failure tracking for both spellings. New tests cover bundling, output, platform-specific cases, and development-server reloads.

Changes

CSS URL resolution

Layer / File(s) Summary
CSS URL decoding and resolver support
src/resolver/resolver.rs, src/resolver/package_json.rs, src/resolver/lib.rs
Adds guarded percent-decoding for selected CSS imports, exports the helper, and suppresses logging for ENAMETOOLONG during directory traversal and file loading.
Bundler retry and failure tracking
src/bundler/bundle_v2.rs
Both bundler resolution paths retry decoded CSS URLs after ModuleNotFound. If retries fail, cache busting and development-server failure tracking consider both spellings.
Bundler URL resolution coverage
test/bundler/esbuild/css.test.ts
Adds API, CLI, and plugin-resolution tests for decoded CSS paths, output rewriting, external matching, and failure cases across path and platform constraints.
Development-server reload coverage
test/bake/dev/css.test.ts
Adds tests for encoded CSS assets during hot reload, missing-asset recovery, imported stylesheets, and plugins that decline URL resolution.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 26134

The decoded CSS URL fallback has no identified merge-blocking issue; merge after normal checks complete.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: resolving percent-encoded CSS url() and @import paths by their decoded path.
Description check Passed The description provides the problem, implementation details, verification results, limitations, and testing scope. It does not use the template headings exactly, but it covers the required informatio…
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.
Full details: Description check

Explanation

The description provides the problem, implementation details, verification results, limitations, and testing scope. It does not use the template headings exactly, but it covers the required information in detail.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

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

Beyond the inline findings, I also checked the lifetime of the decoded slice in percent_decoded_css_url (it is allocated in self.graph.heap, which outlives the Result that may borrow it, and track_resolution_failure joins the specifier into its own path buffer rather than retaining the slice), and the relocated log_for_resolution_failures call in run_resolver (it is still reached on every error path that logs, since the only paths that skip it continue or break out of the loop before any error is recorded).

Extended reasoning...

The change adds a percent-decoding retry for CSS url()/@ import resolution in both bundler resolve loops, exports a new helper from the resolver crate, and widens two directory-read error branches to swallow ENAMETOOLONG; it touches no auth, crypto, or data-exposure surface, but it does turn a build failure into a passing build with changed output, and five inline findings cover the output, dev-server watch, and errno-swallowing consequences, so a human should weigh them.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/resolver/resolver.rs
Comment on lines +6956 to +6966
if strings::contains_any(decoded, b"#?%")
// The output is the raw file name, which cannot carry these to a browser.
|| strings::contains_char(decoded, b'"')
|| decoded.iter().any(u8::is_ascii_control)
|| !strings::is_valid_utf8(decoded)
|| strings::ends_with_char(decoded, b' ')
// A `:` here reads as a scheme or a drive letter.
|| strings::contains_char(first_segment, b':')
{
return None;
}

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.

🔴 Users running bun build --minify on url(./my%20photo.png) now get a passing build whose rule the browser drops, where the base failed with Could not resolve. The guard at src/resolver/resolver.rs:6958 refuses " and control bytes because the copied asset's name is substituted verbatim into the printed url(), yet accepts space, ', ( and ), which the unquoted minified form cannot carry either. Fix: make the emitted url() valid for every byte the decoder accepts in both quoted and unquoted output, e.g. escape at the asset substitution in src/bundler/Chunk.rs:781, or until then refuse those bytes here as " is. The PR calls this accepted; it turns a loud build error into silently broken production CSS. [also at: src/resolver/resolver.rs:6967 - Users bundling a CSS url() to a copied asset with --minify now get a build that passes but a rule the browser drops, where the base failed loudly.]

Why this was flagged

A stylesheet contains url(./my%20photo.png) (or any name whose decoded form has a space, ', ( or )) and the build runs with --minify/minifyWhitespace. percent_decoded_css_url at src/resolver/resolver.rs:6956-6966 accepts the decoded ./my photo.png. With minify, src/css/values/url.rs:110-140 prints the asset placeholder unquoted, and src/bundler/Chunk.rs:769-781 substitutes dest_path verbatim, so the output is url(./my photo-<hash>.png). Per CSS Syntax an unquoted url token with whitespace followed by a non-) is a bad-url token, so the declaration is dropped. On the base branch the same input fails at build time with Could not resolve: "./my%20photo.png", so the author sees it. The guard's own comment at src/resolver/resolver.rs:6957 says bytes the raw output name cannot carry are refused, but that criterion is applied only to ", control bytes and a trailing space, not to the bytes serialize_unquoted_url (src/css/css_parser.rs:5842-5844) must escape.

Verification: Acknowledged in the PR description's "Downsides" and a pinned test. percent_decoded_css_url (src/resolver/resolver.rs:6956-6966) accepts ./my photo.png; src/css/values/url.rs:110-124 prints the placeholder unquoted and src/bundler/Chunk.rs:769-781 substitutes dest_path verbatim, yielding url(./my photo-<hash>.png), a bad-url token the browser drops, where the base printed Could not resolve.

Comment thread src/bundler/bundle_v2.rs
Comment on lines +7004 to +7011
if let Some(decoded) = decoded
&& let Ok(result) = transpiler.resolver.resolve(
source_dir,
decoded,
import_record.kind,
)
{
break 'inner result;

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.

🔴 Users serving a bundled HTML page with Bun.serve whose CSS has url(./my%20photo.png) to a copied asset now get a 404 for it where the base failed the build. The decoded hit adopted at src/bundler/bundle_v2.rs:7011 (and :2796) names the output ./my photo-<hash>.png, so HTMLBundle.rs:584 registers the route /my photo-<hash>.png. The browser requests /my%20photo-<hash>.png and the uws router matches segments literally, so nothing answers. Fix: a decoded hit must produce an output name a browser can fetch back, e.g. percent-encode the copied asset's name in the emitted CSS and route, or refuse decoded names the server cannot route.

Why this was flagged

A stylesheet in a Bun.serve HTML bundle (production, Bun.serve({ routes: { '/': index.html } })) contains url(./my%20photo.png) and the file on disk is my photo.png of 128 KiB or more (COPY_THRESHOLD, src/bundler/bundled_ast.rs:374) or uses the file loader. The raw resolve fails with ModuleNotFound; percent_decoded_css_url returns the decoded name and the second resolve at src/bundler/bundle_v2.rs:7005-7011 succeeds, so the record's path becomes /dir/my photo.png and the copied output file is ./my photo-<hash>.png. src/runtime/server/HTMLBundle.rs:571-588 strips the . and calls append_static_route with /my photo-<hash>.png. The browser percent-encodes the space in the request path, /my%20photo-<hash>.png; packages/bun-uws/src/HttpContext.h:593 routes getUrlForRouting() unchanged and HttpRouter.h:255 compares segments byte-for-byte without decoding, so the static route never matches and the server falls to its 404/fallback. On the base the build itself failed with Could not resolve: "./my%20photo.png", so the user saw the problem at build time.

Verification: src/bundler/bundle_v2.rs:7005-7011 break 'inner result on the decoded second resolver.resolve(source_dir, decoded, ...), so the record now resolves to my photo.png where base raised Could not resolve. A browser resolving url("./my photo-<hash>.png") requests /my%20photo-<hash>.png, which does not byte-equal /my photo-<hash>.png, so no static route fires and the asset 404s.

Comment thread src/resolver/resolver.rs
Comment on lines +4580 to +4584
if !matches!(
err,
crate::Error::Sys(bun_errno::SystemErrno::ENOENT)
| crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)
) {

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.

🟡 (optional) Users of bun build and the dev server lose the Cannot read directory "...": ENAMETOOLONG diagnostic for every import kind, not only the CSS percent-decoded retry. Both directory-read sites (src/resolver/resolver.rs:4580-4584 and src/resolver/resolver.rs:5911-5913) now treat ENAMETOOLONG like ENOENT, so a JS import whose directory exceeds NAME_MAX or whose absolute path exceeds PATH_MAX reports only Could not resolve. Fix: keep the errno visible for the non-CSS case, for example by emitting it as a non-fatal note instead of dropping it, or by silencing it only when the probe comes from a CSS url()/@ import record that has a % escape; the decoded retry must still succeed without a logged error.

Why this was flagged

A JS file does import "./<300 a>.js" or lives in a tree whose absolute path is longer than PATH_MAX (such a file can exist on Linux). read_dir_info reaches the ENAMETOOLONG branch at src/resolver/resolver.rs:4580 or load_as_file reaches src/resolver/resolver.rs:5911, and with this diff both skip add_error_fmt for ENAMETOOLONG. The base branch prints Cannot read directory "<dir>": ENAMETOOLONG followed by Could not resolve; after merging only Could not resolve is printed, so an operator cannot tell a missing file from a path-length limit. The silencing is needed only so the raw probe of an escaped CSS name (29 CJK letters become a 261-byte segment) does not fail the build before the decoded retry at src/bundler/bundle_v2.rs:2790 and src/bundler/bundle_v2.rs:7003 succeeds, but the change applies to every kind and every caller of the resolver. The repository review guidance says to map only the specific expected errno to the benign path; nothing in the diff bounds this to CSS records.

Verification: The trigger is any importer whose specifier or resolved directory path makes openat/readdir return ENAMETOOLONG. src/resolver/resolver.rs:4580-4584 now reads if !matches!(err, Sys(ENOENT) | Sys(ENAMETOOLONG)), and src/resolver/resolver.rs:5911-5913 adds | Sys(ENAMETOOLONG) => {} to the silent arm. Neither site consults the import kind. The build still fails with Could not resolve; only the extra logged error is dropped.

Comment thread src/bundler/bundle_v2.rs
Comment on lines +2811 to +2820
);
}
if busted {
had_busted_dir_cache = true;
continue;
}
}

// Tell Bake's Dev Server to wait for the file to be imported.
dev.track_resolution_failure(
&import_record.source_file,
&import_record.specifier,
target.bake_graph(),
self.graph.input_files.items_loader()
[import_record.importer_source_index as usize],
)
.expect("oom");
for spelling in spellings {

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.

🟡 (optional) Dev-server users with a percent-encoded CSS url() whose file appears later keep an OS directory watch and a dead dependency entry for the rest of the session, where the base released both once the file appeared. Both spellings are tracked at src/bundler/bundle_v2.rs:2811-2820 (and bundle_v2.rs:7040-7048), but hot_reload_event.rs:140-152 frees only the dep that resolves and frees the watcher only when no dep remains; the raw ./my%20photo.png dep never resolves, so the watcher on that directory is never freed and the raw spelling is re-resolved on every event in that directory. Fix: when one spelling of a record resolves, free the sibling deps registered for the same (source file, record), e.g. track the decoded spelling only, or key deps by record so a hit removes both.

Why this was flagged

Trigger: Bake dev server, a stylesheet with url(./my%20photo.png) while my photo.png does not exist yet, then the user creates the file. Both spellings are passed to dev.track_resolution_failure at src/bundler/bundle_v2.rs:2811-2820 and src/bundler/bundle_v2.rs:7040-7048; directory_watch_store.rs:257-287 appends one Dep per spelling to the same directory watcher. On the create event, src/runtime/bake/dev_server/hot_reload_event.rs:129-147 resolves each Dep; only the decoded one resolves and is freed (line 147), the raw one is pushed back onto the chain (lines 149-151), so the watcher at line 155-161 is never freed. On the base branch there was only the raw dep, which stayed only while the import was genuinely unresolved; here the import now resolves but the directory watch and the stale dep persist for the session and every later change in that directory re-runs resolver.resolve on the raw spelling. Remedy: free sibling deps of the same record when any spelling resolves.

Verification: Trigger: Bake dev server, url(./my%20photo.png) missing at first build, my photo.png created later. On the directory event, only a dep whose resolve succeeds is freed (src/runtime/bake/dev_server/hot_reload_event.rs:147), every other dep is re-linked (lines 150-151). ./my%20photo.png never resolves once my photo.png exists, so that dep and the directory watch persist.

Comment thread src/bundler/bundle_v2.rs
Comment on lines +6998 to +7012
let decoded = _resolver::percent_decoded_css_url(
self.arena(),
loader,
import_record.path.text,
import_record.kind,
);
if let Some(decoded) = decoded
&& let Ok(result) = transpiler.resolver.resolve(
source_dir,
decoded,
import_record.kind,
)
{
break 'inner result;
}

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.

🟣 pre-existing, not blocking: pre-existing (partial fix): a Bun.build({ files }) user whose in-memory CSS references url(./my%20photo.png) beside an in-memory my photo.png still gets Could not resolve, while the same layout on disk now builds. Both resolve sites consult file_map.resolve only for the raw spelling (src/bundler/bundle_v2.rs:6926, src/bundler/bundle_v2.rs:2696) and the decoded retry at src/bundler/bundle_v2.rs:7005 goes to the disk resolver only. Fix: try the decoded spelling against the FileMap before (or alongside) the disk resolver at both sites, ideally inside one shared retry helper so the two loops stay in lockstep. [also at: src/bundler/bundle_v2.rs:7003 - pre-existing, partial fix: users of Bun.build({ files }) whose in-memory CSS says url(./my%20photo.png) still get Could not resolve after this change.]
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

A JS API build passes files: { "/app/entry.css": ".a{background:url(./my%20photo.png)}", "/app/my photo.png": <bytes> } with /app/entry.css as entrypoint. In resolve_import_records the FileMap lookup at src/bundler/bundle_v2.rs:6926 is keyed by the raw text ./my%20photo.png and misses; resolve_with_framework at src/bundler/bundle_v2.rs:6989 misses on disk; the new retry at src/bundler/bundle_v2.rs:6998-7012 decodes to ./my photo.png but only calls transpiler.resolver.resolve, which reads the real filesystem, so the virtual file is never consulted and the record falls through to the Could not resolve: "./my%20photo.png" error at src/bundler/bundle_v2.rs:7175. run_resolver has the same shape: FileMap at src/bundler/bundle_v2.rs:2696 for the raw text only, decoded retry at src/bundler/bundle_v2.rs:2782-2797 against the disk resolver only. The base branch fails this input the same way, so this is the sibling input source the fix leaves behind rather than a regression; the PR's variant matrix (api/cli/plugin-declines) does not include files.

Verification: pre-existing (partial fix: the base branch fails this by the same route). A Bun.build({ files: {...} }) build whose in-memory CSS references url(./my%20photo.png). FileMap::resolve (src/bundler/bundle_v2.rs:968-1060) never percent-decodes. The decoded retry at lines 6998-7012 calls only the disk resolver, so the build logs Could not resolve. None of the new tests exercise it.

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