Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(html): warn when build cannot preserve authored stylesheet link order - #23436

Open
kakiuwang-ui wants to merge 1 commit into
vitejs:mainfrom
kakiuwang-ui:fix/html-warn-unpreservable-link-order
Open

feat(html): warn when build cannot preserve authored stylesheet link order#23436
kakiuwang-ui wants to merge 1 commit into
vitejs:mainfrom
kakiuwang-ui:fix/html-warn-unpreservable-link-order

Conversation

@kakiuwang-ui

Copy link
Copy Markdown
Contributor

Description

vite build can silently invert the cascade order of stylesheet links authored in HTML.

vite:build-html converts a resolvable <link rel="stylesheet"> into a JS import and removes the node; the resulting stylesheet is appended at the end of <head> in generateBundle. Links Vite leaves in place — publicDir files, external URLs, media/disabled, unresolvable ones — keep their authored position. So any stylesheet link authored after a bundled one ends up before it, and the page is styled differently than in dev. Nothing fails; only the rendered result differs.

This is #8739 (open since 2022, p3-minor-bug + inconsistency, six reporters).

This PR does not fix the ordering — it makes the failure visible. I built the obvious fix (put the generated <link> back where the authored one was) and measured that it trades one inversion for another: in build, the CSS from HTML-authored links and the CSS imported from the entry's JS are the same output file, so the single generated <link> has one slot, while dev establishes two orderings (authored < retained < fromjs, the last because the dev client appends a <style> to <head> at runtime). Computed colours measured in Chromium:

dev build (main) build (move the link back)
<link> + publicDir link pub wins bundle wins pub wins ✅
the above + CSS imported from JS fromjs wins fromjs wins ✅ pub wins

Details and the proposed real fix (emitting the CSS reachable from HTML-authored links as its own chunk) are in #8739 (comment) — that one needs a maintainer decision, so this PR is deliberately limited to the warning, which is useful either way.

Implementation

Records every <link rel="stylesheet"> that is a direct child of <head> in document order, tagged with whether Vite bundles it away, and warns when a retained one is authored after a bundled one.

  • The head-child restriction is load-bearing, not defensive. traverseHtml descends into <template>, and parse5 runs with scriptingEnabled: false, so a stylesheet link inside <noscript> is a real element in the tree. Those don't participate in the cascade the built page applies, and counting them produced false positives.
  • Only rel="stylesheet" is considered — preload/icon/manifest links don't take part in the cascade.
  • The bundled flag mirrors the existing "CSS references, convert to import" condition rather than restating it; links that later fail to resolve are downgraded to retained in the loop that already handles that case, because they keep their authored position.
  • warnOnce, so a repeated build of the same page doesn't repeat it.

Notes for review

  • The warning names the two conflicting hrefs and the file. I went back and forth on whether to point at the first bundled link or the last; the first is what determines the boundary, so that's what it reports.
  • Interleaved links are reported once, not per pair. A document alternating bundled and retained stylesheet links has several inverted pairs; the warning reports the first. Reporting all of them seemed noisier than useful, but I'll change it if you'd rather.
  • I deliberately did not touch the media/disabled path. Those links are also left in place and so can be displaced, and they're counted here, but CSS with media and disabled prop not processed #9402 tracks the fact that they aren't processed at all — I didn't want to widen this.
  • No behaviour change. Output is byte-identical; this only adds a log line. I verified that by building playground/html on this branch and on main and diffing every emitted file.
  • One adjacent bug I found while investigating and did not fix here: a <link rel="stylesheet"> inside <noscript> in <head> is converted to an import and its CSS is emitted unconditionally, so a no-JS-only stylesheet applies to everyone. That's current main behaviour and independent of this change; happy to open a separate issue.

Test

playground/html/link-order/ adds two complete documents: one with a publicDir sheet authored after a bundled one (must warn) and one with the same two sheets in the order that survives building (must not warn). Both assert against serverLogs in build mode.

They are complete documents rather than fragments on purpose: playground/html's own pre-transform plugin wraps any fixture without a <!doctype html> in a synthesized <html><head>…</head><body>, which pushes authored links into <body> — a fragment fixture cannot exercise this path at all. My first attempt at a fixture did exactly that and passed without the implementation.

Verified discriminating: reverting html.ts and rerunning fails the positive test and leaves the negative one passing.

@kakiuwang-ui

Copy link
Copy Markdown
Contributor Author

The Build&Test: node-20, ubuntu-latest failure is unrelated to this PR.

It failed on playground/legacy/__tests__/watch/legacy-styles-only-entry-watch.spec.ts > rebuilds styles only entry on change with ENOENT: ... dist/watch/.vite/manifest.json — a watch-mode race reading the manifest before it is written, not an assertion failure.

The same test fails on main itself: run 33833431519 (2026-09-04). Across the last 15 main CI runs, 4 failed, each on a different watch/timing-sensitive spec (this one, css/lightningcss, hmr-ssr ×2).

Locally on this branch, VITE_TEST_BUILD=1 vitest run -c vitest.config.e2e.ts playground/legacy is 30/30 green, and the full build suite is 944 passed / 0 failed — the same 944 total the CI job reports. This PR only adds a warnOnce call, so it cannot affect whether a manifest file is written. I don't have permission to re-run the job.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant