Skip to content

Hoist statement at-rules to the front of the aggregated stylesheet - #614

Closed
davesnx wants to merge 3 commits into
parser-selector-gapsfrom
generate-statement-at-rules
Closed

davesnx wants to merge 3 commits into
parser-selector-gapsfrom
generate-statement-at-rules

Conversation

@davesnx

@davesnx davesnx commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Browsers honor @import, @namespace and statement-form @layer a, b; only when they precede every other rule, and styled-ppx.generate preserved module order, so a statement emitted by a module ordered late landed mid-stylesheet where browsers ignore it (#581). After dedup, run now partitions the rendered rules into statement at-rules and everything else and writes the statements first, in their existing relative order. A rule is a statement at-rule when it starts with @ and contains no {; Render keeps block rules on one line too, so the brace is what tells @media (...) { ... } apart. Under --layers the statements come before the @property/@keyframes registrations, the aggregator's own @layer <libraries>; statement, and every wrapped block. Dedup is unchanged.

@charset is dropped with a warning naming the input file instead of hoisted: the generated file always opens with a comment, so @charset can never be its first bytes, and the output is UTF-8 regardless of what a module declares. documents/css-extraction.md records both decisions.

$ styled-ppx.generate a_styles.ml z_statements.ml      # z_ orders after a_
styled-ppx: z_statements.ml: dropping @charset: ...
/* This file is generated by styled-ppx, do not edit manually */
@import url("reset.css");
@namespace svg url("http://www.w3.org/2000/svg");
@layer utilities, base;
.a{color:red;}

Evidence at 091beef:

  • New cram test packages/generate/test/statement-at-rules.t: two modules with no dependency edge, the statement module ordered second; covers the flat output and --layers. Red before the change: .a{color:red;} came first, @charset leaked through without a warning.
  • packages/ppx/test/css-support/at-rule-statement-forms.t: expectation updated for the @charset drop and its warning; no reordering in that single-module case.
  • make format-check, make build, make test: all exit 0.

Risk: two-way door. Output changes only for stylesheets that contain statement at-rules or @charset; a revert restores module order for them.

@vercel

vercel Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
styled-ppx Ignored Ignored Preview Sep 23, 2026 6:40am UTC

Request Review

@davesnx
davesnx force-pushed the parser-selector-gaps branch from 9adb516 to 751b12d Compare September 22, 2026 12:46
@davesnx
davesnx force-pushed the generate-statement-at-rules branch 2 times, most recently from 8475cec to e5ec9b6 Compare September 22, 2026 13:32
@davesnx
davesnx force-pushed the parser-selector-gaps branch 2 times, most recently from c69888c to 87f2f2e Compare September 23, 2026 06:11
@davesnx
davesnx force-pushed the generate-statement-at-rules branch from e5ec9b6 to 6274a2a Compare September 23, 2026 06:11
Browsers honor `@import`, `@namespace` and statement-form `@layer a, b;`
only when they precede every other rule, and dependency order routinely
places the module that emits one after modules with plain style rules.
After dedup, `run` partitions the rendered rules into statement at-rules
(start with `@`, contain no `{`) and everything else, and writes the
statements first in both the flat and the `--layers` output, ahead of the
`@property`/`@keyframes` registrations and the aggregator's own `@layer`
statement.

`@charset` is dropped with a warning naming the input file instead of
hoisted: the generated file always opens with a comment, so `@charset`
can never be its first bytes, and the output is UTF-8 regardless (#581).
Hoisting every @-rule without a `{` also moved statement-form `@layer a, b;`,
which changes layer order (first occurrence wins), let `@namespace` precede
`@import` against Cascade 5, and misread `@import url("a{b.css");` as a
block rule. Classify by prefix and trailing `;`, emit the `@import` block
first and the `@namespace` block second, each keeping its relative order,
and leave `@layer` statements where ordering placed them. Docs and CHANGES
follow.
@davesnx
davesnx force-pushed the parser-selector-gaps branch from 87f2f2e to 957d121 Compare September 23, 2026 06:39
@davesnx
davesnx force-pushed the generate-statement-at-rules branch from 6274a2a to 86e02dc Compare September 23, 2026 06:39
davesnx added a commit that referenced this pull request Sep 23, 2026
Browsers only honor `@import` and `@namespace` when they precede every other rule, and CSS Cascade 5 requires every `@import` to precede every `@namespace`, while dependency order routinely places a module that emits one after modules with plain style rules. styled-ppx.generate now hoists the `@import` block, then the `@namespace` block, each keeping its relative order. Rules are classified by their `@import`/`@namespace` prefix and trailing `;`, not by the absence of `{`, so `@import url("a{b.css");` is not mistaken for a block rule. Statement-form `@layer a, b;` stays where ordering placed it, because layer order is first occurrence and moving it would reorder a library's cascade layers. `@charset` is dropped with a warning naming the input file: the generated file always opens with its own comment.

Closes #581. Squashes the three commits of PR #614 (772faa2, d814af3, 86e02dc) onto this branch.
@davesnx davesnx closed this Sep 23, 2026
davesnx added a commit that referenced this pull request Sep 23, 2026
Browsers only honor `@import` and `@namespace` when they precede every other rule, and CSS Cascade 5 requires every `@import` to precede every `@namespace`, while dependency order routinely places a module that emits one after modules with plain style rules. styled-ppx.generate now hoists the `@import` block, then the `@namespace` block, each keeping its relative order. Rules are classified by their `@import`/`@namespace` prefix and trailing `;`, not by the absence of `{`, so `@import url("a{b.css");` is not mistaken for a block rule. Statement-form `@layer a, b;` stays where ordering placed it, because layer order is first occurrence and moving it would reorder a library's cascade layers. `@charset` is dropped with a warning naming the input file: the generated file always opens with its own comment.

Closes #581. Squashes the three commits of PR #614 (772faa2, d814af3, 86e02dc) onto this branch.
davesnx added a commit that referenced this pull request Sep 23, 2026
Browsers only honor `@import` and `@namespace` when they precede every other rule, and CSS Cascade 5 requires every `@import` to precede every `@namespace`, while dependency order routinely places a module that emits one after modules with plain style rules. styled-ppx.generate now hoists the `@import` block, then the `@namespace` block, each keeping its relative order. Rules are classified by their `@import`/`@namespace` prefix and trailing `;`, not by the absence of `{`, so `@import url("a{b.css");` is not mistaken for a block rule. Statement-form `@layer a, b;` stays where ordering placed it, because layer order is first occurrence and moving it would reorder a library's cascade layers. `@charset` is dropped with a warning naming the input file: the generated file always opens with its own comment.

Closes #581. Squashes the three commits of PR #614 (772faa2, d814af3, 86e02dc) onto this branch.
davesnx added a commit that referenced this pull request Sep 24, 2026
Browsers only honor `@import` and `@namespace` when they precede every other rule, and CSS Cascade 5 requires every `@import` to precede every `@namespace`, while dependency order routinely places a module that emits one after modules with plain style rules. styled-ppx.generate now hoists the `@import` block, then the `@namespace` block, each keeping its relative order. Rules are classified by their `@import`/`@namespace` prefix and trailing `;`, not by the absence of `{`, so `@import url("a{b.css");` is not mistaken for a block rule. Statement-form `@layer a, b;` stays where ordering placed it, because layer order is first occurrence and moving it would reorder a library's cascade layers. `@charset` is dropped with a warning naming the input file: the generated file always opens with its own comment.

Closes #581. Squashes the three commits of PR #614 (772faa2, d814af3, 86e02dc) onto this branch.
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