@symbiote-native/css-parser 0.4.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 +14 -10
- package/build/lightning/selectors.d.ts +3 -1
- package/build/lightning/selectors.js +96 -0
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -16,9 +16,10 @@ CSS Modules all work identically regardless of source language.
|
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
|
-
Not installed directly by an app —
|
|
20
|
-
`@symbiote-native/vue`, `@symbiote-native/
|
|
21
|
-
|
|
19
|
+
Not installed directly by an app — every adapter package (`@symbiote-native/react`,
|
|
20
|
+
`@symbiote-native/vue`, `@symbiote-native/svelte`, `@symbiote-native/solid`,
|
|
21
|
+
`@symbiote-native/angular`) already depends on it and re-exports it via its own
|
|
22
|
+
`./metro-css-parser` subpath. Writing a Metro transformer for a new adapter yourself:
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
npm install @symbiote-native/css-parser
|
|
@@ -27,10 +28,11 @@ npm install @symbiote-native/css-parser
|
|
|
27
28
|
## Who calls this, and how
|
|
28
29
|
|
|
29
30
|
**An app never imports this package directly.** It runs only inside a Metro transformer, on the
|
|
30
|
-
Node build machine — never shipped in the app's native JS bundle.
|
|
31
|
-
(`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
Node build machine — never shipped in the app's native JS bundle. Every adapter package
|
|
32
|
+
(`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/svelte`,
|
|
33
|
+
`@symbiote-native/solid`, `@symbiote-native/angular`) depends on `@symbiote-native/css-parser` as a
|
|
34
|
+
regular dependency and re-exports it via its own `./metro-css-parser` subpath, so a consuming app's
|
|
35
|
+
`metro.config.js` wires:
|
|
34
36
|
|
|
35
37
|
```js
|
|
36
38
|
// metro.config.js
|
|
@@ -133,11 +135,12 @@ import {
|
|
|
133
135
|
- It does not implement Tailwind CSS — that needs whole-project class scanning and JIT utility
|
|
134
136
|
generation, a fundamentally different shape than "one source file reduces to CSS text", and is
|
|
135
137
|
being designed as a separate, future package.
|
|
136
|
-
- It supports `scoped` / `:global()` / CSS Modules and SCSS/Sass/Less/Stylus preprocessing
|
|
138
|
+
- It supports `scoped` / `:global()` / CSS Modules and SCSS/Sass/Less/Stylus preprocessing —
|
|
139
|
+
including Svelte's own `<style>` block (its preprocessor calls this package's
|
|
140
|
+
`compileScopedCss`, the same compile every Vue `<style scoped>` block goes through) — and it does
|
|
137
141
|
not yet generate a typed `.d.ts` for an **inline** Vue `<style module>` block (only standalone
|
|
138
142
|
`.module.css` files get the strict, no-index-signature type — Vue's own Volar plugin gives inline
|
|
139
|
-
blocks a looser, typo-tolerant type for free)
|
|
140
|
-
exists in SymbioteNative today).
|
|
143
|
+
blocks a looser, typo-tolerant type for free).
|
|
141
144
|
|
|
142
145
|
## Related packages
|
|
143
146
|
|
|
@@ -145,6 +148,7 @@ import {
|
|
|
145
148
|
`resolveClassName`) this package's compiled output resolves against, and the class+style merge
|
|
146
149
|
used by every adapter.
|
|
147
150
|
- [`@symbiote-native/react`](../../adapters/react) / [`@symbiote-native/vue`](../../adapters/vue) /
|
|
151
|
+
[`@symbiote-native/svelte`](../../adapters/svelte) / [`@symbiote-native/solid`](../../adapters/solid) /
|
|
148
152
|
[`@symbiote-native/angular`](../../adapters/angular) — each depends on this package directly and
|
|
149
153
|
re-exports it via its own `./metro-css-parser` subpath, so a consuming app needs no extra install
|
|
150
154
|
step.
|
|
@@ -8,7 +8,7 @@ export interface ISelectorMatch {
|
|
|
8
8
|
readonly combinators: readonly ISelectorCombinator[];
|
|
9
9
|
}
|
|
10
10
|
export interface IDroppedSelector {
|
|
11
|
-
readonly reason: 'root' | 'pseudo-class' | 'pseudo-element' | 'attribute' | 'element' | 'id' | 'universal' | 'unsupported';
|
|
11
|
+
readonly reason: 'root' | 'pseudo-class' | 'pseudo-element' | 'attribute' | 'element' | 'id' | 'universal' | 'unsupported' | 'state-pseudo-class';
|
|
12
12
|
/** e.g. 'hover', '[data-x]', 'div'. */
|
|
13
13
|
readonly detail: string;
|
|
14
14
|
}
|
|
@@ -18,4 +18,6 @@ export interface ISelectorResult {
|
|
|
18
18
|
/** The rest, with a reason. */
|
|
19
19
|
readonly dropped: readonly IDroppedSelector[];
|
|
20
20
|
}
|
|
21
|
+
export declare const IS_STATE_TOKEN_ENABLED: boolean;
|
|
22
|
+
export declare const STATE_TOKEN = ":active";
|
|
21
23
|
export declare function selectorsToMatches(selectors: unknown, filename: string): ISelectorResult;
|
|
@@ -54,7 +54,31 @@ const DROP_EXPLANATION = {
|
|
|
54
54
|
id: 'React Native has no id selectors',
|
|
55
55
|
universal: 'React Native has no universal selector',
|
|
56
56
|
unsupported: 'this selector shape has no React Native equivalent',
|
|
57
|
+
// Never reaches the shared "can never match" sentence — it has its own branch at the warn site.
|
|
58
|
+
'state-pseudo-class': '',
|
|
57
59
|
};
|
|
60
|
+
// `:active` support is OFF, and the selector machinery below is kept intact so one line turns it
|
|
61
|
+
// back on.
|
|
62
|
+
//
|
|
63
|
+
// WHY. The pressed look has a second, better route that did not exist when `:active` landed: a
|
|
64
|
+
// functional `style={({pressed}) => …}`, which every lowering transform specialises into
|
|
65
|
+
// `style` + `activeStyle` at build time (2026-08-23). It reaches the same slot with no pseudo-class
|
|
66
|
+
// machinery, it is what the ecosystem already writes, and it lowers — so the reason `:active`
|
|
67
|
+
// existed, keeping a Pressable lowerable without a state-reading callback, is gone.
|
|
68
|
+
//
|
|
69
|
+
// Keeping BOTH live is what argues against it: they occupy different cascade slots (`activeStyle`
|
|
70
|
+
// replaces the authored style, an `:active` class rule replaces the class style), so an adapter has
|
|
71
|
+
// two ways to say one thing and a debugging session has two places to look. This is also part of a
|
|
72
|
+
// larger pseudo-class-state feature that is not built; shipping a fragment of it invites code that
|
|
73
|
+
// depends on the fragment.
|
|
74
|
+
//
|
|
75
|
+
// The engine side is deliberately untouched: with no `:active` rule ever registered,
|
|
76
|
+
// `hasActiveRules` stays false and `resolveActiveClassName` is never called, so the path costs
|
|
77
|
+
// nothing while it waits.
|
|
78
|
+
// Typed `boolean`, not inferred as the literal `false`: the literal would let tsc prune the
|
|
79
|
+
// enabled branch of `selectors.test.ts` as unreachable, and that branch is the recorded contract
|
|
80
|
+
// the flag restores — pruning it is how the dormant half rots unnoticed.
|
|
81
|
+
export const IS_STATE_TOKEN_ENABLED = false;
|
|
58
82
|
const DEEP_PSEUDO_ELEMENTS = new Set(['v-deep', 'ng-deep']);
|
|
59
83
|
// Every `SelectorComponent['type']`, so the guard below rejects a shape lightningcss does not
|
|
60
84
|
// produce instead of trusting any object that happens to carry a `type` string.
|
|
@@ -98,6 +122,10 @@ function combinatorFor(value) {
|
|
|
98
122
|
return null;
|
|
99
123
|
}
|
|
100
124
|
}
|
|
125
|
+
// lightningcss reports a plain state pseudo-class by name in `kind`. The TOKEN keeps the colon so
|
|
126
|
+
// it stays unspellable as a class name.
|
|
127
|
+
const STATE_PSEUDO_CLASS = 'active';
|
|
128
|
+
export const STATE_TOKEN = ':active';
|
|
101
129
|
function createBuilder() {
|
|
102
130
|
return {
|
|
103
131
|
tokens: [],
|
|
@@ -197,6 +225,31 @@ function consumePayload(builder, args, wrapper) {
|
|
|
197
225
|
const nameIndex = index + (isPseudoElement ? 2 : 1);
|
|
198
226
|
const name = identAt(args, nameIndex) ?? wrapper;
|
|
199
227
|
builder.specificity[isPseudoElement ? 2 : 1]++;
|
|
228
|
+
// `:active` is kept on this path too, or the SAME CSS behaves differently on the
|
|
229
|
+
// cssModules flag — the recurring hazard this file's header names. `:global(.btn:active)`
|
|
230
|
+
// arrives parsed (`kind:'global'`) with cssModules ON and reaches the ordinary walk, which
|
|
231
|
+
// keeps it; with the flag OFF it arrives here as a raw token stream and would drop the
|
|
232
|
+
// WHOLE rule. Same source, opposite outcome, decided by which file it lives in.
|
|
233
|
+
//
|
|
234
|
+
// Keeping it is also the right side of the `:deep` asymmetry rather than an exception to
|
|
235
|
+
// it: `:global()` says only that the NAME lives outside this file's scope, while
|
|
236
|
+
// `:deep()` says the MATCH may cross a scope boundary. Only the second breaks the promise
|
|
237
|
+
// the state token rests on — that the rule targets the node whose press machine owns the
|
|
238
|
+
// state. Svelte cares most, since `:global()` is its ONLY escape hatch.
|
|
239
|
+
if (!isPseudoElement && name === STATE_PSEUDO_CLASS) {
|
|
240
|
+
if (builder.combinators.includes('deep') ||
|
|
241
|
+
builder.pending === 'deep') {
|
|
242
|
+
drop(builder, 'pseudo-class', 'active through a deep combinator');
|
|
243
|
+
}
|
|
244
|
+
else if (IS_STATE_TOKEN_ENABLED) {
|
|
245
|
+
pushToken(builder, STATE_TOKEN);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
drop(builder, 'state-pseudo-class', 'active');
|
|
249
|
+
}
|
|
250
|
+
index = nameIndex;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
200
253
|
drop(builder, isPseudoElement ? 'pseudo-element' : 'pseudo-class', name);
|
|
201
254
|
index = nameIndex;
|
|
202
255
|
break;
|
|
@@ -301,6 +354,42 @@ function consumeComponent(builder, component) {
|
|
|
301
354
|
return;
|
|
302
355
|
}
|
|
303
356
|
}
|
|
357
|
+
// `:active` is the ONE state pseudo-class this module keeps, and it is not an exception to
|
|
358
|
+
// the "a selector RN can never match" principle above — it is the one state the ENGINE
|
|
359
|
+
// actually knows, because the press machine owns it. It is kept as an ordinary compound
|
|
360
|
+
// TOKEN (`.btn:active` -> tokens ['btn', ':active'], combinator 'none'), so specificity,
|
|
361
|
+
// source order, scoping and the resolve cache all keep working with no new concept in the
|
|
362
|
+
// registry: the engine adds the token to a pressed node's class list and the existing
|
|
363
|
+
// matcher does the rest. A CSS identifier cannot carry an unescaped `:`, so the token can
|
|
364
|
+
// never collide with a real class name.
|
|
365
|
+
//
|
|
366
|
+
// `:hover` / `:focus` stay dropped deliberately — RN has no hover or focus state the engine
|
|
367
|
+
// owns — as do `:nth-child` and the rest. Widening past `:active` is its own decision, and
|
|
368
|
+
// the limit it will hit is the registry's resolve cache: it is capped at 512 entries and
|
|
369
|
+
// CLEARS WHOLE rather than evicting, so states that COMBINE multiply the distinct class
|
|
370
|
+
// strings on a screen and can drop the cache, which breaks the identity `isAlreadyPublished`
|
|
371
|
+
// depends on for every node at once.
|
|
372
|
+
if (component.kind === STATE_PSEUDO_CLASS) {
|
|
373
|
+
builder.specificity[1]++;
|
|
374
|
+
// ...but NOT through a scope boundary. `:deep(.b:active)` already dropped, because a
|
|
375
|
+
// custom-function payload is a raw token stream this walk re-parses; `.a >>> .b:active`
|
|
376
|
+
// did NOT, because `>>>` is a real combinator and the walk reaches the pseudo-class
|
|
377
|
+
// normally. Two spellings of one relation behaving differently is the bug, and the
|
|
378
|
+
// decision (2026-08-23) is to refuse BOTH: a deep selector reaches into another
|
|
379
|
+
// component's internals, and the state token is only meaningful on the node whose press
|
|
380
|
+
// machine owns it — which is exactly the node a deep rule cannot predict.
|
|
381
|
+
if (builder.combinators.includes('deep') ||
|
|
382
|
+
builder.pending === 'deep') {
|
|
383
|
+
drop(builder, 'pseudo-class', 'active through a deep combinator');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!IS_STATE_TOKEN_ENABLED) {
|
|
387
|
+
drop(builder, 'state-pseudo-class', 'active');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
pushToken(builder, STATE_TOKEN);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
304
393
|
// `:not()`/`:is()` take the specificity of their argument rather than a flat 1, but they are
|
|
305
394
|
// dropped here regardless, and only a KEPT selector's specificity is ever read.
|
|
306
395
|
builder.specificity[1]++;
|
|
@@ -359,6 +448,13 @@ export function selectorsToMatches(selectors, filename) {
|
|
|
359
448
|
// `root` is returned, never announced from here — see DROP_EXPLANATION.root.
|
|
360
449
|
if (problem.reason === 'root')
|
|
361
450
|
continue;
|
|
451
|
+
// Its own sentence, because the shared one below ends in "can never match in React Native"
|
|
452
|
+
// and that is FALSE here — the pressed look is fully supported, by a different route. A
|
|
453
|
+
// warning that misdescribes the cause is worse than none: it sends the reader to the engine.
|
|
454
|
+
if (problem.reason === 'state-pseudo-class') {
|
|
455
|
+
console.warn(`[@symbiote-native/css-parser] ${filename}: dropped \`:${problem.detail}\` — pseudo-class state is currently disabled in this parser. Use a functional style instead: style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}, which every lowering transform compiles to style + activeStyle.`);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
362
458
|
console.warn(`[@symbiote-native/css-parser] ${filename}: dropped a rule on \`${problem.detail}\` — ${DROP_EXPLANATION[problem.reason]}, so it can never match in React Native.`);
|
|
363
459
|
continue;
|
|
364
460
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/css-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Build-time CSS/SCSS/Less/Stylus compiler for SymbioteNative — compiles stylesheets to React Native style objects, resolved at runtime via a cross-adapter class-name registry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
"main": "./build/index.js",
|
|
18
18
|
"module": "./build/index.js",
|
|
19
19
|
"types": "./build/index.d.ts",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"react-native",
|
|
22
|
+
"symbiote-native",
|
|
23
|
+
"css",
|
|
24
|
+
"scss",
|
|
25
|
+
"css-modules",
|
|
26
|
+
"postcss",
|
|
27
|
+
"lightningcss",
|
|
28
|
+
"metro",
|
|
29
|
+
"build-tool"
|
|
30
|
+
],
|
|
20
31
|
"exports": {
|
|
21
32
|
".": {
|
|
22
33
|
"types": "./build/index.d.ts",
|