feat(webpack-plugin): unstable_layeredOutput for chunkable CSS via @layer#851
Draft
layershifter wants to merge 13 commits into
Draft
feat(webpack-plugin): unstable_layeredOutput for chunkable CSS via @layer#851layershifter wants to merge 13 commits into
layershifter wants to merge 13 commits into
Conversation
Adds a design doc for chunking the extracted Griffel CSS via opt-in CSS Cascade Layers (`unstable_layeredOutput`), plus a small `apps/chunking-repro` app that demonstrates the cross-chunk cascade ordering problem the design addresses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10-task TDD plan for delivering unstable_layeredOutput, broken into 1) bucket reclassification in @griffel/core (gated), 2) bucketStrategy plumb through @griffel/transform, 3) loader-time @layer wrapping in @griffel/webpack-plugin, 4) asset-time global manifest aggregation across chunks, and 5) a layered build mode in apps/chunking-repro. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional strategy parameter to getStyleBucketName. The new "extended" strategy walks the full selector and bucketizes by the last LVHA pseudo found anywhere, instead of only the leading character.
Adds an options parameter to resolveStyleRules and forwards bucketStrategy to getStyleBucketName from every call site, including recursive descents into nested selectors and at-rules.
Threads ResolveStyleRulesOptions through resolveStyleRulesForSlots and resolveResetStyleRules so build-time callers can opt into the extended bucket strategy.
Forwards a new bucketStrategy option to resolveStyleRulesForSlots so callers of @griffel/transform can extract atomic CSS using the extended bucket assignment.
Pure helpers used by the loader to emit @layer wrappers and by the plugin's processAssets pass to substitute media/container placeholders.
Adds a wrapInLayer mode to generateCSSRules. Each block between @Griffel:css-start / @Griffel:css-end markers is wrapped in @layer griffel.<bucket>[.s<priority>][.<placeholder>] { ... }, with marker comments staying outside the wrapper so the asset-time parser continues to find them at the top level.
Reads the layered-output flags from the loader context and forwards them to transformSync and generateCSSRules.
Adds the opt-in flag, plumbs it to the loader context, and skips the forced 'griffel' SplitChunks cache group + chunk-merge fallback when the flag is on. Throws when combined with unstable_attachToEntryPoint.
Adds a processAssets pass that, when unstable_layeredOutput is on, walks every griffel-bearing CSS asset, aggregates the union set of @media / @container queries, sorts them with compareMediaQueries, emits a global manifest at the top of every asset, and substitutes hash placeholders with indexed q<N> layer names. Test helper: Option A (extend compileSourceWithWebpack with entryMap? in TestOptions) so existing tests are unaffected. Added readAsset() to the return value. New fixture: __fixtures__/layered-multi-entry/ with page-a.ts (800px), page-b.ts (1200px), and shared.ts. Note: 'c' bucket is excluded from the static-bucket assertion because @griffel/core does not currently emit 'c' metadata for container queries, so no __griffelcq_ placeholder is generated and the bucket name is emitted verbatim as griffel.c (handled by bucketLayerName). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the repro to the new unstable_layeredOutput plugin option so the layered output can be inspected side-by-side with the default and split modes.
…kets
STATIC_BUCKET_LAYERS only declared priority sub-layers for bucket 'd'.
Any selector-driven bucket can carry shorthand priority (e.g.
':hover': { padding: ... } → bucket 'h', priority -1), so the manifest
must declare s-2/s-1 sub-layers for d, l, v, w, f, i, h, a.
Without this, sub-layer ordering across chunks becomes
implementation-defined and reintroduces the cascade nondeterminism
the layered design exists to solve.
Also corrects the spec to match the implementation's hash handling
(no build-time collision detection; relies on 32-bit entropy).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in
GriffelPlugin({ unstable_layeredOutput: true })that lets webpack's defaultSplitChunksPluginchunk Griffel-extracted CSS into multiple files without breaking cascade order.When the flag is on, every emitted atomic rule is wrapped in a CSS Cascade Layer (
@layer griffel.<bucket>[.s<priority>][.q<N>] { … }), and the plugin prepends a build-time-discovered global layer manifest to every griffel-bearing CSS asset. Cross-chunk LVHA, shorthand→longhand priority, and overlapping@mediabreakpoints all resolve via layer order — independent of which CSS file the browser parses first.When the flag is off, behavior is byte-identical to today (verified by 23 fixture-based inline-snapshot tests passing without regeneration).
Architecture
@griffel/core: new opt-inBucketStrategy = 'leading' | 'extended'.'extended'reclassifies nested-pseudo selectors (e.g.& .foo:hover) into their pseudo bucket so layer order doesn't defeat their specificity. Plumbed throughresolveStyleRulesandresolveStyleRulesForSlots.@griffel/transform:transformSyncaccepts and forwardsbucketStrategy.@griffel/webpack-plugin:bucketLayerName,mediaPlaceholder,containerPlaceholder,hashOfQuery.generateCSSRules({ wrapInLayer: true })wraps each@griffel:css-start … @griffel:css-endblock in@layer …. Marker comments stay outside the wrapper soparseCSSRulescontinues to find them at the top level.bucketStrategyandwrapInLayerfrom the loader context.GriffelPluginaddsunstable_layeredOutput?: boolean. When on: skips the forced'griffel'SplitChunks cache group + chunk-merge fallback; sets the loader-context flags; throws on combo withunstable_attachToEntryPoint.processAssetsruns a multi-chunk pass: walks all griffel-bearing CSS assets, aggregates the union set of@mediaand@containerqueries, sorts viacompareMediaQueries, builds a global@layer ...;manifest, substitutes hash placeholders forq<N>indices, prepends the manifest to every asset.@layerat runtime; SSR rehydration unchanged.Spec:
docs/superpowers/specs/2026-04-25-griffel-css-chunking-design.mdRepro:
apps/chunking-repro/— three modes (default,split,layered) for side-by-side inspection.Trade-offs / known limitations
@layerrequires Chrome 99+ / Safari 15.4+ / Firefox 97+ (released early 2022). Apps with older baselines should leave the flag off.t(@layer/@supports): not split into per-rule sub-layers in V1.Test plan
@griffel/core:test— 432 tests pass@griffel/transform:test— 80 tests pass@griffel/webpack-plugin:test— 59 tests pass (incl. 4 newunstable_layeredOutputtests covering manifest emission, layered output for nested-pseudo + priority shorthands, mutual exclusion withunstable_attachToEntryPoint)@griffel/webpack-plugin:type-check— cleane2e/rspacksnapshot unchangedapps/chunking-reprobuilds in all three modes; layered mode produces 3 CSS files each starting with the global manifest, no placeholders remainingMigration
This ships as
unstable_layeredOutputfor one release cycle. Once feedback settles (especially the unlayered-vs-layered consumer trade-off and the mixed-mode caveat), promote to a stablelayeredOutputoption.🤖 Generated with Claude Code