Skip to content

bundler: stop rendering "././" output paths from naming templates that start with [dir] - #38366

Open
robobun wants to merge 1 commit into
mainfrom
farm/6e856a72/naming-template-dot-segments
Open

bundler: stop rendering "././" output paths from naming templates that start with [dir]#38366
robobun wants to merge 1 commit into
mainfrom
farm/6e856a72/naming-template-dot-segments

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A server build that imports an HTML file and uses a naming template starting with [dir] (bun build --entry-naming "[dir]/[name].[ext]", or Bun.build({ naming: { entry: "[dir]/[name].[ext]" } })) embeds "index":"././home.html" and "path":"././home-<hash>.js" in the HTML import manifest. The default templates give ./home.html. Same for --chunk-naming and --asset-naming, and static/[dir]/... gives ./static/./home.html.
  • The same string is reused everywhere the output path is exposed: metafile output keys ("././server.js"), the value appended to --public-path (https://cdn.example/./a-<hash>.js, because cheap_prefix_normalizer strips one ./), the bun build summary (./server.js), and the embedded file keys of --compile.
  • Cause: src/runtime/cli/Arguments.rs:2571 and src/runtime/api/JSBundler.rs:998 prefix user templates with ./, and path_template_print (src/bundler/options.rs) renders [dir] as . for a file at the root, so ./[dir]/[name].[ext] renders as ././a.js. The rendered bytes are stored verbatim as chunk.final_rel_path (src/bundler/linker_context/generateChunksInParallel.rs) and as the asset's dest_path (src/bundler/bundle_v2.rs, process_files_to_copy); HTMLImportManifest.rs and the other consumers copy them through.
  • Windows additionally had a #[cfg(windows)] block in generateChunksInParallel.rs that ran a full normalize whenever /./ appeared, which also drops the leading ./: the same build printed home.html on Windows, ././home.html on Linux, and ./home.html with the default templates.

Fix

  • PathTemplate::render renders the template and removes every . segment after the first one; both render sites (chunks and copied assets) go through it, and print becomes private so nothing can bypass it. The Windows-only block is deleted because a /./ can no longer reach it.
  • The first segment is kept on purpose: ./x is the shape every default template renders (./chunk-[hash].[ext], ./[name]-[hash].[ext], and [dir]/[name].[ext] at the root), and the existing manifest and metafile snapshots encode it. Custom templates now render the same shape on every platform.
  • Removing a . segment never changes which file is written (the OS resolves it the same way); it only changes the string handed to the manifest, metafile, public path join, summary and --compile keys, which now all agree with the defaults.
  • Fixing it at the render site rather than in the manifest writer is required for --compile: StandaloneModuleGraph.rs keys the embedded files by the same dest_path string the manifest prints, so the two must be derived from one path.
  • .., _.._, // and separators are left alone. --compile relies on a leading .. surviving, naming/WithPathTraversal relies on a literal .. being left to the filesystem, and asset paths on Windows still carry \ (that is a separate bug, bundler: emit posix-relative paths in the HTML-import manifest on Windows #34557).
  • Chunk hashes are unaffected (the content hash covers the template text, not the rendered path), so no existing snapshot changes.
  • Verified with:
    • test/bundler/html-import-manifest.test.ts (manifest-paths-with-dir-template-api / -cli): root and nested HTML imports, JS/CSS/HTML chunks and assets, [dir]/... and static/[dir]/... templates, through both Bun.build and the CLI flags. Fail on the release binary with ././index.html / ./static/./favicon-….svg, pass with bun bd test.
    • test/bundler/bundler_naming.test.ts (naming/DirTemplateAtRoot): metafile keys and the publicPath import specifier. Fails on the release binary with ././a.js, passes with bun bd test.
    • Rust unit test for the helper in options.rs, run with cargo miri test -p bun_bundler --lib (the bundler crate's unit tests cannot link outside miri).
    • bun bd test on bundler_naming, html-import-manifest, bun-serve-html-manifest, metafile, bundler_html, bundler_html_server, bundler_files, bundler_loader, bundler_edgecase, bun-build-api, bundler_splitting, bundler_compile_splitting, compile-asset-bunfs, standalone: all green.
    • Windows debug build: chunk paths now come out as ./home.html like the other platforms, .\.\icon-….png becomes .\icon-….png, and naming/EntryNamingTemplate1, naming/WithPathTraversal, naming/ImplicitOutbase2, naming/DirTemplateAtRoot pass without the removed block (itBundled tests have to be forced to run there, see test/bundler: stop silently dropping every itBundled test on Windows #34552).
  • bundler: emit posix-relative paths in the HTML-import manifest on Windows #34557 and bundler: fail the build instead of aborting when a naming template renders an output path that does not fit a path buffer #37502 touch the same two render sites (separators and a length check respectively); all three compose, whichever lands later needs a one-line rebase.

Background

  • Naming templates (--entry-naming, --chunk-naming, --asset-naming, Bun.build({ naming })) describe output paths with [dir], [name], [hash], [ext] placeholders. [dir] is the source file's directory relative to the project root; for a file at the root it is empty and is rendered as . so that [dir]/[name].[ext] gives ./a.js rather than /a.js.
  • final_rel_path (chunks) and dest_path (copied assets) are the outdir-relative output paths computed once per build after hashing. The HTML import manifest, the metafile, the import paths printed into other chunks, the build summary and the --compile file table all read them.
  • The HTML import manifest is the JSON object a server build embeds for import home from "./home.html": index is the HTML file's output path and files[] lists every output the page needs, each with its outdir-relative path. Bun.serve turns those paths into routes, and users read files[].path directly (for example to upload or precompress the files).
Repro, before and after
$ printf '<script type="module" src="./home.js"></script>' > home.html
$ echo 'console.log(1)' > home.js
$ printf 'import home from "./home.html";\nconsole.log(home.index, home.files.map(f => f.path));\n' > server.js
$ bun build server.js --target=bun --outdir out --entry-naming "[dir]/[name].[ext]" \
    --chunk-naming "[dir]/[name]-[hash].[ext]" --asset-naming "[dir]/[name]-[hash].[ext]" \
    --public-path https://cdn.example/ --metafile=out/meta.json && bun out/server.js

bun 1.4.0:

  ./server.js         0.58 KB    (entry point)
  ./home-e95jb7ta.js  27 bytes   (entry point)
  ./home.html         123 bytes  (entry point)
././home.html [ "././home-e95jb7ta.js", "././home.html" ]
metafile outputs: [ "././server.js", "././home-e95jb7ta.js", "././home.html" ]
home.html: <script ... src="https://cdn.example/./home-e95jb7ta.js">

This branch:

  server.js         0.58 KB    (entry point)
  home-e95jb7ta.js  27 bytes   (entry point)
  home.html         123 bytes  (entry point)
./home.html [ "./home-e95jb7ta.js", "./home.html" ]
metafile outputs: [ "./server.js", "./home-e95jb7ta.js", "./home.html" ]
home.html: <script ... src="https://cdn.example/home-e95jb7ta.js">

Same build with the default templates, on both versions: ./home.html [ "./home-…js", "./home.html" ].

The CLI and Bun.build prefix user naming templates with "./", and [dir]
renders as "." for a file at the root, so a template starting with [dir]
rendered "././a.js" (and "static/[dir]/..." rendered "./static/./a.js").
That raw string was stored as the chunk's final_rel_path and the asset's
dest_path, so it showed up in the HTML import manifest, the metafile,
public path joins and the build summary. Windows had a separate partial
fix that fully normalized such paths instead, dropping the leading "./"
and diverging from the other platforms.

PathTemplate::render now renders chunk and asset paths and removes every
"." segment after the first one, so custom templates produce the same
"./a.js" shape as the default templates on every platform. The
Windows-only normalization is removed since the paths it matched can no
longer occur.
@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: 41 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: a73cdc3e-7dcc-428c-b5da-83d8157a3a01

📥 Commits

Reviewing files that changed from the base of the PR and between f7ad274 and f615195.

📒 Files selected for processing (5)
  • src/bundler/bundle_v2.rs
  • src/bundler/linker_context/generateChunksInParallel.rs
  • src/bundler/options.rs
  • test/bundler/bundler_naming.test.ts
  • test/bundler/html-import-manifest.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, waiting on CI.

Reproduced on bun 1.4.0 with bun build server.js --target=bun --outdir out --entry-naming "[dir]/[name].[ext]" on a project that imports home.html: the manifest prints ././home.html, the metafile keys and --public-path joins carry the same ././ / /./. With this branch the same build prints ./home.html, identical to the default templates.

Tests: test/bundler/html-import-manifest.test.ts (manifest-paths-with-dir-template-api, -cli) and test/bundler/bundler_naming.test.ts (naming/DirTemplateAtRoot) fail on the release binary and pass with the debug build.

@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 — targeted normalization of rendered naming-template paths, thoroughly tested.

What was reviewed:

  • remove_redundant_dot_segments: segment walk keeps first segment, drops later . segments; copy_within bounds and index_of_any (Option<usize>) arithmetic checked; ../dotfiles/// correctly preserved per unit test.
  • PathTemplate::print made module-private — grep confirms only render and the const-variant Display still call it; both external render sites (chunks in generateChunksInParallel.rs, assets in bundle_v2.rs) now go through render.
  • Removed #[cfg(windows)] normalize block: it only fired on /./ and additionally collapsed ../leading ./, so Windows diverged from POSIX; new path is platform-consistent and PR reports naming/WithPathTraversal still passes on Windows.
  • New tests cover CLI + API, root + nested, chunks + assets, [dir]/... and static/[dir]/..., plus metafile keys and publicPath specifiers.
Extended reasoning...

Overview

This PR fixes rendered bundler output paths that contained redundant . segments (././a.js, ./static/./a.js) when a naming template starts with or embeds [dir] and the source is at the project root. It adds a small in-place byte helper remove_redundant_dot_segments in src/bundler/options.rs, wraps the existing PathTemplate::print in a new render that applies the helper, makes print module-private so nothing bypasses it, and rewires the two render sites (generateChunksInParallel.rs for chunks, bundle_v2.rs for copied assets). A Windows-only normalize_buf block that fired on /./ is removed because render now handles . segments uniformly.

Security risks

None. The change is pure string normalization on paths the bundler itself constructed; it removes segments the OS already treats as no-ops, so which file is written is unchanged. .. segments are explicitly left alone (the existing write_sanitized_parent_dirs still guards outdir escape for disk output), and no user-controlled data reaches new sinks.

Level of scrutiny

Moderate. The rendered path feeds several user-visible surfaces (HTML import manifest, metafile keys, publicPath joins, --compile embedded keys, build summary), so a wrong normalization would be widely visible — but the transformation is narrow (drop non-leading . segments only) and cannot change filesystem resolution. The helper is ~25 lines of straightforward two-pointer byte copying with a unit test covering the exact shapes the templates produce plus adversarial cases (.., _.._, dotfiles, .well-known, Windows \\). I traced strings::index_of_any to confirm it returns Option<usize>, so read + i is well-typed, and grepped for other PathTemplate::print callers to confirm making it private breaks nothing.

Other factors

The removed Windows block was itself inconsistent: it ran a full posix normalize (dropping leading ./ and collapsing ..) but only when /./ appeared, so the same template rendered differently on Windows vs POSIX and differently on Windows depending on whether [dir] happened to be at the root. The new behavior is platform-uniform and matches what POSIX already did. The PR description documents verification on a Windows debug build (including naming/WithPathTraversal, which exercises literal .. in a template) and 14 existing bundler test suites. Three new integration tests cover both Bun.build and CLI entry points, root and nested sources, and both the [dir]/... and prefix/[dir]/... shapes; naming/DirTemplateAtRoot additionally asserts the metafile key and the publicPath-joined import specifier. Chunk hashes are unaffected because the content hash covers template text, not the rendered path. No outstanding reviewer comments.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:22 AM PT - Aug 14th, 2026

@robobun, your commit f615195502e8f85934cd7806a7605b6a3ba4ea67 passed in Build #95489! 🎉


🧪   To try this PR locally:

bunx bun-pr 38366

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

bun-38366 --bun

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