Repository navigation
fix(core): bundling and rendering keep print-only, titled and non-CSS styles conditional - #4664
Conversation
A style with a non-empty title belongs to a style sheet set; the browser enables only the preferred set, so merging it into an untitled (persistent) style or one with another title changes what applies. An empty title is persistent, like no title; titles compare case-sensitively. An inlined linked sheet carries its link's title. The merge key returns undefined for a non-CSS type (never merged or inlined) and treats media="all" as no media, as the browser does.
…eeping media and title
jrusso1020
left a comment
There was a problem hiding this comment.
Review at 5a6ac471ca63a229301afe392b4ca96c9412d0a8.
Verdict: REQUEST_CHANGES
The head-style part is correct and well tested. cssStyleMergeKey and headStyleRuns do what the body says, both paths call them, and every edge case I tried in headless Chrome keeps its condition in the bundle, the Studio bundle and the render. That covers PRINT in upper case, screen and (min-width: …), media="", Text/CSS, text/css; charset=utf-8 on a <style>, two titled sets, a preferred titled link followed by a differently titled style, rel="alternate stylesheet", a disabled link, and a linked sheet whose @import has its own media. One problem is new, though. Composition links are deduped by href alone, and now that they carry media and title, the first link's condition applies to every later composition that links the same file. On main that case worked. I also found some older paths in the render that still make a conditional sheet unconditional. They are not blocking, but they cut against the PR's claim that the two paths now match.
Findings
Blocking
1. A composition's plain link disappears when an earlier composition links the same file with a media or title. inlineSubCompositions.ts still dedupes hoisted links with seenLinkHrefs, keyed on href only. Only the first link is emitted, with its media and title.
Probe: composition a has <link rel="stylesheet" href="shared.css" media="print"> and composition b has <link rel="stylesheet" href="shared.css">. The bundle, the Studio bundle and the render all emit one <link media="print" href="shared.css">. In Chrome, b's text loses its styling on screen (computed black, expected green). The same happens with title. When a's link has title="alt" and the root's preferred set is main, the one emitted link is an alternate sheet, so b is unstyled again.
On main this was fine for b, because the media was dropped and the one link was unconditional. So this PR fixes a by breaking b. A small fix works: key the dedupe on href plus the link's condition (rel, normalized media, title), and add a test with two compositions sharing an href.
Non-blocking
2. The render turns a remote @import in any <style> into an unconditional <link>. promoteCssImportsToLinkTags (htmlCompiler.ts:766) runs right before the coalescer (:2011). It ignores the style's media, title and type, and it also ignores the @import's own media list. Probes, run against a local server:
<style media="print">@import url(http://…/blue.css);</style>gives screen color blue in the render and red in the bundle.<style type="text/x-tpl">@import url(…);</style>loses its text in the render and gains an applied stylesheet link.@import url(http://…/blue.css) print; .t{color:green}becomes a link plus the style textprint; .t{color:green}. The strayprint;makes the next rule invalid, so the render shows blue while the bundle and the source show green.
This code predates the PR, but it is on the render path the title says is fixed. Please fix it here, or add it to the known limits.
3. The render drops a media-bearing @import in the second style of a merged run. The producer's merge regex (htmlCompiler.ts:875) does not match @import url("s.css") screen;, so the import is not hoisted. After the merge it sits mid-sheet and Chrome ignores it. The bundler's CSS_IMPORT_RE does hoist it. Probe: <style>.u{}</style><style>@import url("s.css") screen; .v{}</style> shows blue in the bundle and black in the render. This is older than the PR too. Using the bundler's regex (or joinCssHoistingImports) in the producer would close it.
4. The Studio bundle lifts @import out of print and non-CSS styles. placeSceneStylesLikeRender pulls every @import from every head style and moves it into the first unconditional style. The known limits already mention this, and my probe confirms it for a root print style and for a composition print style. It also applies to a type="text/x-tpl" style, which the note does not mention.
5. Hoisted composition links lose disabled and type. ExternalLink carries media and title but not those two attributes. A disabled composition link comes out enabled (computed blue in all three outputs), and a type="text/x-scss" link comes out as a plain applied stylesheet. The head-link path now skips both cases. The composition path could do the same, or copy the attributes. This predates the PR.
6. <link type="text/css; charset=utf-8"> is no longer inlined. Chrome applies a link with that type (the source probe shows blue). cssStyleMergeKey treats it as non-CSS, so the bundle now leaves a relative link where main inlined the sheet. That is right for <style>, where Chrome does not apply that type, but it is too strict for <link>. The effect is small: the sheet still loads when the bundle is served from the project directory.
7. Test gap. 24 of the 25 mutations I ran against the new guards were caught. The one that survived: removing the non-CSS skip in the bundler's template branch with no matching inner root (htmlBundler.ts, the innerDoc.querySelectorAll("style") loop). No test reaches that branch with a non-CSS style.
Claims checked
- One merge decision for both paths: confirmed. The bundler's and the producer's
coalesceHeadStylesAndBodyScriptsboth callheadStyleRuns, and replacing it with a single all-styles run in the producer fails 2 parity tests. - The merge key rules (media trimmed and lowercased,
allcounted as none, empty title counted as none, CSS type case-insensitive, adjacent runs only): confirmed in source. Each rule has a mutation that fails a test. - Each local linked sheet is inlined at its place with its
mediaandtitle, anddisabledand non-CSS links are left alone: confirmed in the bundle. The render never inlines local links, so they stay as links there. That keeps their condition, but the inlined-versus-linked difference between the two paths remains, as the known limits say. - The
InlineSubCompositionsResult.stylesAPI change: the only consumers are the bundler and the producer, and both are updated.tsc --noEmitis clean for core and producer. - "Composition
<link>elements keepmediaandtitle": true, except for the dedupe problem in finding 1. - "Render and preview produce the same styles": true for the new cases. It does not hold for findings 2 and 3, which are older code.
- I did not reproduce the claim that 87 fixtures are byte-identical to main.
Tests run
- Core
vitest run src/compiler: 378 passed. Coretsc --noEmit: clean. - Producer
vitest run src/services/htmlCompiler*: the vitest-lane files pass, including the new parity tests. The bun-lane files pass (144, viabun test). Producertsc --noEmit: clean. - Probes: 27 small projects run through
bundleToSingleHtml,bundleToSingleHtml({ sceneParts: true })andcompileForRender. Each output was loaded in headless Chrome with JS off, and I checked the computed colors and which sheets were applied. The source HTML was the reference for head-only cases. - Mutations: 25 reverted guards across
scriptRuns.ts,htmlBundler.ts,inlineSubCompositions.tsandhtmlCompiler.ts. 24 were caught. The producer's vitest resolves@hyperframes/core/compilertodist, so I rebuilt core for each core mutation checked against the parity tests.
Checks
62 passing, 8 skipped, none failing or pending at this head.
Gate
reviewDecision REVIEW_REQUIRED, mergeStateStatus BLOCKED, mergeable MERGEABLE. There are no reviews on the PR yet.
— Rames
jrusso1020
left a comment
There was a problem hiding this comment.
Re-review at 67b51c6253e6e391aa856b1af128ed8a5cd4739c. This keeps my REQUEST_CHANGES at 5a6ac471.
Verdict: REQUEST_CHANGES
The only new commit is a merge of main. git show --remerge-diff on it is empty, and the PR's own patch against its merge base is the same as at 5a6ac471 apart from hunk line numbers. So the blocker from my last review is still there, and I reproduced it at this head. The merge also creates one new problem. Main's new font dedupe (#4643) treats a titled <style> as always applied. Now that this PR keeps titled styles apart, the bundle can drop the only applied copy of an @font-face. Both fixes are small, and I sketch them below.
Findings
Blocking
1. Still open from my last review: composition links are deduped by href alone, so one link's media or title wins for every composition that links the same file. No code changed here.
Probe at this head: composition a has <link rel="stylesheet" href="shared.css" media="print"> and composition b has <link rel="stylesheet" href="shared.css">. The bundle, the Studio bundle and the render all emit one <link media="print">, and in Chrome b renders black on screen instead of green. The same happens with media="print" then media="screen", and with title="alt" then no title. On main all three cases render green. Two identical print links still collapse to one print link, which is correct.
The fix has to reach every site that dedupes by href, not only seenLinkHrefs:
inlineSubCompositions.ts:453(seenLinkHrefs)htmlBundler.ts:1269, which skips a composition link when anylink[href]already existshtmlCompiler.ts:1040in the producer, which does the same for the rendercompositionLoader.ts:460, the runtime mount, which does the same in the live preview. That one behaved this way on main too.
I checked that fixing one site is not enough. I wrote a 6-case test (below). It fails at this head. It passes when seenLinkHrefs is keyed on [href, rel, cssStyleMergeKey(link)] and the bundler check at :1269 only skips a link with the same rel, media and title. With only one of those two changes, the two "plain link after a conditional link" cases still fail. With both changes the core compiler suite still passes (393 tests plus the 6 new ones). The producer and runtime sites need the same check for the render and the mount.
async function sharedLinks(aAttrs: string, bAttrs: string) {
const dir = project({
"index.html": index(host("a") + host("b")),
"a.html": comp("a", `<link rel="stylesheet" href="shared.css"${aAttrs}>`),
"b.html": comp("b", `<link rel="stylesheet" href="shared.css"${bAttrs}>`),
"shared.css": ".a,.b{color:green}",
});
const { document } = parseHTML(await bundleToSingleHtml(dir));
return [...document.querySelectorAll('link[href="shared.css"]')].map((el) => ({
media: el.getAttribute("media"),
title: el.getAttribute("title"),
}));
}
// fails at this head:
expect(await sharedLinks(' media="print"', "")).toContainEqual({ media: null, title: null });
expect(await sharedLinks(' title="alt"', "")).toContainEqual({ media: null, title: null });
// passes before and after:
expect(await sharedLinks(' media="print"', ' media="print"')).toEqual([{ media: "print", title: null }]);2. New with the merge: the bundle's font dedupe can drop the only applied @font-face when an alternate titled style repeats it. isAlwaysAppliedStyle (htmlBundler.ts:713, from main) excludes styles with media, a non-CSS type, or a template/noscript/svg parent. It does not exclude title. dedupeFontFaceRules keeps the last copy of each face. Before this PR, titled head styles were merged into the untitled ones, so every copy was applied and it did not matter which copy survived. With this PR, a later <style title="alt"> stays its own element. When alt is not the preferred set, Chrome does not apply it, and the bundle has removed the face from the untitled style.
Probe: head <style>@font-face{font-family:"PF";src:url(f.woff2)}.t{font-family:"PF"}</style><style title="main">.x{}</style><style title="alt">@font-face{…same…}</style>. In Chrome, document.fonts holds a loaded PF in the source, in the render and in main's bundle. In this head's bundle document.fonts is empty. The same happens when the repeated face comes from a composition's titled style. A test that expects the untitled style to keep its @font-face fails at this head. Adding !el.hasAttribute("title") to isAlwaysAppliedStyle makes it pass, and the core compiler suite stays green. A preferred set on its own is applied, so it would be safe to dedupe into. But skipping every titled style is the simple, safe choice.
Non-blocking
3. rel="alternate stylesheet" and a plain link to the same file. If composition a links shared.css as an alternate sheet and b links it plainly, only the alternate link is emitted and b is unstyled. Main does the same, so this is not new. Including rel in the dedupe key from finding 1 fixes it.
4. Root link and composition link to the same file. If the root has <link href="shared.css" media="print"> (or title="alt") and a composition links shared.css plainly, the render drops the composition's link, so b is unstyled. The bundle inlines the root link as a print style and keeps the composition link, so b is styled there. Main does the same, so this is not new. The producer fix from finding 1 closes it.
5. Findings 2 to 7 from my last review are unchanged. No code changed, so the notes on promoteCssImportsToLinkTags, the producer's @import merge regex, @import lifting in the Studio bundle, disabled/type on hoisted composition links, <link type="text/css; charset=utf-8"> and the untested template branch still apply as written.
Claims checked
- "Merge main" only: confirmed. The first-parent log since
5a6ac471has one commit, the merge. Its remerge diff is empty. The PR patch against the new merge base differs from the old one only in hunk offsets. - Nothing addresses the dedupe finding: confirmed.
seenLinkHrefsstill keys onhref(inlineSubCompositions.ts:317,:453). There are no new commits, comments or replies since my review. - Composition
<style media>and titled styles still keep their condition: confirmed at this head. A composition<style media="print">renders black on screen in all three outputs. On main it is green. - How main's changes affect the PR files: the merge brings #4643's
dedupeFontFaceRulesandisAlwaysAppliedStyleintohtmlBundler.ts. That is where finding 2 comes from. No other main change touches the PR's code paths.
Tests run
- Core
vitest run src/compiler: 393 passed. Core and producertsc --noEmit: clean.oxlintandoxfmt --checkon the 8 PR files: clean. - Producer bun lane: 806 passed, 0 failed. Producer vitest lane: 849 passed and 1 failed. The failure is
audioPadTrim.integration.test.ts(a loudness bound, 4.3 against 3). That file is not touched by this PR,Testpasses in CI, and it most likely depends on my local ffmpeg build.htmlCompiler.parity.test.ts: all passed. - Probes: 16 projects built with
bundleToSingleHtml,bundleToSingleHtml({ sceneParts: true })andcompileForRender, at this head and at main (d2f0bc7f). I loaded each output in headless Chrome with JS off and checked computed colors, the links and styles emitted, anddocument.fonts. Cases: shared href with differingmedia(print then plain, plain then print, print then screen, both print),title(both orders),disabled,rel="alternate stylesheet",type="text/x-scss",crossorigin, a root link plus a composition link with the same href, a composition<style media="print">, and the two font-dedupe cases plus a no-font control. - Mutations: the fix for finding 1 applied at each dedupe site on its own (each alone leaves 2 of the 6 cases red), and the one-line fix for finding 2. I reverted everything afterwards.
Checks
All 10 required contexts pass at this head: Semantic PR title, Test: runtime contract, Typecheck, Build, regression, Test, Render on windows-latest, Tests on windows-latest, Studio and player captures, Test reachability. In total 56 checks pass and 9 are skipped, with none failing or pending. There are no legacy commit statuses.
Gate
reviewDecision CHANGES_REQUESTED, mergeStateStatus BLOCKED, mergeable MERGEABLE. The only review is my REQUEST_CHANGES at 5a6ac471. main does not dismiss stale reviews on push, requires approval of the last push, and requires one approving code-owner review.
— Rames
…other links the same file Hoisted composition links are deduped on href, rel, condition, type and disabled rather than href alone, in the bundle, the render and the live mount, and keep their type and disabled state.
…ate style repeats it The font dedupe asks cssStyleMergeKey whether a style always applies, so a titled style no longer counts as always applied.
Chrome applies <link type="text/css; charset=utf-8">, so a link's type is read by its MIME essence; a style's type still has to be exactly text/css. Adds a test for a template with no matching root and a non-CSS style.
…policy, not noscript copies One helper, hasSameLink, decides whether a hoisted link duplicates a live one, for the bundle, the render and the mount. inlineSubCompositions no longer dedupes its result on its own. Tests cover rel, disabled, non-CSS type, CORS, integrity, referrer policy and a root noscript link.
…n fetch attributes ship once The link key goes back to href, rel, condition and disabled; crossorigin, integrity and referrerpolicy are not compared, so a root snippet repeated in a composition is emitted once and the root's override styles keep their order.
jrusso1020
left a comment
There was a problem hiding this comment.
Re-review at 907d15b4bdafd0704722fe9c61e8b00ed949a322. This follows my REQUEST_CHANGES at 67b51c62.
Verdict: APPROVE
Both blockers are fixed. I reproduced each one at 67b51c62 and checked it is gone at this head in headless Chrome, in bundleToSingleHtml, bundleToSingleHtml({ sceneParts: true }) and compileForRender. Every fix has a test that fails when the fix is reverted. The notes below are non-blocking.
Prior blockers
1. Composition links deduped by href alone: fixed. There is now one key, linkDedupeKey in scriptRuns.ts: href, rel (trimmed, lowercased), the condition from cssStyleMergeKey (or the raw type for a non-CSS link), and disabled. One helper, hasSameLink, uses it. ensureExternalLinkTag is the one place a hoisted link is added. All four sites I listed now go through it:
inlineSubCompositions.ts:seenLinkHrefsis gone, and the result may list a link twice.htmlBundler.ts: callsensureExternalLinkTag.- the producer's
htmlCompiler.ts: callsensureExternalLinkTag. compositionLoader.ts, the runtime mount: callshasSameLink(document.head, clonedLink).
Chrome, JS off. Composition a links shared.css one way and b links it plainly or another way. b should be green on screen.
| case | 67b51c62 (bundle / Studio / render) |
this head |
|---|---|---|
| print, then plain | black / black / black | green / green / green |
| print, then screen | black / black / black | green / green / green |
title="alt", then plain |
black / black / black | green / green / green |
rel="alternate stylesheet", then plain |
black / black / black | green / green / green |
| root print link, composition plain | green / green / black | green / green / green |
root title="alt" link, composition plain |
green / green / black | green / green / green |
| print twice (should stay print) | one print link, black | one print link, black |
So my old non-blocking findings 3 (alternate, then plain) and 4 (root link, then composition link) are fixed too. The runtime mount is covered by the new parity test, which runs loadExternalCompositions under happy-dom. I did not check the mount in Chrome.
2. Font dedupe could drop the only applied @font-face: fixed. isAlwaysAppliedStyle now requires cssStyleMergeKey(el) === UNCONDITIONAL_CSS_KEY, so a titled style no longer counts as always applied. Probe: an untitled style with @font-face PF, a title="main" style, then a title="alt" style that repeats the face. At 67b51c62 the bundle's untitled style was .t{font-family:"PF"} with the face removed. At this head the untitled style keeps the face in the bundle, the Studio bundle and the render, and document.fonts has PF loaded in all three. This holds whether the repeated face comes from the root or from a composition.
New findings
Blocking
None.
Non-blocking
1. The "Known limits" wording is narrower than what happens. I checked each one I could.
- "A stylesheet
<link>between two head styles … the render can reorder a linked sheet." It is not only the render. With<style>.t{color:red}</style><link href=blue.css><style>.t{color:green}</style>, the source is green, and the bundle, the Studio bundle and the render all move the second style above the link and show blue. Main does the same, so it is not new. The wording should say all three outputs. - "A root
<link crossorigin>… stands in for a composition's plain link." The same happens between two compositions. Ifalinks a file withcrossoriginfrom a server that sends no CORS headers andblinks it plainly, onlya's link ships andbis unstyled. A root link with anintegrityvalue that does not match does the same thing. Main does the same in both cases. One sentence covering fetch attributes in general would be accurate. - The hoisted link attributes (no
integrity,referrerpolicy,as,fetchpriorityin the bundle and render; kept by the runtime mount's clone) and the producer font helpers' owntypechecks (deterministicFonts.ts:866,authoredGoogleFonts.ts:54) match the code.
2. A local root link plus the same composition link still differs between the bundle and the render. Root head: <link rel="stylesheet" href="shared.css"><style>.pb{color:red}</style>, and composition b links shared.css. The bundle inlines the root link as a style first, so the composition's link no longer matches it and is added at the end of the head. b is green. The render still has the root link, the key matches, and it skips the composition's link. b is red. The runtime mount compares a relative href with an absolute one, so it appends the link too and would match the bundle. Main gives the same results, so this is not new. It fits with the follow-up on link placement.
3. Some spellings still produce two links. That is harmless. rel="alternate stylesheet" (two spaces) and rel="alternate stylesheet" ship as two links, and so do ./shared.css and shared.css. The same sheet applied twice under the same condition renders the same. Over-merging is the harmful direction, and I found none: rel case (Stylesheet), media=" PRINT " against print, and media="all" against no media each collapse correctly and render correctly. A link inside an unrelated root <template> or <noscript> is not treated as live.
Claims checked
- "One dedupe owner for bundle, render and mount": confirmed. See blocker 1.
linkDedupeKeyis private, and all three paths reach it throughhasSameLink. - "Fetch attributes are not part of the key, so a root link and a composition's link to the same file ship once, as on main": confirmed by the test and by probe.
907d15b4put this back afterc54add6dhad added them to the key. - "A link's
typeis read by its MIME essence … and still does not on a<style>": confirmed. The new test covers both halves, and mutating either half fails it. - Non-blocking 3 to 7 from my last review:
typeanddisabledare now carried on hoisted links. The charset link is inlined. The template branch with no matching root now has a test. The@importpoints are listed as known limits for a follow-up, and their wording matches what I found before. - Merges: one merge of main (
34eeee38) since67b51c62.git show --remerge-diffon it is empty. Six PR commits follow it.
Tests run
- Core
vitest run src/compiler src/runtime/compositionLoader.test.ts: 473 passed. Core and producertsc --noEmit: clean.oxlintandoxfmt --checkon the 9 changed files: clean. - Producer
htmlCompiler.parity.test.ts: 13 passed. Producer bun tests forhtmlCompiler.test.ts,htmlCompiler.animatedGif.test.tsandhtmlCompiler.naturalDuration.test.ts: 144 passed. - Probes: 21 projects at this head and at the merge base
5606b310, each built three ways and loaded in headless Chrome with JS off. I checked computed colors, the emitted links and styles, anddocument.fonts. Remote cases used a local HTTP server. - Mutations: I made 15, one at a time, and rebuilt core before each producer run. All 15 failed a test. Four of them each failed both the core tests and the parity test: key on
hrefonly, key without condition,ensureExternalLinkTagnever dedupes, and the bundler back to the old href check. The producer back to its old href check and the runtime mount back to its old href check each failed the parity test. The key withoutrel, the key withoutdisabled, the key withcrossoriginadded, andnoscriptlinks counted as live each failed a core test. So did droppingtypeordisabledon hoisted links, the oldisAlwaysAppliedStyle, reading a link's type raw, and reading a style's type by its MIME essence. I restored every file afterwards, andgit statuswas clean.
Checks
All 10 required contexts pass at this head: Semantic PR title, Test: runtime contract, Typecheck, Build, regression, Test, Render on windows-latest, Tests on windows-latest, Studio and player captures, Test reachability. In total 67 checks pass and 6 are skipped, with none failing or pending. A second regression run at this head also finished green.
Gate
reviewDecision CHANGES_REQUESTED (my review at 67b51c62), mergeStateStatus BLOCKED, mergeable MERGEABLE. This approval replaces it. main requires approval of the last push and one approving code-owner review.
— Rames
What
Bundling and rendering no longer turn a conditional stylesheet into an always-on one. A
<style media="print">, a titled (alternate) style, a disabled or non-CSS stylesheet, and the same inside a composition, now keep their condition in the bundled film and in the render, and the two paths produce the same styles.Why
coalesceHeadStylesAndBodyScriptsmerged every head<style>into the first one, keeping only that one'smediaandtype. So<style media="print">p{color:blue}</style>came out as an unconditionalp{color:blue}and applied on screen. The render path (packages/producer/src/services/htmlCompiler.ts) had its own copy of the same merge, and composition styles were copied as text only, so they lost their attributes on both paths.How
cssStyleMergeKeyandheadStyleRuns(packages/core/src/compiler/scriptRuns.ts) decide which head styles may merge: only adjacent styles that the browser applies under the same condition (samemedia, trimmed and lowercased, withalltreated as none; sametitle, an empty title counting as none; a CSStype). Adjacent runs only, so rule order never changes. The bundler and the render path both call it.<link rel="stylesheet">is inlined as its own<style>in its own place, carrying the link'smediaandtitle. Adisabledlink and a non-CSStypeare left as links.media,title) instead of strings.styleElementsForturns them into<style>elements on both paths, so a conditional composition style is its own element. A malformedmediavalue then matches nothing instead of swallowing later CSS, and an@importin it stays at the top of its own sheet. Composition<link>elements keepmedia,title,typeanddisabledtoo. A non-CSS composition style is kept out of CSS.linkDedupeKey(scriptRuns.ts) is href plus rel plus the link's condition fromcssStyleMergeKey, plusdisabled.ensureExternalLinkTag(inlineSubCompositions.ts) is the one place that adds a hoisted link; the bundler, the render and the runtime mount all compare by that key. Two compositions linking the same file under different conditions now each keep their own link; the same file under the same condition still collapses to one. Fetch attributes (crossorigin,integrity,referrerpolicy) are not part of the key, so a root link and a composition's link to the same file ship once, as on main.isAlwaysAppliedStylenow askscssStyleMergeKeyfor the unconditional key, so an alternate titled style is no longer a place to keep the only copy of an@font-face.typeis read by its MIME essence, sotext/css; charset=utf-8counts as CSS on a<link>(Chrome applies it) and still does not on a<style>(Chrome does not).API note:
InlineSubCompositionsResult.styles, exported from@hyperframes/core/compiler, changes fromstring[]to{ css, media?, title? }[]. A caller that joined the styles as text needs to read.css.Test plan
type="TEXT/CSS"andmedia=" ALL "); rule order kept across a conditional style; each linked sheet at its link's place with its media; non-CSS and disabled links not inlined; titled styles merge only with the same title; composition print style kept as its own element (file and inline template); non-CSS composition style kept out of CSS.htmlCompiler.parity.test.ts): render equals preview for mixed conditional head styles, and for conditional composition styles and links (invalid media keeps later compositions styled, a screen-media@importsurvives, titles kept).@importcases, and titled and linked composition styles.bundleToSingleHtml(plain and Studio options) are byte-identical to the merge base apart from the inlined preview runtime (whose loader changed), and 139 of the 141 that render throughcompileForRenderare byte-identical (the other two differ only in embedded font bytes, which also differ between two runs of the same head). None of them has a conditional style or link.typeanddisabledcarried on hoisted links, a charset link inlined and a charset style not, and a template with no matching root and a non-CSS style. A parity test checks the shared-file case in the bundle, the render and the runtime mount.tsc --noEmitpasses for core and producer.packages/studioorpackages/player, so no before/after captures are needed.Known limits
<link>between two head styles does not yet end a merge run, so the render can reorder a linked sheet against the styles around it (as on main). The fix changes the Studio preview's style placement too and lands with the follow-up that keeps a lifted@importunder its style's media.@importinside a conditional<style>is not yet kept under that condition by the render's import promotion, by the render's run merge when the import has its own media, or by the Studio bundle's import lifting. The follow-up that keeps a lifted@importunder its style's media owns all three.media,title,type,disabledandcrossorigin, but not yetintegrity,referrerpolicy,asorfetchpriorityin the bundle and the render (the runtime mount keeps them), as on main.<link crossorigin>whose server sends no CORS headers still stands in for a composition's plain link to the same file, and that composition renders unstyled, as on main.typeby its MIME essence.