Skip to content

feat(core): support CSS cascade layers (@layer) - #11462

Open
aleclarson wants to merge 2 commits into
NativeScript:mainfrom
aleclarson:feat/css-cascade-layers
Open

aleclarson wants to merge 2 commits into
NativeScript:mainfrom
aleclarson:feat/css-cascade-layers

Conversation

@aleclarson

@aleclarson aleclarson commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

What is the current behavior?

Rules inside @layer blocks are silently dropped: _populateRules (ui/styling/style-scope.ts) only descends into @media at-rules, so every modern build-time CSS pipeline that emits cascade layers (Tailwind v4, Panda CSS, Bamboo, UnoCSS) produces a stylesheet that is dead on NativeScript — see NativeScript/tailwind#197 for the same wall hit from the Tailwind side.

What is the new behavior?

@layer applies per CSS Cascading and Inheritance Level 5:

  • Parsers:
    • css/css-tree-parser.ts (default __CSS_PARSER__) — @layer nodes keep their prelude (layer name list) alongside nested rules; the statement form (@layer a, b;) parses with no block.
    • css/lib/parse (vendored rework parser) — same support, which also fixes the legacy __CSS_PARSER__=rework runtime path and gives the bundler-side serializers a layer-aware parser to converge on.
  • Ordering (ui/styling/style-scope.ts): a name→ordinal registry shared across all stylesheets feeding the cascade (document-wide, per spec). Statement form pre-declares order; block form registers on first sight; dotted names (a.b) walk ancestors so a top-level @layer a.b refers to the same layer as @layer a { @layer b { … } }; anonymous blocks get synthesized names that can't be re-referenced.
  • Cascade (ui/styling/css-selector.ts): each RuleSet carries layerPath: number[]; compareSelectors compares layer paths before specificity. Unlayered rules carry an infinite rank at every depth, so they always beat layered rules — matching the spec, which also handles nested-layer comparison lexicographically.
  • Imports: @import "x.css" layer(name) (and bare layer for anonymous) nests the imported sheet under that layer.

Deliberately not implemented: !important layer-order reversal — NativeScript does not implement !important at all today (cleanupImportantFlags warns and strips), so there is nothing to reverse. @supports/@container/@scope remain dropped (they need real condition evaluation; dropping is safer than unconditional application).

Known follow-up (out of scope here)

The bundler-side serializers — @nativescript/vite's bundled-css path (helpers/main-entry.ts, parseCssToAst) and webpack's css2json-loader — run the npm css package (reworkcss/css), which misparses @layer into bogus selectors and drops the rules. That package is a separate upstream; swapping those call sites to core's now-layer-aware vendored parser (or css-tree) is the natural follow-up.

Tests

New ui/styling/css-layer.spec.ts — 13 cases covering population, statement ordering over specificity+position, unlayered-beats-layered, nested/dotted equivalence, anonymous ordering, media queries inside layers, keyframes inside layers, and the same ordering driven through the rework-parser AST end to end. Verified on-device (iOS sim) via a harness probe: a class rule in the later layer beat an id selector in the earlier one, and an unlayered rule beat a layered id selector. Full packages/core suite: 510 tests passing locally.

Implements #11461.

Rules inside @layer blocks were silently dropped by _populateRules,
which only descends into @media. This makes NativeScript unable to
consume stylesheets emitted by modern build-time CSS compilers
(Tailwind v4, Panda, Bamboo — all emit cascade layers).

- css-tree adapter captures the @layer prelude and nested rules
- _populateRules assigns each ruleset a layerPath of first-seen
  ordinals; statement form `@layer a, b;` pre-declares order, dotted
  names resolve through ancestors, anonymous blocks get synthetic ids
- compareSelectors orders by layerPath before specificity, with
  unlayered rules sorting last (winning) per spec
- @import "x.css" layer(name) nests the imported sheet's rules
@nx-cloud

nx-cloud Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 4182f37

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 52s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded 7s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-09-28 05:07:31 UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11462
npm i https://pkg.pr.new/@nativescript/vite@11462
npm i https://pkg.pr.new/@nativescript/webpack@11462

commit: 4182f37

The build-time css→AST serialization (webpack css2json-loader, vite's
bundled-css serializer in @nativescript/vite helpers/main-entry) runs the
rework parser, which mis-parsed `@layer` into garbage selectors and
dropped the rules. Teach it both forms — `@layer name { rules }` and the
order-declaring `@layer a, b;` statement — emitting layer nodes the
cascade now consumes. Also fixes the legacy __CSS_PARSER__=rework
runtime path.

Spec drives the same ordering through the rework AST end to end.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant