Skip to content

dev server: serve a route's stylesheets in source order - #46990

Open
robobun wants to merge 1 commit into
mainfrom
robobun/d5a2e8aa/dev-server-css-source-order
Open

robobun wants to merge 1 commit into
mainfrom
robobun/d5a2e8aa/dev-server-css-source-order

Conversation

@robobun

@robobun robobun commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes #28117

Problem

  • The dev server serves a page's stylesheets in reverse order (one two three becomes three two one), so the cascade breaks ties the other way from bun build.
  • Cause: process_edge_attachment (src/runtime/bake/dev_server/incremental_graph.rs:1049 on main) pushes each import edge on the front of a file's import list. trace_imports emits stylesheets in walk order.

Fix

  • A rebuild appends each edge through a tail pointer (NewImports), so the list is in import order.
  • Correct: trace_imports is the only reader that uses the order. Each stylesheet is linked at its first reference, as bun build does for an HTML route with local stylesheets.
  • Verified: new tests in test/bake/dev/css.test.ts and bundle.test.ts fail on 1.4.3-canary.1. test/bake/dev/ passes.
  • Self-reviewed: 16 concerns, wording and tests addressed. Not done: a tracking issue, a Tailwind test (Notes).

Background

Downsides

  • Order changes on load. A media="print" stylesheet linked last now wins on screen, as in bun build (Handling of media="print" incorrectly #18526).
  • Covers load and reload only: the client moves a hot-updated stylesheet behind the <link> stylesheets. With bun-plugin-tailwind that happens on each save. No issue tracks this.
  • Rebuild cost: +11 instructions per kept import, +12 per new import, 0 allocations. Release .text: +0 B.
Notes

Relation to #37845, #39488 and #30488. The Rust change is #37845, cut again on main. #37845 was closed into the consolidation PR #39488 (commit ab5c1811f0 there). #39488 is open and conflicts with main, so main still has the bug. The NewImports block is the text of that branch, so a later merge of main into #39488 gets no new conflict. git merge-file --diff3 reports 11 conflict hunks in incremental_graph.rs before this PR and 11 after. One line differs from #37845: a file that the walk already attached returns Stop, as main does since #46270. That is the guard against a circular @import. #30488 reported the <link> shape of this bug and was closed as a duplicate of #28117.

The reference is source order. The server now sends the stylesheets of a route depth first, each file at its first reference. For an HTML route with local stylesheets, bun build concatenates them in the same order (Measurements). Two differences from bun build stay:

  • bun build moves an external @import above each stylesheet of the page. The dev server links one chunk per stylesheet, so the external import stays in its stylesheet. For <link a.css><link b.css> where b.css imports an external x.css: bun build gives x a b, this PR a x b, main x b a.
  • A framework route lists the stylesheets of the page and then of each layout, each in import order. I did not compare that with a production build of a framework.

What else moves to source order. These read the same list through the same walk:

  • The module order in the initial client bundle of a route (/_bun/client/*.js) and the sources of its source map. Probe: index.ts imports a.ts, b.ts, c.ts, and a.ts imports a1.ts, a2.ts. This PR gives index.html index.ts a.ts a1.ts a2.ts b.ts c.ts. Main gives index.html index.ts c.ts b.ts a.ts a2.ts a1.ts. The bundle is an object with one key per module, so only the byte order changes.
  • The order of the failures in the error response of a route (ensure_route.rs:423).
  • List 2 of the hot update message (finalize.rs:959). The client reads it as a set.

Not fixed here.

  • The stylesheet order of an open page after a hot update. The owner is editCssContent (src/runtime/bake/client/css-reloader.ts:171). It puts each stylesheet that a hot update sends into document.adoptedStyleSheets. Those come after each <link> stylesheet in the cascade, until the page loads again. Enabled stylesheets in cascade order, read in the test client:

    Page, then action This PR: on load after Main: on load after
    links a, b, c, then a save of a.css a b c b c a c b a c b a
    links a, b, c that each import p.css, then a save of p.css a b c c b a c b a c b a
    a serve plugin named tailwind-noop, links tailwind.css, app.css, then a save of a script tailwind app app tailwind app tailwind app tailwind

    When the name of a serve plugin contains tailwind, each rebuild bundles the Tailwind stylesheet again and sends it (hot_reload_event.rs:208). So each save of any file is a hot update of that stylesheet. A page with the Tailwind stylesheet before another stylesheet is in source order from load to the first save. On main that page is in reverse order all the time. A page with the Tailwind stylesheet last no longer changes on a save. The same applies to HTMLBundle is inconsistent with the css content of Bun.build #28117 with hmr: true: after a save of foo.css, bar.css comes first again until a reload. The fix belongs in the client: keep a stylesheet at the position of its <link>. No issue tracks this yet.

  • With framework: "react", a client navigation appends the <link> of a new stylesheet at the end of <head> (bun-framework-react/client.tsx:226). The order then depends on the navigation history. Main does the same.

  • meta.styles of a framework route is cached for each route, and not each rebuild clears the cache (finalize.rs:928-950). For example, a swap of two imports in a route with no client component keeps the old list. dev server: refresh a framework route's meta.styles when its imports change #42849 has a fix. The framework tests read the rebuilt lists through a route that has no cached list.

  • A <link> loses its media and disabled attributes in the dev server and in bun build (Handling of media="print" incorrectly #18526, Inconsistent Bundling and Path Handling Between Development and Production in Bun #18809). For screen.css then print.css media="print", main links print screen, this PR links screen print, and bun build concatenates screen print. No tag has media, so the print rules now win on screen in the dev server too.

  • The comment at incremental_graph.rs:893 names check_edge_removal, which does not exist. The early return at :868 cannot run, because server_seen_bit_set is always empty. Both stay as they are.

Self-review. The review raised 16 concerns. Most ask for wording. The body now names source order as the reference and the two differences from bun build. It also names the media case and each action that reorders an open page. Two ask for tests. The framework test now runs with one server graph and with a separate SSR graph. It goes through a "use client" component and reads the lists after a rebuild. Not done:

  • Ten of the concerns ask for a tracking issue for the open page order. I did not open one. The section above has the mechanism, the owner and the probes for that issue.
  • A test with a tailwind plugin. On load that mode takes the same path as the HTML route test. The order after a save is what the mode changes, and that assertion belongs with the client fix.

Measurements. Main 449877f against this PR. Release means --profile=release (ThinLTO).

  • Served stylesheet order against bun build, six inputs: differs on 0 of 6 on this PR and on 5 of 6 on main. The inputs: three <link> tags. Links with a script between them, and the script has a nested import. A stylesheet that two scripts import. A stylesheet that the page links and a script imports. <link a><link b><link a>. import, require() and import() in one file. None of them has an external @import.
  • process_edge_attachment: 151 to 162 instructions for an import that stays (+11), 171 to 183 for a new import (+12). process_chunk_dependencies: +1 instruction per bundled file. Nothing per request. Method: qemu-user execution trace of the release build, one script with 500 stylesheet imports, one cold bundle and 20 rebuilds.
  • Allocations inside process_chunk_dependencies: 1,195 on main and 1,195 on this PR. The run is one cold bundle and 21 rebuilds of that script (gdb breakpoints on the mimalloc entry points). Edge (32 B) and EdgeLists (16 B) do not change. NewImports is 16 B of stack. The head pointer that it replaces was 8 B.
  • Release binary: .text is 82,164,617 B on both. The three list builder symbols change by +16, +32 and -11 B (llvm-size, llvm-nm).

Tests.

  • html route links its stylesheets in source order (css.test.ts). The page has links with a script between them, a nested import, and a repeated <link>. Two modules import one stylesheet, and one stylesheet is linked and imported. The test then rebuilds with no change, swaps two links, swaps two imports, and adds and removes imports. bun build gives the expected order for the files of each step. A reverse of the emitted list cannot pass. Main serves seven one four six three five two. The test expects one two three four five six seven.
  • framework route lists its stylesheets in import order (bundle.test.ts) runs once with one server graph and once with a separate SSR graph. It reads meta.styles of a route that imports stylesheets directly, through a module, and through a "use client" component. It then rewrites the component and the module (swap, add, remove) and reads a second route. It is in bundle.test.ts because that file already imports minimalFramework. The open PR test(bake): cover edits to a stylesheet that two CSS roots share #46957 edits the import line of css.test.ts.
  • All three fail on 1.4.3-canary.1+367d939d9 and on a debug build of main. They pass 10 of 10 runs on a debug build of this PR.
  • test/bake/dev/ (20 files) on a debug build of this PR: all pass. On a debug build of main the new tests fail. That run of main also had 6 timeouts at the 5 s default in production.test.ts and react-spa.test.ts. Those tests take 3 to 5 s on a debug build with ASAN. A later run of production.test.ts with the build of this PR had 9 such timeouts, so the timeouts do not come from this change.

The dev server rebuilds the import list of a file each time it bundles
the file. It pushed each edge on the front of the list, so the list was
in reverse import order. The route trace walks that list, and the
`<link>` tags of an HTML route and `meta.styles` of a framework route
follow the walk. A page got its stylesheets in reverse order, so the
cascade resolved ties the other way from the source.

The rebuild now appends each edge through a tail pointer. The list is in
import record order, so the trace reaches each stylesheet in source
order: depth first, each file at its first reference. For an HTML route
with local stylesheets, `bun build` concatenates them in that order.

This covers the order that the server sends. The client still moves a
stylesheet that a hot update replaces behind the `<link>` stylesheets
until the page loads again.

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
@robobun

robobun commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on a debug build of main (449877f) and on 1.4.3-canary.1+367d939d9. An HTML route that links one.css, two.css, three.css with development: true is served with its <link> tags as three two one. meta.styles of a framework route is reversed in the same way. The three new tests in test/bake/dev/css.test.ts and test/bake/dev/bundle.test.ts fail on both builds and pass on this branch.

PR: #46990

@robobun

robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:43 AM PT - Oct 26th, 2026

✅ @robobun, your commit bcd9ea7db5153c46d998d395321ed21ebd9d2784 passed in Build #133132! 🎉


🧪   To try this PR locally:

bunx bun-pr 46990

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

bun-46990 --bun

@coderabbitai

coderabbitai Bot commented Oct 26, 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 449877f and bcd9ea7.

Files selected for processing: 3
  • src/runtime/bake/dev_server/incremental_graph.rs
  • test/bake/dev/bundle.test.ts
  • test/bake/dev/css.test.ts

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

Walkthrough

The dev server now rebuilds import edges in source-record order. New tests check stylesheet ordering after rebuilds and in framework route metadata.

Changes

Import Order Preservation

Layer / File(s) Summary
Ordered import edge rebuild
src/runtime/bake/dev_server/incremental_graph.rs
Import edges are appended in record order, whether reused or newly allocated. The rebuilt list head is assigned to the file’s import list.
Stylesheet order validation
test/bake/dev/css.test.ts, test/bake/dev/bundle.test.ts
Tests check stylesheet order across no-op and changing rebuilds, duplicate references, and framework route metadata in both server-graph configurations.

Possibly related PRs

  • oven-sh/bun#46742: Preserves import records and updates dev-server barrel handling, which relates to how import ordering is represented and traversed.

Suggested reviewers: jarred-sumner

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Minimal · up to bcd9e

The stylesheet-order change is ready to merge after normal checks; no actionable merge-blocking issue remains.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title "dev server: serve a route's stylesheets in source order" matches the main change. The raw summary and PR objectives show that import edges are now appended in source-record order instead of…
Description check Passed The description follows the template. It has a "What does this PR do?" section, covered by the Problem, Fix, Background, and Downsides sections. It has a "How did you verify your code works?" section,…
Linked Issues check Passed Issue #28117 requires Bun.serve to preserve sequential stylesheet import order so the later stylesheet wins as in Bun.build. The PR changes rebuilt import lists from front insertion to tail append in …
Out of Scope Changes check Passed The changed Rust code directly implements the stylesheet import-order fix. The added tests cover HTML and framework routes and are supporting automated coverage for issue #28117. No unrelated product …
Full details: Title check

Explanation

The title "dev server: serve a route's stylesheets in source order" matches the main change. The raw summary and PR objectives show that import edges are now appended in source-record order instead of prepended, so route stylesheets are sent in import order. The title is specific and does not use vague terms. It omits the framework-route and HTML-route scope, but it still names the primary change.

Full details: Description check

Explanation

The description follows the template. It has a "What does this PR do?" section, covered by the Problem, Fix, Background, and Downsides sections. It has a "How did you verify your code works?" section, covered by the Verified and Tests content and the Measurements section. It links the issue (#28117) and gives concrete evidence: failing-before and passing-after test runs, a comparison with bun build, and performance measurements. It is very detailed, and the main body is long, but every required section has substantive content. Some open items are noted as follow-up work, so the description is mostly complete.

Full details: Linked Issues check

Explanation

Issue #28117 requires Bun.serve to preserve sequential stylesheet import order so the later stylesheet wins as in Bun.build. The PR changes rebuilt import lists from front insertion to tail append in src/runtime/bake/dev_server/incremental_graph.rs. The added HTML-route test verifies source order across rebuilds, additions, removals, and repeated references. The framework-route test verifies the corresponding metadata order.

Full details: Out of Scope Changes check

Explanation

The changed Rust code directly implements the stylesheet import-order fix. The added tests cover HTML and framework routes and are supporting automated coverage for issue #28117. No unrelated product behavior or unrelated files appear in the reviewed changes.

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

@robobun

robobun commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

#48141 changes where a bundle puts the imports of an eager import.meta.glob(): ahead of the file's own imports, as the runtime and Vite do. It adds a row to test/bake/dev-and-prod.test.ts. The row asserts that the stylesheets of the glob are linked before the file's own stylesheets, in development and in production.

Today the dev server passes that row only because it links stylesheets in reverse. The parser adds the import records of a glob during the visit pass, so they come behind the file's own records. For import "./reset.css"; import.meta.glob("./sheets/*.css", { eager: true }); import "./last.css"; the records are reset.css, last.css, a.css, b.css.

If the dev server follows import record order, development links .reset .last .a .b. Production writes .a .b .reset .last after #48141. The row then fails in development. The PR that lands second has to put the records of the glob ahead of the file's own in that walk, or take the order from the parts.

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.

HTMLBundle is inconsistent with the css content of Bun.build

1 participant