@symbiote-native/css-parser 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -31
- package/build/generate-dts/index.js +3 -8
- package/build/generate-dts-cli.js +5 -1
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte +25 -0
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte.d.ts +3 -0
- package/build/index.d.ts +8 -7
- package/build/index.js +4 -4
- package/build/lightning/declarations.d.ts +46 -0
- package/build/lightning/declarations.js +885 -0
- package/build/lightning/rules.d.ts +52 -0
- package/build/lightning/rules.js +157 -0
- package/build/lightning/selectors.d.ts +23 -0
- package/build/lightning/selectors.js +468 -0
- package/build/metro-css-module/index.d.ts +30 -2
- package/build/metro-css-module/index.js +142 -36
- package/build/preprocessors/index.js +4 -3
- package/build/properties.d.ts +5 -6
- package/build/properties.js +26 -14
- package/build/scoped-classes.d.ts +23 -0
- package/build/scoped-classes.js +35 -0
- package/build/values.d.ts +3 -7
- package/build/values.js +3 -60
- package/package.json +13 -3
- package/typescript-plugin.cjs +21 -14
- package/build/global-selectors.d.ts +0 -25
- package/build/global-selectors.js +0 -98
- package/build/parser/index.d.ts +0 -59
- package/build/parser/index.js +0 -338
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CSSModuleExports } from 'lightningcss';
|
|
2
|
+
import { type IStyleObject } from './declarations.ts';
|
|
3
|
+
import { type ISelectorCombinator } from './selectors.ts';
|
|
4
|
+
export interface IStyleRule {
|
|
5
|
+
/** Class names as authored, then through {@link ICompiledCss.exports} when a pattern renamed them. */
|
|
6
|
+
readonly tokens: readonly string[];
|
|
7
|
+
readonly specificity: readonly [number, number, number];
|
|
8
|
+
/** Source order across the whole file, 0-based. Breaks a specificity tie, as the cascade does. */
|
|
9
|
+
readonly order: number;
|
|
10
|
+
readonly style: IStyleObject;
|
|
11
|
+
/**
|
|
12
|
+
* One per gap between tokens. Carried but NOT yet consumed: the registry matches a rule by token
|
|
13
|
+
* SUBSET, so `.a .b` still fires like `.a.b` today (trap 6). Stage 4 is where this starts to
|
|
14
|
+
* mean something — it needs parent pointers, which only exist after the engine's commit walk.
|
|
15
|
+
*/
|
|
16
|
+
readonly combinators: readonly ISelectorCombinator[];
|
|
17
|
+
}
|
|
18
|
+
export interface ICompileRulesOptions {
|
|
19
|
+
readonly filename: string;
|
|
20
|
+
/** Root font size for `rem`. */
|
|
21
|
+
readonly remToPx?: number;
|
|
22
|
+
/**
|
|
23
|
+
* A lightningcss CSS-Modules pattern, `[local]` first — `[local]__module__<hash>` for a
|
|
24
|
+
* `.module.*` file or a Vue `<style module>`, `[local]__data-v-<hash>` for a Vue `<style
|
|
25
|
+
* scoped>`, `[local]__svelte-<hash>` for a Svelte `<style>`. Omitted for a plain global `.css`.
|
|
26
|
+
*/
|
|
27
|
+
readonly pattern?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ICompiledCss {
|
|
30
|
+
readonly rules: readonly IStyleRule[];
|
|
31
|
+
/**
|
|
32
|
+
* Authored class name -> the name it registers under, straight out of lightningcss. Empty
|
|
33
|
+
* without a `pattern`. The markup rewriter reads THIS — never its own recomputation of the
|
|
34
|
+
* name, which is the bug class the single-renamer design exists to close.
|
|
35
|
+
*/
|
|
36
|
+
readonly exports: Readonly<Record<string, string>>;
|
|
37
|
+
/**
|
|
38
|
+
* The same map unflattened, kept because CSS Modules needs more than the new name: `composes`
|
|
39
|
+
* lives on the entry, and resolving a chain (`.a composes .b composes .c`) walks it. Empty
|
|
40
|
+
* without a `pattern`.
|
|
41
|
+
*/
|
|
42
|
+
readonly moduleExports: CSSModuleExports;
|
|
43
|
+
/**
|
|
44
|
+
* Class tokens the rules use that lightningcss did NOT rename. Under a `pattern` that means
|
|
45
|
+
* exactly one thing — the name came out of a `:global()` — so the escape hatch is DERIVED here
|
|
46
|
+
* rather than re-detected by walking selectors a second time (which is all
|
|
47
|
+
* a retired second selector walk ever was). Without a pattern nothing is renamed, so this is
|
|
48
|
+
* simply every token in the file, and no caller has a use for it.
|
|
49
|
+
*/
|
|
50
|
+
readonly globals: ReadonlySet<string>;
|
|
51
|
+
}
|
|
52
|
+
export declare function compileCssToRules(css: string, options: ICompileRulesOptions): ICompiledCss;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The glue of the lightningcss-typed pipeline: CSS text in, engine style RULES out. One
|
|
2
|
+
// `transform()` call feeds both halves — `./selectors.ts` for the selector, `./declarations.ts`
|
|
3
|
+
// for the values — and nothing in between re-derives a name from text.
|
|
4
|
+
//
|
|
5
|
+
// WHY a rule instead of a key. The retired text pipeline COLLAPSED a selector into one string
|
|
6
|
+
// key (`.a.b` -> `aB`, camelCased) and the runtime registry reversed that guess by
|
|
7
|
+
// permuting the element's class tokens. Every lossy step in the collapse mapped two different
|
|
8
|
+
// selectors onto one key and the later rule silently overwrote the earlier — the seventh trap in
|
|
9
|
+
// `.claude/rules/style-registry-collisions.md`. A rule carries the TOKEN SET the selector was
|
|
10
|
+
// written with, so the registry matches by subset and there is nothing to guess or reverse.
|
|
11
|
+
//
|
|
12
|
+
// Class names are emitted AS AUTHORED (`card-title` stays `card-title`, plus whatever the scoping
|
|
13
|
+
// rename appended). No camelCase anywhere in this file: normalization is what the trap was made of.
|
|
14
|
+
//
|
|
15
|
+
// Measured 2026-08-20 and the reason this is ONE pass, not two: with `cssModules` on, the visitor
|
|
16
|
+
// still sees the ORIGINAL class names — lightningcss renames AFTER the visitor walk. So the AST
|
|
17
|
+
// gives authored tokens and `exports` gives their renamed spelling, and the renamed CSS text never
|
|
18
|
+
// has to be parsed a second time (which is exactly what `../metro-css-module/index.ts` had to do,
|
|
19
|
+
// and what mangled a scope tail whose base36 hash began with a letter).
|
|
20
|
+
//
|
|
21
|
+
// `:global()` is the one thing the mode DOES change, and it changes shape rather than presence:
|
|
22
|
+
// with `cssModules` off it is a `custom-function` pseudo-class holding a raw token stream, with it
|
|
23
|
+
// on a `kind:'global'` pseudo-class holding a real parsed selector. `./selectors.ts` handles both;
|
|
24
|
+
// either way the tokens come out and `exports` not naming them is what marks them global.
|
|
25
|
+
import { transform } from 'lightningcss';
|
|
26
|
+
import { declarationToStyle, variablesIn, } from "./declarations.js";
|
|
27
|
+
import { selectorsToMatches } from "./selectors.js";
|
|
28
|
+
import { warnOnce } from "../values.js";
|
|
29
|
+
// A conditional at-rule is DROPPED WHOLE, its nested rules with it. There is no media-query engine
|
|
30
|
+
// in React Native, so applying `.responsive` from `@media (min-width: 900px)` would paint it on
|
|
31
|
+
// every phone — worse than not supporting the rule, because it looks supported. The retired text
|
|
32
|
+
// pass dropped these too, but silently: it simply never walked into an at-rule. Measured
|
|
33
|
+
// 2026-08-20: returning `[]` from the at-rule visitor removes it BEFORE the walk descends, so a
|
|
34
|
+
// nested style rule never reaches the collector; without it lightningcss hoists it out.
|
|
35
|
+
//
|
|
36
|
+
// `@keyframes` and `@font-face` need no entry here — neither emits a style rule to begin with.
|
|
37
|
+
const CONDITIONAL_AT_RULES = ['media', 'supports', 'container'];
|
|
38
|
+
// The wrappers `./selectors.ts` unwraps itself. lightningcss implements neither, so it reports both
|
|
39
|
+
// as an unsupported pseudo-class even though the rule survives.
|
|
40
|
+
const HANDLED_PSEUDO_CLASSES = new Set(['global', 'deep']);
|
|
41
|
+
function isHandledPseudoClass(value) {
|
|
42
|
+
if (typeof value !== 'object' || value === null)
|
|
43
|
+
return false;
|
|
44
|
+
const record = { ...value };
|
|
45
|
+
return (record.type === 'UnsupportedPseudoClass' &&
|
|
46
|
+
typeof record.value === 'string' &&
|
|
47
|
+
HANDLED_PSEUDO_CLASSES.has(record.value));
|
|
48
|
+
}
|
|
49
|
+
export function compileCssToRules(css, options) {
|
|
50
|
+
const { filename, pattern, remToPx } = options;
|
|
51
|
+
const collected = [];
|
|
52
|
+
const exports = {};
|
|
53
|
+
const warned = new Set();
|
|
54
|
+
const dropAtRule = (name) => () => {
|
|
55
|
+
warnOnce(warned, `at-rule:${name}`, `[@symbiote-native/css-parser] ${filename}: \`@${name}\` is not supported — the rules ` +
|
|
56
|
+
'inside it are dropped. React Native evaluates no CSS condition at all; branch in JS ' +
|
|
57
|
+
'(useWindowDimensions, Platform) instead.');
|
|
58
|
+
return [];
|
|
59
|
+
};
|
|
60
|
+
const result = transform({
|
|
61
|
+
filename,
|
|
62
|
+
code: Buffer.from(css),
|
|
63
|
+
// A rule lightningcss cannot use must not take the whole file down: the retired text pass
|
|
64
|
+
// never threw either, and a stylesheet is authored content, not our code.
|
|
65
|
+
errorRecovery: true,
|
|
66
|
+
// Without this `>>>` / `/deep/` die at parse as a dangling combinator, so the selector half
|
|
67
|
+
// never gets the chance to fold them. Inert until stage 4 consumes a combinator.
|
|
68
|
+
nonStandard: { deepSelectorCombinator: true },
|
|
69
|
+
...(pattern === undefined ? {} : { cssModules: { pattern } }),
|
|
70
|
+
visitor: {
|
|
71
|
+
Rule: {
|
|
72
|
+
...Object.fromEntries(CONDITIONAL_AT_RULES.map(name => [name, dropAtRule(name)])),
|
|
73
|
+
style(rule) {
|
|
74
|
+
// Every reason but `root` is announced by `selectorsToMatches` itself, with the
|
|
75
|
+
// explanation spelled out for the author — warning again from this level printed the
|
|
76
|
+
// same drop twice. `root` is the exception it hands back silently, because only here
|
|
77
|
+
// are the declarations in scope, and a `:root` block is worth a word ONLY when it
|
|
78
|
+
// carries something that would have painted. Custom properties are its whole purpose
|
|
79
|
+
// and are already collected by `collectCustomProperties`.
|
|
80
|
+
const { matches, dropped } = selectorsToMatches(rule.value.selectors, filename);
|
|
81
|
+
// `importantDeclarations` last: `!important` wins inside its own rule, and a later
|
|
82
|
+
// entry overwrites an earlier one when both name the same RN property.
|
|
83
|
+
const declarations = [
|
|
84
|
+
...rule.value.declarations.declarations,
|
|
85
|
+
...rule.value.declarations.importantDeclarations,
|
|
86
|
+
];
|
|
87
|
+
if (dropped.some(entry => entry.reason === 'root') &&
|
|
88
|
+
declarations.some(declaration => declaration.property !== 'custom')) {
|
|
89
|
+
warnOnce(warned, `root-paints:${filename}`, `[@symbiote-native/css-parser] ${filename}: a \`:root\` rule declares more than ` +
|
|
90
|
+
'custom properties — everything else in it is dropped, because `:root` matches no ' +
|
|
91
|
+
'node in React Native. Move those declarations onto a class.');
|
|
92
|
+
}
|
|
93
|
+
for (const match of matches) {
|
|
94
|
+
collected.push({ ...match, declarations });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
// `errorRecovery` keeps a malformed rule from taking the file down, but silence is how a whole
|
|
101
|
+
// rule disappears unnoticed: `examples/angular/**/ApiPlaygroundScreen.css` had a comment closed
|
|
102
|
+
// early by a `.hero-*/` inside it, which swept the real class after it into a garbage selector.
|
|
103
|
+
// The retired text pass "recovered" by registering the garbage; this one drops it — either way
|
|
104
|
+
// the author has to be told.
|
|
105
|
+
//
|
|
106
|
+
// Two things this loop must NOT do. It must not claim an outcome — a SelectorError drops the
|
|
107
|
+
// rule but a value-level one drops only the declaration, and asserting the wrong one is worse
|
|
108
|
+
// than staying quiet about it. And it must not repeat a complaint we deliberately answer:
|
|
109
|
+
// lightningcss does not implement `:global()` or `:deep()`, so it warns "not recognized as a
|
|
110
|
+
// valid pseudo-class" for both while `./selectors.ts` unwraps them and keeps the rule. Passing
|
|
111
|
+
// that through told the author their working rule was thrown away.
|
|
112
|
+
for (const warning of result.warnings) {
|
|
113
|
+
if (isHandledPseudoClass(warning.value))
|
|
114
|
+
continue;
|
|
115
|
+
const where = `${warning.loc.line}:${warning.loc.column}`;
|
|
116
|
+
warnOnce(warned, `lightningcss:${warning.type}:${warning.message}`, `[@symbiote-native/css-parser] ${filename}:${where}: ${warning.message}`);
|
|
117
|
+
}
|
|
118
|
+
// Sorted: `exports` is a Rust HashMap whose iteration order is randomized PER PROCESS, and this
|
|
119
|
+
// map is emitted into a generated module — an unsorted one churns Metro's content cache.
|
|
120
|
+
for (const [local, entry] of Object.entries(result.exports ?? {}).sort(([left], [right]) => (left < right ? -1 : 1))) {
|
|
121
|
+
exports[local] = entry.name;
|
|
122
|
+
}
|
|
123
|
+
// Custom properties come from their own pass because nothing guarantees `:root` is declared
|
|
124
|
+
// before the rule that reads it — the retired text pass had the same two-phase shape.
|
|
125
|
+
const variables = variablesIn(css, filename);
|
|
126
|
+
const context = {
|
|
127
|
+
filename,
|
|
128
|
+
variables,
|
|
129
|
+
...(remToPx === undefined ? {} : { remToPx }),
|
|
130
|
+
};
|
|
131
|
+
const globals = new Set();
|
|
132
|
+
const rules = collected.map((rule, order) => ({
|
|
133
|
+
tokens: rule.tokens.map(token => {
|
|
134
|
+
const renamed = exports[token];
|
|
135
|
+
if (renamed === undefined)
|
|
136
|
+
globals.add(token);
|
|
137
|
+
return renamed ?? token;
|
|
138
|
+
}),
|
|
139
|
+
specificity: rule.specificity,
|
|
140
|
+
combinators: rule.combinators,
|
|
141
|
+
order,
|
|
142
|
+
style: rule.declarations.reduce((style, declaration) => ({
|
|
143
|
+
...style,
|
|
144
|
+
...declarationToStyle(declaration, context),
|
|
145
|
+
}), {}),
|
|
146
|
+
}));
|
|
147
|
+
// A rule whose every declaration was dropped — all unsupported, or all `var()`s declared in
|
|
148
|
+
// another file — has nothing to contribute, and keeping it only adds a candidate the registry
|
|
149
|
+
// must consider on every lookup of those tokens.
|
|
150
|
+
const painting = rules.filter(rule => Object.keys(rule.style).length > 0);
|
|
151
|
+
return {
|
|
152
|
+
rules: painting,
|
|
153
|
+
exports,
|
|
154
|
+
moduleExports: result.exports ?? {},
|
|
155
|
+
globals,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ISelectorCombinator = 'none' | 'descendant' | 'child' | 'next-sibling' | 'later-sibling' | 'deep';
|
|
2
|
+
export interface ISelectorMatch {
|
|
3
|
+
/** Class names AS AUTHORED — NO camelCase, NO collapsing, in source order. */
|
|
4
|
+
readonly tokens: readonly string[];
|
|
5
|
+
/** CSS specificity (a,b,c) = (id, class/attr/pseudo-class, element/pseudo-element). */
|
|
6
|
+
readonly specificity: readonly [number, number, number];
|
|
7
|
+
/** The combinators between the tokens, one per gap (length === tokens.length - 1). */
|
|
8
|
+
readonly combinators: readonly ISelectorCombinator[];
|
|
9
|
+
}
|
|
10
|
+
export interface IDroppedSelector {
|
|
11
|
+
readonly reason: 'root' | 'pseudo-class' | 'pseudo-element' | 'attribute' | 'element' | 'id' | 'universal' | 'unsupported' | 'state-pseudo-class';
|
|
12
|
+
/** e.g. 'hover', '[data-x]', 'div'. */
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ISelectorResult {
|
|
16
|
+
/** One per comma-separated selector that is usable. */
|
|
17
|
+
readonly matches: readonly ISelectorMatch[];
|
|
18
|
+
/** The rest, with a reason. */
|
|
19
|
+
readonly dropped: readonly IDroppedSelector[];
|
|
20
|
+
}
|
|
21
|
+
export declare const IS_STATE_TOKEN_ENABLED: boolean;
|
|
22
|
+
export declare const STATE_TOKEN = ":active";
|
|
23
|
+
export declare function selectorsToMatches(selectors: unknown, filename: string): ISelectorResult;
|