@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.
@@ -0,0 +1,468 @@
1
+ // Selector reading for the lightningcss pipeline: an AST walk that replaced the retired
2
+ // text-based `extractClassName` / `extractClassTokens` pair.
3
+ //
4
+ // That pair derived a registry key from the SELECTOR TEXT — it camelCased each class, collapsed
5
+ // every class in the selector into one concatenated key, and returned `null` for whatever its
6
+ // regexes failed to recognize. Each of those steps is lossy in a way that maps two different
7
+ // rules onto ONE key, and the later rule then overwrites the earlier per property, silently:
8
+ //
9
+ // .card-title{…} .cardTitle{…} -> both key `cardTitle` the first rule is GONE
10
+ // .card:hover{…} .card{…} -> both key `card` the :hover rule OVERWRITES the base
11
+ // .card[data-x]{…} -> key `card[dataX]` a key no element can ever carry
12
+ // .a.b / .a .b / .a>.b / .a+.b -> all key `aB` five selectors, one key, merged
13
+ //
14
+ // (Traps six and seven in `.claude/rules/style-registry-collisions.md`, measured 2026-08-20.)
15
+ //
16
+ // So this module reports what the selector ACTUALLY says and refuses to guess:
17
+ // - tokens stay AS AUTHORED — `card-title` is `card-title`. Casing is the caller's problem, and
18
+ // a caller that never re-cases can never collide two spellings onto one key.
19
+ // - the class list stays a LIST, with the combinator of every gap beside it, so a descendant
20
+ // rule is distinguishable from a compound one instead of both flattening to a concatenation.
21
+ // - a selector RN can never match (pseudo-class/-element, attribute, element, id, universal) is
22
+ // dropped WITH a reason and a warning, rather than silently degrading into a key that either
23
+ // collides with a real rule or matches nothing at all.
24
+ //
25
+ // The caller must pass `nonStandard: { deepSelectorCombinator: true }` to lightningcss's
26
+ // `transform`/`bundle` for a `deep` combinator to ever appear: without it `>>>` and `/deep/` are
27
+ // rejected at parse time as an invalid dangling combinator and the whole rule is lost before this
28
+ // walk sees it. That flag covers ONLY those two spellings — the other three deep forms are not
29
+ // lightningcss features at all and need no flag, so do not go re-reading its docs looking for
30
+ // them: `::v-deep` / `::ng-deep` arrive as an ordinary custom pseudo-element between two
31
+ // descendant combinators, and Vue's `:deep(X)` as a custom-function pseudo-class whose payload is
32
+ // a raw token stream — structurally the same thing `:global(X)` is under that same mode. All three
33
+ // are folded into `deep` here.
34
+ //
35
+ // `:global(X)` arrives in TWO DIFFERENT SHAPES and BOTH are live, so both are handled (measured
36
+ // 2026-08-20, lightningcss 1.32, same CSS through both modes):
37
+ //
38
+ // cssModules OFF {kind:'custom-function', name:'global', arguments:[…raw token stream…]}
39
+ // cssModules ON {kind:'global', selector:[…parsed SelectorComponent[]…]}
40
+ //
41
+ // A `.module.css` / scoped file runs WITH cssModules and gets the parsed form; the plain-`.css`
42
+ // pipeline runs WITHOUT it and gets the token stream. Handling only one of them silently drops the
43
+ // whole rule on the other — which is exactly how `:global(.reset)` in a `.module.css` came to
44
+ // contribute no rule and no export. `:deep()` does NOT have this split: lightningcss implements no
45
+ // `:deep()` at all, so it is the raw custom-function form under BOTH modes.
46
+ // Why the rule can never fire on a device, per reason — the half of the warning that tells an
47
+ // author what to do instead of just what was thrown away.
48
+ const DROP_EXPLANATION = {
49
+ root: 'a `:root` rule paints nothing — it exists to declare custom properties, which are collected by their own pass',
50
+ 'pseudo-class': 'React Native has no pseudo-class state (no hover/focus/nth-child)',
51
+ 'pseudo-element': 'React Native has no pseudo-elements',
52
+ attribute: 'React Native has no attribute selectors',
53
+ element: 'React Native has no element/tag selectors',
54
+ id: 'React Native has no id selectors',
55
+ universal: 'React Native has no universal selector',
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': '',
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;
82
+ const DEEP_PSEUDO_ELEMENTS = new Set(['v-deep', 'ng-deep']);
83
+ // Every `SelectorComponent['type']`, so the guard below rejects a shape lightningcss does not
84
+ // produce instead of trusting any object that happens to carry a `type` string.
85
+ const SELECTOR_COMPONENT_TYPES = new Set([
86
+ 'combinator',
87
+ 'universal',
88
+ 'namespace',
89
+ 'type',
90
+ 'id',
91
+ 'class',
92
+ 'attribute',
93
+ 'pseudo-class',
94
+ 'pseudo-element',
95
+ 'nesting',
96
+ ]);
97
+ function isRecord(value) {
98
+ return typeof value === 'object' && value !== null;
99
+ }
100
+ function isSelectorComponent(value) {
101
+ return (isRecord(value) &&
102
+ typeof value.type === 'string' &&
103
+ SELECTOR_COMPONENT_TYPES.has(value.type));
104
+ }
105
+ function isTokenOrValue(value) {
106
+ return isRecord(value) && typeof value.type === 'string';
107
+ }
108
+ /** The subset of lightningcss combinators this pipeline can act on; the rest drop the rule. */
109
+ function combinatorFor(value) {
110
+ switch (value) {
111
+ case 'descendant':
112
+ case 'child':
113
+ case 'next-sibling':
114
+ case 'later-sibling':
115
+ return value;
116
+ // `>>>` and `/deep/` differ only in spelling; a caller that treats them apart would be
117
+ // encoding CSS trivia, not a matching rule.
118
+ case 'deep-descendant':
119
+ case 'deep':
120
+ return 'deep';
121
+ default:
122
+ return null;
123
+ }
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';
129
+ function createBuilder() {
130
+ return {
131
+ tokens: [],
132
+ combinators: [],
133
+ specificity: [0, 0, 0],
134
+ pending: 'none',
135
+ drop: null,
136
+ };
137
+ }
138
+ function pushToken(builder, name) {
139
+ if (builder.tokens.length > 0)
140
+ builder.combinators.push(builder.pending);
141
+ builder.tokens.push(name);
142
+ builder.pending = 'none';
143
+ }
144
+ function setCombinator(builder, next) {
145
+ // A descendant never downgrades an explicit combinator already pending. lightningcss brackets
146
+ // `::v-deep` with a synthetic descendant on BOTH sides, and inside a `:global()` token stream
147
+ // `>` arrives surrounded by whitespace — in either case the second descendant would otherwise
148
+ // erase the real combinator the author wrote.
149
+ if (next === 'descendant' && builder.pending !== 'none')
150
+ return;
151
+ builder.pending = next;
152
+ }
153
+ function drop(builder, reason, detail) {
154
+ builder.drop ??= { reason, detail };
155
+ }
156
+ //#region custom-function payload token streams
157
+ /**
158
+ * Fold the payload of a custom-function pseudo-class into the builder as if its classes had been
159
+ * written bare. Serves both `:global(X)` and `:deep(X)` — lightningcss implements neither, so both
160
+ * arrive in the identical shape and one unwrapper covers them.
161
+ *
162
+ * This is the RAW-TOKEN half of `:global()`, which is the shape a PLAIN `.css` file produces —
163
+ * it is not run through cssModules, so `:global()` stays a `custom-function` pseudo-class whose
164
+ * arguments are an unparsed token stream (measured, lightningcss 1.32): `:global(.a > .b)` arrives
165
+ * as delim `.` · ident `a` · white-space · delim `>` · white-space · delim `.` · ident `b`.
166
+ * With cssModules ON the same source arrives as `kind:'global'` carrying a real parsed selector,
167
+ * handled where that kind is matched — `:global()` never disappears, it changes shape, and
168
+ * assuming the mode erased it is what silently killed the rule in every `.module.*` file.
169
+ *
170
+ * Neither wrapper changes which classes an element must carry — `:global()` only says a name lives
171
+ * outside the file's scope, `:deep()` only says the match may cross a scope boundary. So the
172
+ * payload participates exactly as if unwrapped, the rule `stripGlobalWrappers` follows in the text
173
+ * parser. `wrapper` is the authored spelling, used only so a drop warning names the right one.
174
+ */
175
+ function consumePayload(builder, args, wrapper) {
176
+ for (let index = 0; index < args.length; index++) {
177
+ const argument = args[index];
178
+ if (!isTokenOrValue(argument) || argument.type !== 'token') {
179
+ drop(builder, 'unsupported', wrapper);
180
+ return;
181
+ }
182
+ const token = argument.value;
183
+ switch (token.type) {
184
+ case 'white-space':
185
+ setCombinator(builder, 'descendant');
186
+ break;
187
+ case 'delim': {
188
+ if (token.value === '>') {
189
+ setCombinator(builder, 'child');
190
+ break;
191
+ }
192
+ if (token.value === '+') {
193
+ setCombinator(builder, 'next-sibling');
194
+ break;
195
+ }
196
+ if (token.value === '~') {
197
+ setCombinator(builder, 'later-sibling');
198
+ break;
199
+ }
200
+ if (token.value === '*') {
201
+ drop(builder, 'universal', '*');
202
+ break;
203
+ }
204
+ if (token.value !== '.') {
205
+ drop(builder, 'unsupported', `${wrapper.slice(0, -1)}${token.value})`);
206
+ break;
207
+ }
208
+ // A class is two tokens — delim `.` then the ident. A `.` with no ident behind it means
209
+ // the payload is malformed, and nothing about it is then trustworthy.
210
+ const name = identAt(args, index + 1);
211
+ if (name === null) {
212
+ drop(builder, 'unsupported', wrapper);
213
+ return;
214
+ }
215
+ builder.specificity[1]++;
216
+ pushToken(builder, name);
217
+ index++;
218
+ break;
219
+ }
220
+ case 'colon': {
221
+ // One colon is a pseudo-class, two a pseudo-element; either way the name is the ident
222
+ // after them, and reporting it is the difference between a warning an author can act on
223
+ // and one that only says ":global()" / ":deep()".
224
+ const isPseudoElement = isColonAt(args, index + 1);
225
+ const nameIndex = index + (isPseudoElement ? 2 : 1);
226
+ const name = identAt(args, nameIndex) ?? wrapper;
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
+ }
253
+ drop(builder, isPseudoElement ? 'pseudo-element' : 'pseudo-class', name);
254
+ index = nameIndex;
255
+ break;
256
+ }
257
+ case 'ident':
258
+ builder.specificity[2]++;
259
+ drop(builder, 'element', token.value);
260
+ break;
261
+ case 'id-hash':
262
+ builder.specificity[0]++;
263
+ drop(builder, 'id', `#${token.value}`);
264
+ break;
265
+ case 'square-bracket-block': {
266
+ // The parsed shape reports `[data-x]` by name, so the token stream reads the name too —
267
+ // otherwise the same CSS would warn differently depending on the cssModules flag. The
268
+ // block's remaining tokens are left to fall through: they can only add a second drop, and
269
+ // the first one already recorded is the one reported.
270
+ const name = identAt(args, index + 1);
271
+ builder.specificity[1]++;
272
+ drop(builder, 'attribute', name === null ? '[…]' : `[${name}]`);
273
+ break;
274
+ }
275
+ default:
276
+ drop(builder, 'unsupported', wrapper);
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ function identAt(args, index) {
282
+ const argument = args[index];
283
+ if (!isTokenOrValue(argument) || argument.type !== 'token')
284
+ return null;
285
+ return argument.value.type === 'ident' ? argument.value.value : null;
286
+ }
287
+ function isColonAt(args, index) {
288
+ const argument = args[index];
289
+ return (isTokenOrValue(argument) &&
290
+ argument.type === 'token' &&
291
+ argument.value.type === 'colon');
292
+ }
293
+ //#endregion custom-function payload token streams
294
+ function consumeComponent(builder, component) {
295
+ switch (component.type) {
296
+ case 'class':
297
+ builder.specificity[1]++;
298
+ pushToken(builder, component.name);
299
+ return;
300
+ case 'combinator': {
301
+ const combinator = combinatorFor(component.value);
302
+ if (combinator === null) {
303
+ drop(builder, 'unsupported', component.value);
304
+ return;
305
+ }
306
+ setCombinator(builder, combinator);
307
+ return;
308
+ }
309
+ case 'id':
310
+ builder.specificity[0]++;
311
+ drop(builder, 'id', `#${component.name}`);
312
+ return;
313
+ case 'type':
314
+ builder.specificity[2]++;
315
+ drop(builder, 'element', component.name);
316
+ return;
317
+ case 'universal':
318
+ drop(builder, 'universal', '*');
319
+ return;
320
+ case 'attribute':
321
+ builder.specificity[1]++;
322
+ drop(builder, 'attribute', `[${component.name}]`);
323
+ return;
324
+ case 'pseudo-class':
325
+ // Neither wrapper contributes specificity of its own — the payload's classes carry it.
326
+ // The parsed `:global(X)` of cssModules mode. Its payload is an ordinary component array, so
327
+ // the SAME walk reads it — which is also why a non-class payload drops with the payload's
328
+ // own reason here for free, identically to the token-stream branch below.
329
+ if (component.kind === 'global') {
330
+ consumeSelector(builder, component.selector);
331
+ return;
332
+ }
333
+ // `:root` is not state — it is where an author is SUPPOSED to declare custom properties, and
334
+ // `collectCustomProperties` has already read them by the time this runs. Reported with its
335
+ // own reason so the caller can stay silent about the ordinary token sheet and speak up only
336
+ // when the rule also carries a declaration that would have painted. Warning unconditionally
337
+ // meant every stylesheet with a `:root { --token: … }` block printed "it can never match",
338
+ // about the one construct the docs tell people to write.
339
+ if (component.kind === 'root') {
340
+ drop(builder, 'root', 'root');
341
+ return;
342
+ }
343
+ if (component.kind === 'custom-function') {
344
+ if (component.name === 'global') {
345
+ consumePayload(builder, component.arguments, ':global()');
346
+ return;
347
+ }
348
+ if (component.name === 'deep') {
349
+ // `:deep(X)` reaches THROUGH a scope boundary into X — the same relation `>>>` and
350
+ // `::v-deep` express, so the gap into X's own tokens is `deep`, overriding the
351
+ // descendant lightningcss reports just before it.
352
+ setCombinator(builder, 'deep');
353
+ consumePayload(builder, component.arguments, ':deep()');
354
+ return;
355
+ }
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
+ }
393
+ // `:not()`/`:is()` take the specificity of their argument rather than a flat 1, but they are
394
+ // dropped here regardless, and only a KEPT selector's specificity is ever read.
395
+ builder.specificity[1]++;
396
+ drop(builder, 'pseudo-class', component.kind === 'custom' || component.kind === 'custom-function'
397
+ ? component.name
398
+ : component.kind);
399
+ return;
400
+ case 'pseudo-element':
401
+ if (component.kind === 'custom' &&
402
+ DEEP_PSEUDO_ELEMENTS.has(component.name)) {
403
+ setCombinator(builder, 'deep');
404
+ return;
405
+ }
406
+ builder.specificity[2]++;
407
+ drop(builder, 'pseudo-element', component.kind === 'custom' || component.kind === 'custom-function'
408
+ ? component.name
409
+ : component.kind);
410
+ return;
411
+ // `namespace` (`ns|div`) and `nesting` (`&`) both need context this compiler does not have.
412
+ default:
413
+ drop(builder, 'unsupported', component.type);
414
+ return;
415
+ }
416
+ }
417
+ /**
418
+ * `selectors` is lightningcss's own `rule.value.selectors` (an array of selector-component
419
+ * arrays). Each entry is one comma-separated selector and is judged on its own: a list where some
420
+ * parts survive and some drop yields both a match and a drop.
421
+ */
422
+ function consumeSelector(builder, selector) {
423
+ for (const component of selector) {
424
+ if (!isSelectorComponent(component)) {
425
+ drop(builder, 'unsupported', String(component));
426
+ continue;
427
+ }
428
+ consumeComponent(builder, component);
429
+ }
430
+ }
431
+ export function selectorsToMatches(selectors, filename) {
432
+ if (!Array.isArray(selectors))
433
+ return { matches: [], dropped: [] };
434
+ const matches = [];
435
+ const dropped = [];
436
+ for (const selector of selectors) {
437
+ if (!Array.isArray(selector) || selector.length === 0)
438
+ continue;
439
+ const builder = createBuilder();
440
+ consumeSelector(builder, selector);
441
+ // No class survived and nothing explained why (a lone `::v-deep`, an empty compound) — there
442
+ // is still nothing to register, so say so rather than emitting a match with zero tokens.
443
+ if (builder.drop === null && builder.tokens.length === 0)
444
+ drop(builder, 'unsupported', 'no class selector');
445
+ const problem = builder.drop;
446
+ if (problem !== null) {
447
+ dropped.push(problem);
448
+ // `root` is returned, never announced from here — see DROP_EXPLANATION.root.
449
+ if (problem.reason === 'root')
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
+ }
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.`);
459
+ continue;
460
+ }
461
+ matches.push({
462
+ tokens: builder.tokens,
463
+ combinators: builder.combinators,
464
+ specificity: builder.specificity,
465
+ });
466
+ }
467
+ return { matches, dropped };
468
+ }
@@ -1,6 +1,34 @@
1
- import { type ICssParserOptions } from '../parser/index.ts';
1
+ import { type IStyleRule } from '../lightning/rules.ts';
2
2
  export interface ICompiledCssFile {
3
3
  code: string;
4
4
  }
5
5
  export declare function isCssModuleFile(filename: string): boolean;
6
- export declare function compileCssFile(source: string, filename: string, options?: ICssParserOptions): Promise<ICompiledCssFile>;
6
+ export type ICompiledCssModule = {
7
+ /** The rules, their tokens already carrying the scope. */
8
+ rules: readonly IStyleRule[];
9
+ /** The default export: authored name -> the token(s) to put in the markup. */
10
+ classMap: Record<string, string>;
11
+ };
12
+ /**
13
+ * One CSS source compiled as a CSS MODULE — a standalone `.module.*` file and a Vue
14
+ * `<style module>` block are the same thing and go through this, so neither can register a class
15
+ * under a name the other would not.
16
+ *
17
+ * The scope tail is our own `hashFilePath`, not lightningcss's `[hash]`: the runtime registry
18
+ * parses this tail to factor a scope back out (SCOPE_TAIL_PATTERN in
19
+ * core/engine/src/style-registry) and its alphabet is lowercase base36, which lightningcss's
20
+ * mixed-case hash does not fit — that would silently kill scoped-token base layering. Same hash
21
+ * the `<style scoped>` and Svelte scopers use, so all three scoping shapes stay one algorithm.
22
+ */
23
+ export declare function compileCssModule(css: string, filename: string): ICompiledCssModule;
24
+ /**
25
+ * The names a `.module.*` file's default export actually carries.
26
+ *
27
+ * The `.d.ts` generator MUST read them from here rather than re-deriving names off the raw
28
+ * source: what a rule MATCHES on and what the module EXPORTS are different sets. A compound
29
+ * rule `.card.big` matches on both its tokens, but only the names the author wrote as classes
30
+ * are exports. Typing off a re-derived set invents members that are `undefined` at runtime and
31
+ * hides valid ones behind a TS2339.
32
+ */
33
+ export declare function moduleClassNames(source: string, filename: string): Promise<string[]>;
34
+ export declare function compileCssFile(source: string, filename: string): Promise<ICompiledCssFile>;