octane 0.2.2 → 0.2.4

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.
Files changed (59) hide show
  1. package/dist/cjs/compatibility.cjs +35 -0
  2. package/dist/cjs/component-flags.cjs +8 -1
  3. package/dist/cjs/css.cjs +9 -0
  4. package/dist/cjs/dom-tables.cjs +8 -1
  5. package/dist/cjs/error-codes.client.generated.cjs +15 -2
  6. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  7. package/dist/cjs/event-names.cjs +39 -0
  8. package/dist/cjs/index.cjs +13 -0
  9. package/dist/cjs/runtime.cjs +1345 -359
  10. package/dist/cjs/runtime.server.cjs +620 -183
  11. package/dist/cjs/server/index.cjs +26 -0
  12. package/dist/cjs/version.cjs +1 -1
  13. package/dist/compatibility.d.ts +7 -0
  14. package/dist/compatibility.js +10 -0
  15. package/dist/compiler/bundler.js +20 -30
  16. package/dist/compiler/compile-renderer-boundaries.js +1 -0
  17. package/dist/compiler/compile-universal.js +3 -0
  18. package/dist/compiler/compile.js +877 -627
  19. package/dist/compiler/hook-deps.js +100 -3
  20. package/dist/compiler/hook-names.js +1 -0
  21. package/dist/compiler/hydrate-boundaries.js +165 -15
  22. package/dist/compiler/manual-hooks.js +171 -0
  23. package/dist/compiler/parser.node.js +34 -8
  24. package/dist/compiler/plain-hook-memo.js +54 -5
  25. package/dist/compiler/slot-hooks.d.ts +43 -0
  26. package/dist/compiler/slot-hooks.js +154 -35
  27. package/dist/compiler/strong-mode.js +836 -3
  28. package/dist/compiler/style-scopes.js +1134 -0
  29. package/dist/compiler/volar.LICENSES.txt +1 -1
  30. package/dist/compiler/volar.js +4543 -2839
  31. package/dist/component-flags.d.ts +1 -1
  32. package/dist/component-flags.js +8 -1
  33. package/dist/css.d.ts +7 -0
  34. package/dist/css.js +8 -0
  35. package/dist/dom-tables.js +8 -1
  36. package/dist/error-codes.client.generated.d.ts +3 -1
  37. package/dist/error-codes.client.generated.js +15 -2
  38. package/dist/error-codes.server.generated.d.ts +1 -0
  39. package/dist/error-codes.server.generated.js +5 -0
  40. package/dist/event-names.d.ts +1 -0
  41. package/dist/event-names.js +15 -0
  42. package/dist/html-tree-validation.d.ts +17 -34
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.js +12 -1
  45. package/dist/jsx-runtime.d.ts +6 -1
  46. package/dist/public-types.d.ts +188 -0
  47. package/dist/public-types.js +0 -0
  48. package/dist/runtime.d.ts +128 -69
  49. package/dist/runtime.js +1349 -360
  50. package/dist/runtime.server.d.ts +67 -15
  51. package/dist/runtime.server.js +619 -184
  52. package/dist/server/index.d.ts +4 -2
  53. package/dist/server/index.js +23 -0
  54. package/dist/static/index.d.ts +2 -1
  55. package/dist/static/index.js +3 -1
  56. package/dist/universal-core.d.ts +8 -1
  57. package/dist/universal-core.js +95 -9
  58. package/dist/version.js +1 -1
  59. package/package.json +4 -3
@@ -0,0 +1,1134 @@
1
+ /**
2
+ * Lexically scoped `<style>` blocks, `$class`, and `apply` for the Octane
3
+ * compiler (RFC tsrx-org/RFCs#1). One copy-on-write pre-pass per top-level
4
+ * statement decides which blocks belong to which template scope, emits one
5
+ * `injectStyle(hash, css)` entry per scope in lexical pre-order, and stamps
6
+ * every host element with the hashes of all its enclosing scopes followed by
7
+ * the classes of every applied theme — so the client and server emitters only
8
+ * serialize what is already on the AST and agree by construction.
9
+ *
10
+ * Scope model (shared with `@tsrx/core`'s `transform/jsx/style-scopes.js`;
11
+ * amendment A1 of the RFC):
12
+ *
13
+ * - A standalone block is a child of a native element or a fragment, and
14
+ * that CHILDREN LIST is its scope: the block styles the items beside it and
15
+ * everything below them; it never styles the element that contains it, nor
16
+ * any ancestor. A `@{ … }` body or a directive branch body holds setup
17
+ * statements and exactly one output node, and a block is an output node
18
+ * like any other, so a block beside the output node is the parser's
19
+ * multiple-outputs error and a lone block as the output is the analyzer's
20
+ * `tsrx-style-standalone-needs-fragment`; those lists are only searched for
21
+ * nested templates and assigned blocks. A list holding no block is not a
22
+ * scope and adds no hash.
23
+ * - Every block of one list shares the scope hash (the first bodied block's
24
+ * position-derived hash) and renders into ONE injection — the runtime
25
+ * dedupes by id. Lists nested in a scope that hold blocks are nested scopes
26
+ * with hashes of their own; two blocks in different lists never share one.
27
+ * - A scope's sheets are pruned against the list's other children and their
28
+ * subtrees (function boundaries excluded): a selector that matches none of
29
+ * them survives only as a `(unused)` comment, so a rule aimed at the
30
+ * container is visibly dead. `@tsrx/core`'s `prune_css` matches selectors
31
+ * against elements and records what it found on their metadata, and the
32
+ * adopted AST may be frozen, so the elements it sees are a private clone of
33
+ * the items (`cloneAstNode`, fresh metadata per node) carrying the ancestor
34
+ * paths its combinator matching reads.
35
+ * - Elements carry `authored hashes… applied…`: enclosing scope hashes outer
36
+ * first, then applied theme classes (literals for same-module themes whose
37
+ * class is statically known, `theme.$class` reads otherwise). `apply` on a
38
+ * standalone block reaches the same set of elements as its CSS.
39
+ * - Emission order is lexical pre-order: a scope's sheet sits where its first
40
+ * block is, after the assigned blocks declared before it in the enclosing
41
+ * statement list, before the scopes and assigned blocks nested in it.
42
+ * - Raw CSS in `<style>` is TSRX template syntax: the core analyzer rejects a
43
+ * standalone block outside a `@{ … }` or directive body
44
+ * (`tsrx-style-standalone-outside-template`), so plain-TSX returns never
45
+ * reach this pass with one. `<style>{expr}</style>` parses as an ordinary
46
+ * `JSXElement`: it is not a block, opens no scope, and is never stamped.
47
+ * - Assigned blocks (`const theme = <style>…</style>`) lower anywhere a
48
+ * declaration is legal: their sheet injects at the declaration position and
49
+ * the initializer becomes the class-map object (`$class` first). Exported or
50
+ * applied blocks are themes and keep every selector; other blocks keep only
51
+ * what the class map exposes. The classification comes from the core
52
+ * analyzer (`metadata.styleKind`, `metadata.styleApplies`), which runs on
53
+ * the parser AST before it is adopted, so this pass reads it read-only.
54
+ * - `style(expr)` is Octane's class-string expression (core has no such
55
+ * intrinsic): it resolves to the scope chain plus the value only where TSRX
56
+ * reads a class value — as the expression of a JSX attribute value or of a
57
+ * template expression-container child, directly or nested in the
58
+ * array/conditional/logical/template expressions of that container. The
59
+ * `style` attribute is excluded (its value is CSS, never a class list), and
60
+ * every other `style(...)` — a statement, a declaration initializer, a call
61
+ * argument, a callback body — is an ordinary user call left untouched.
62
+ * - Float style resources (`<style href precedence>`) stay in the tree.
63
+ *
64
+ * The parser AST may be frozen (see `adoptParserAst`): nodes are never
65
+ * written; changed spines are rebuilt and StyleSheet subtrees are cloned
66
+ * before the core render pipeline touches them.
67
+ */
68
+
69
+ import {
70
+ analyzeCss,
71
+ buildStyleClassMap,
72
+ builders as b,
73
+ clone_ast_node as cloneAstNode,
74
+ createScopeRoot,
75
+ createStyleClassMapFromStylesheet,
76
+ prepareStylesheetForRender,
77
+ pruneCss,
78
+ renderStylesheets,
79
+ } from '@tsrx/core';
80
+
81
+ const SKIP_KEYS = new Set(['loc', 'start', 'end', 'parent', 'metadata', 'css']);
82
+ const DIRECTIVE_TYPES = new Set([
83
+ 'JSXIfExpression',
84
+ 'JSXForExpression',
85
+ 'JSXSwitchExpression',
86
+ 'JSXTryExpression',
87
+ ]);
88
+
89
+ /**
90
+ * @typedef {{
91
+ * inheritOriginLoc: (root: any, origin: any) => any,
92
+ * markSynthesized: (node: any) => any,
93
+ * markSynthesizedAttr: (attr: any) => any,
94
+ * headResourceKind: (node: any) => string | null,
95
+ * isCompositeJsxTag: (node: any) => boolean,
96
+ * isStyleCall: (node: any) => boolean,
97
+ * }} StyleScopeTools
98
+ * @typedef {{ hash: string | null, applied: Array<string | any> }} ScopeEntry
99
+ * @typedef {{
100
+ * ctx: any,
101
+ * tools: StyleScopeTools,
102
+ * stack: ScopeEntry[],
103
+ * sequence: number,
104
+ * orderBase: number,
105
+ * firstHash: string | null,
106
+ * staticClasses: Map<any, string | null>,
107
+ * appliedParts: Map<any, Array<string | any>>,
108
+ * runtimeApplied: any[],
109
+ * stampedHost: boolean,
110
+ * }} PassState `stampedHost` is set while the attributes of a host element
111
+ * the current chain will be stamped on are walked: a `style(expr)` nested in
112
+ * that element's class value yields its value alone, the stamp adds the chain.
113
+ */
114
+
115
+ /**
116
+ * Bind the compiler helpers this pass needs (they live in compile.js).
117
+ *
118
+ * @param {StyleScopeTools} tools
119
+ */
120
+ export function createStyleScopePass(tools) {
121
+ /**
122
+ * Rewrite one top-level statement (or a component function) copy-on-write.
123
+ * Injections are pushed onto `ctx.cssInjections` in emission order with an
124
+ * `order` key that sorts them among the module's other entries.
125
+ *
126
+ * @param {any} root
127
+ * @param {any} ctx
128
+ * @returns {{ node: any, cssHash: string | null, runtimeApplied: any[] }}
129
+ * the rewritten node, the outermost scope hash the root owns (or `null`,
130
+ * kept for callers that key "this component has scoped CSS" on it), and
131
+ * the `theme.$class` targets the root's standalone scopes read at runtime
132
+ * (imported themes), one per distinct target expression
133
+ */
134
+ function applyStyleScopes(root, ctx) {
135
+ /** @type {PassState} */
136
+ const state = {
137
+ ctx,
138
+ tools,
139
+ stack: [],
140
+ sequence: 0,
141
+ orderBase: typeof root?.start === 'number' ? root.start : 0,
142
+ firstHash: null,
143
+ staticClasses: new Map(),
144
+ appliedParts: new Map(),
145
+ runtimeApplied: [],
146
+ stampedHost: false,
147
+ };
148
+ const node = walk(root, state, 'statement');
149
+ return { node, cssHash: state.firstHash, runtimeApplied: state.runtimeApplied };
150
+ }
151
+
152
+ return { applyStyleScopes };
153
+ }
154
+
155
+ // --- traversal ---------------------------------------------------------------
156
+
157
+ /**
158
+ * @param {any} node
159
+ * @returns {boolean}
160
+ */
161
+ function isTemplateNode(node) {
162
+ return node?.type === 'JSXElement' || node?.type === 'JSXFragment';
163
+ }
164
+
165
+ /**
166
+ * @param {any} node
167
+ * @returns {boolean}
168
+ */
169
+ function isDirective(node) {
170
+ return node != null && DIRECTIVE_TYPES.has(node.type);
171
+ }
172
+
173
+ /**
174
+ * @param {any} node
175
+ * @returns {boolean}
176
+ */
177
+ function isFunctionNode(node) {
178
+ return (
179
+ node?.type === 'FunctionDeclaration' ||
180
+ node?.type === 'FunctionExpression' ||
181
+ node?.type === 'ArrowFunctionExpression'
182
+ );
183
+ }
184
+
185
+ /**
186
+ * @param {any} node
187
+ * @param {string} key
188
+ * @returns {boolean}
189
+ */
190
+ function isStatementListKey(node, key) {
191
+ if (key === 'body') return node.type === 'BlockStatement' || node.type === 'Program';
192
+ if (key === 'consequent') return node.type === 'SwitchCase';
193
+ return false;
194
+ }
195
+
196
+ /**
197
+ * @template T
198
+ * @param {T[]} list
199
+ * @param {(item: T) => T | null} map `null` drops the item
200
+ * @returns {T[]}
201
+ */
202
+ function mapList(list, map) {
203
+ let out = null;
204
+ for (let i = 0; i < list.length; i++) {
205
+ const item = list[i];
206
+ const next = item !== null && typeof item === 'object' ? map(item) : item;
207
+ if (out === null && next !== item) out = list.slice(0, i);
208
+ if (out !== null && next !== null) out.push(next);
209
+ }
210
+ return out ?? list;
211
+ }
212
+
213
+ /**
214
+ * Copy-on-write map over a node's child properties.
215
+ *
216
+ * @param {any} node
217
+ * @param {(child: any, key: string) => any} map
218
+ * @returns {any}
219
+ */
220
+ function rewriteChildren(node, map) {
221
+ let out = null;
222
+ for (const key of Object.keys(node)) {
223
+ if (SKIP_KEYS.has(key)) continue;
224
+ const value = node[key];
225
+ if (value === null || typeof value !== 'object') continue;
226
+ let next;
227
+ if (Array.isArray(value)) {
228
+ next = mapList(value, (child) => map(child, key));
229
+ } else if (typeof value.type === 'string') {
230
+ next = map(value, key);
231
+ } else {
232
+ continue;
233
+ }
234
+ if (next !== value) {
235
+ if (out === null) out = { ...node };
236
+ out[key] = next;
237
+ }
238
+ }
239
+ return out ?? node;
240
+ }
241
+
242
+ /**
243
+ * The main copy-on-write walk.
244
+ *
245
+ * `mode` says what a node found here is:
246
+ * - `statement`: a statement slot (a bare `<style>` here was already reported
247
+ * by the analyzer),
248
+ * - `expression`: a value slot (a `<style>` here is an assigned block).
249
+ *
250
+ * A native element or fragment is the same thing in either slot: it is
251
+ * stamped with the current chain and its children list may open a scope.
252
+ *
253
+ * @param {any} node
254
+ * @param {PassState} state
255
+ * @param {'statement' | 'expression'} mode
256
+ * @returns {any}
257
+ */
258
+ function walk(node, state, mode) {
259
+ if (node === null || typeof node !== 'object' || typeof node.type !== 'string') return node;
260
+
261
+ switch (node.type) {
262
+ case 'JSXStyleElement':
263
+ // A value slot is an assigned block. A statement slot (or the render
264
+ // slot of a code block) is the analyzer's `needs-fragment` error; the
265
+ // node is left alone and contributes no CSS.
266
+ if (mode === 'expression') return lowerAssignedStyle(node, state);
267
+ return node;
268
+ case 'JSXCodeBlock':
269
+ return processCodeBlock(node, state);
270
+ case 'JSXElement':
271
+ case 'JSXFragment':
272
+ return walkTemplateNode(node, state);
273
+ case 'JSXExpressionContainer':
274
+ return rewriteChildren(node, (child) => walk(child, state, 'expression'));
275
+ case 'ExpressionStatement':
276
+ return rewriteChildren(node, (child) => walk(child, state, 'statement'));
277
+ case 'JSXAttribute':
278
+ return processAttribute(node, state);
279
+ default:
280
+ if (isDirective(node)) return processDirective(node, state);
281
+ if (isFunctionNode(node)) {
282
+ // A function boundary: its template is not stamped by the enclosing
283
+ // scopes, but still hosts scopes and assigned blocks of its own.
284
+ return withStack(state, [], () =>
285
+ rewriteChildren(node, (child, key) =>
286
+ walk(
287
+ child,
288
+ state,
289
+ key === 'body' && child.type === 'JSXCodeBlock' ? 'statement' : 'expression',
290
+ ),
291
+ ),
292
+ );
293
+ }
294
+ break;
295
+ }
296
+
297
+ return rewriteChildren(node, (child, key) =>
298
+ walk(child, state, isStatementListKey(node, key) ? 'statement' : 'expression'),
299
+ );
300
+ }
301
+
302
+ /**
303
+ * @template T
304
+ * @param {PassState} state
305
+ * @param {ScopeEntry[]} stack
306
+ * @param {() => T} run
307
+ * @returns {T}
308
+ */
309
+ function withStack(state, stack, run) {
310
+ const previous = state.stack;
311
+ state.stack = stack;
312
+ try {
313
+ return run();
314
+ } finally {
315
+ state.stack = previous;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * A native element or fragment: stamp it with the CURRENT chain (host
321
+ * elements only — the element itself is never inside its own children's
322
+ * scope), walk its attribute values as expressions of that same chain, then
323
+ * walk its children list. When the list holds standalone blocks it is a scope:
324
+ * the blocks are stripped, their sheet prepared, and the other children walked
325
+ * with the scope pushed. A list without blocks keeps the current chain.
326
+ *
327
+ * @param {any} node
328
+ * @param {PassState} state
329
+ * @returns {any}
330
+ */
331
+ function walkTemplateNode(node, state) {
332
+ // Attribute values first — a `style(expr)` in a class value resolves to the
333
+ // chain before the stamp reads that value — then the stamp itself.
334
+ const stamped = isStampedHost(node, state);
335
+ const previousStampedHost = state.stampedHost;
336
+ state.stampedHost = stamped;
337
+ let out;
338
+ try {
339
+ out = rewriteChildren(node, (child, key) =>
340
+ key === 'children' ? child : walk(child, state, 'expression'),
341
+ );
342
+ } finally {
343
+ state.stampedHost = previousStampedHost;
344
+ }
345
+ if (stamped) out = addScopeClasses(out, state);
346
+ const children = node.children;
347
+ if (!Array.isArray(children)) return out;
348
+ const own = collectOwnBlocks(children);
349
+ const scope =
350
+ own.length > 0
351
+ ? prepareScope(
352
+ own,
353
+ state,
354
+ children.filter((child) => !own.includes(child)),
355
+ )
356
+ : null;
357
+ const next = withStack(state, scope === null ? state.stack : [...state.stack, scope], () =>
358
+ mapList(children, (child) => {
359
+ if (child.type === 'JSXStyleElement') return own.includes(child) ? null : child;
360
+ return walkListItem(child, state);
361
+ }),
362
+ );
363
+ return next === children ? out : { ...out, children: next };
364
+ }
365
+
366
+ /**
367
+ * Whether the scope chain is stamped on this element: a host element or a
368
+ * dynamic `<{expr}>` tag. Composite components stop stamping (their host
369
+ * elements belong to their own scopes), and a `style` host element —
370
+ * `<style>{expr}</style>`, an ordinary element after amendment A1 — is never
371
+ * stamped, like a `<style>` block.
372
+ *
373
+ * @param {any} node
374
+ * @param {PassState} state
375
+ * @returns {boolean}
376
+ */
377
+ function isStampedHost(node, state) {
378
+ if (node.type !== 'JSXElement') return false;
379
+ if (state.tools.isCompositeJsxTag(node) && !node.metadata?.dynamicElement) return false;
380
+ const name = node.openingElement?.name;
381
+ return !(name?.type === 'JSXIdentifier' && name.name === 'style');
382
+ }
383
+
384
+ /**
385
+ * `@{ … }`: setup statements (searched for assigned blocks and nested
386
+ * templates) and the single output node, walked in the current chain.
387
+ *
388
+ * @param {any} node
389
+ * @param {PassState} state
390
+ * @returns {any}
391
+ */
392
+ function processCodeBlock(node, state) {
393
+ const body = processList(node.body || [], state);
394
+ const render = node.render ? walkListItem(node.render, state) : node.render;
395
+ if (body === node.body && render === node.render) return node;
396
+ return { ...node, body, render };
397
+ }
398
+
399
+ /**
400
+ * Each branch body of a directive is a scope; `@else if` chains parse as
401
+ * plain `IfStatement` alternates.
402
+ *
403
+ * @param {any} node
404
+ * @param {PassState} state
405
+ * @returns {any}
406
+ */
407
+ function processDirective(node, state) {
408
+ return rewriteChildren(node, (child, key) => {
409
+ if (child.type === 'BlockStatement') return processBlockBody(child, state);
410
+ if (child.type === 'CatchClause') {
411
+ const body = processBlockBody(child.body, state);
412
+ return body === child.body ? child : { ...child, body };
413
+ }
414
+ if (child.type === 'SwitchCase') {
415
+ const consequent = processList(child.consequent || [], state);
416
+ return consequent === child.consequent ? child : { ...child, consequent };
417
+ }
418
+ if (key === 'alternate' && (isDirective(child) || child.type === 'IfStatement')) {
419
+ return processDirective(child, state);
420
+ }
421
+ return walk(child, state, 'expression');
422
+ });
423
+ }
424
+
425
+ /**
426
+ * @param {any} block
427
+ * @param {PassState} state
428
+ * @returns {any}
429
+ */
430
+ function processBlockBody(block, state) {
431
+ const body = processList(block.body || [], state);
432
+ return body === block.body ? block : { ...block, body };
433
+ }
434
+
435
+ /**
436
+ * A statement list (a `@{ … }` body, a directive branch body, a switch case):
437
+ * not a scope of its own. Its items are searched for nested templates and
438
+ * assigned blocks in the current chain.
439
+ *
440
+ * @param {any[]} nodes
441
+ * @param {PassState} state
442
+ * @returns {any[]}
443
+ */
444
+ function processList(nodes, state) {
445
+ return mapList(nodes, (item) => walkListItem(item, state));
446
+ }
447
+
448
+ /**
449
+ * @param {any} item
450
+ * @param {PassState} state
451
+ * @returns {any}
452
+ */
453
+ function walkListItem(item, state) {
454
+ if (isTemplateNode(item)) return walkTemplateNode(item, state);
455
+ if (item.type === 'JSXStyleElement') return item;
456
+ if (item.type === 'JSXExpressionContainer') {
457
+ // A template child hole is a class-string position for `style(expr)`.
458
+ const expression = walkStyleValue(item.expression, state);
459
+ return expression === item.expression ? item : { ...item, expression };
460
+ }
461
+ return walk(item, state, 'statement');
462
+ }
463
+
464
+ /**
465
+ * A JSX attribute: its value is a class-string position for `style(expr)`
466
+ * unless the attribute is `style` (CSS, never a class list — a `style(...)`
467
+ * there is a user helper), so that one and every other value walk as ordinary
468
+ * expressions. In the `class` of an element the chain is stamped on, only a
469
+ * whole-value `style(expr)` carries the chain itself; a call nested in an
470
+ * array, conditional, logical, or template there yields its value alone, and
471
+ * the stamp appends the chain once to the composed value — so the chain is
472
+ * present whichever branch runs, and never twice.
473
+ *
474
+ * @param {any} attribute
475
+ * @param {PassState} state
476
+ * @returns {any}
477
+ */
478
+ function processAttribute(attribute, state) {
479
+ const value = attribute.value;
480
+ if (value?.type !== 'JSXExpressionContainer') {
481
+ return rewriteChildren(attribute, (child) => walk(child, state, 'expression'));
482
+ }
483
+ const name = attribute.name;
484
+ const isStyleAttribute = name?.type === 'JSXIdentifier' && name.name === 'style';
485
+ const isClassAttribute =
486
+ name?.type === 'JSXIdentifier' && (name.name === 'class' || name.name === 'className');
487
+ const expression = isStyleAttribute
488
+ ? walk(value.expression, state, 'expression')
489
+ : walkStyleValue(value.expression, state, true, isClassAttribute && state.stampedHost);
490
+ if (expression === value.expression) return attribute;
491
+ return { ...attribute, value: { ...value, expression } };
492
+ }
493
+
494
+ /**
495
+ * The class-string positions inside an attribute value or a child hole: the
496
+ * expression itself, or the parts of an array, conditional, logical, template,
497
+ * or parenthesized/TS-wrapper expression around it. Anything else is walked as
498
+ * an ordinary expression, so a `style(...)` inside (a call argument, a callback
499
+ * body) stays a user call.
500
+ *
501
+ * @param {any} node
502
+ * @param {PassState} state
503
+ * @param {boolean} [whole] whether `node` is the whole value (wrappers are
504
+ * transparent), as opposed to a part of an array/conditional/logical/template
505
+ * @param {boolean} [stamped] whether the value is the class of an element the
506
+ * chain is stamped on: a `style(expr)` that is only a PART of it then yields
507
+ * its value alone (see processAttribute)
508
+ * @returns {any}
509
+ */
510
+ function walkStyleValue(node, state, whole = true, stamped = false) {
511
+ if (node === null || typeof node !== 'object' || typeof node.type !== 'string') return node;
512
+ if (state.tools.isStyleCall(node)) {
513
+ if (stamped && !whole) return walk(node.arguments[0], state, 'expression');
514
+ return resolveStyleCall(node, state);
515
+ }
516
+ switch (node.type) {
517
+ case 'ArrayExpression': {
518
+ const elements = mapList(node.elements, (element) =>
519
+ element?.type === 'SpreadElement'
520
+ ? walk(element, state, 'expression')
521
+ : walkStyleValue(element, state, false, stamped),
522
+ );
523
+ return elements === node.elements ? node : { ...node, elements };
524
+ }
525
+ case 'ConditionalExpression': {
526
+ const test = walk(node.test, state, 'expression');
527
+ const consequent = walkStyleValue(node.consequent, state, false, stamped);
528
+ const alternate = walkStyleValue(node.alternate, state, false, stamped);
529
+ if (test === node.test && consequent === node.consequent && alternate === node.alternate) {
530
+ return node;
531
+ }
532
+ return { ...node, test, consequent, alternate };
533
+ }
534
+ case 'LogicalExpression': {
535
+ const left = walkStyleValue(node.left, state, false, stamped);
536
+ const right = walkStyleValue(node.right, state, false, stamped);
537
+ return left === node.left && right === node.right ? node : { ...node, left, right };
538
+ }
539
+ case 'TemplateLiteral': {
540
+ const expressions = mapList(node.expressions, (expression) =>
541
+ walkStyleValue(expression, state, false, stamped),
542
+ );
543
+ return expressions === node.expressions ? node : { ...node, expressions };
544
+ }
545
+ case 'ParenthesizedExpression':
546
+ case 'TSAsExpression':
547
+ case 'TSSatisfiesExpression':
548
+ case 'TSNonNullExpression':
549
+ case 'TSTypeAssertion': {
550
+ const expression = walkStyleValue(node.expression, state, whole, stamped);
551
+ return expression === node.expression ? node : { ...node, expression };
552
+ }
553
+ default:
554
+ return walk(node, state, 'expression');
555
+ }
556
+ }
557
+
558
+ /**
559
+ * The standalone blocks a children list owns: its direct `<style>` items, in
560
+ * source order. Blocks inside a nested element or fragment belong to that
561
+ * node's own children list; Float style resources are not scoped blocks.
562
+ *
563
+ * @param {any[]} nodes
564
+ * @returns {any[]}
565
+ */
566
+ function collectOwnBlocks(nodes) {
567
+ /** @type {any[]} */
568
+ const blocks = [];
569
+ for (const node of nodes) {
570
+ if (node?.type === 'JSXStyleElement' && !isFloatStyleResource(node)) blocks.push(node);
571
+ }
572
+ return blocks.sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
573
+ }
574
+
575
+ /**
576
+ * The elements a scope's selectors can match (the core transform's
577
+ * `collect_css_prunable_elements`): every host element of the items and their
578
+ * subtrees, nested scopes included, stopping at function boundaries and
579
+ * skipping `<style>` hosts. Each element gets its ancestor chain as
580
+ * `metadata.path`, which `pruneCss` reads for combinators; the nodes are a
581
+ * private clone, so the writes never reach the (possibly frozen) parser AST.
582
+ *
583
+ * @param {any} value a cloned node or list of cloned nodes
584
+ * @param {any[]} elements
585
+ * @param {any[]} path
586
+ * @returns {any[]}
587
+ */
588
+ function collectPrunableElements(value, elements, path) {
589
+ if (Array.isArray(value)) {
590
+ for (const item of value) collectPrunableElements(item, elements, path);
591
+ return elements;
592
+ }
593
+ if (value === null || typeof value !== 'object' || typeof value.type !== 'string')
594
+ return elements;
595
+ if (isFunctionNode(value)) return elements;
596
+ if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
597
+ const name = value.openingElement?.name;
598
+ if (name?.type === 'JSXIdentifier' && name.name === 'style') return elements;
599
+ value.metadata.path = path.slice();
600
+ elements.push(value);
601
+ }
602
+ const childPath = [...path, value];
603
+ for (const key of Object.keys(value)) {
604
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
605
+ continue;
606
+ }
607
+ const child = value[key];
608
+ if (Array.isArray(child)) {
609
+ for (const item of child) {
610
+ if (item !== null && typeof item === 'object' && typeof item.type === 'string') {
611
+ collectPrunableElements(item, elements, childPath);
612
+ }
613
+ }
614
+ } else if (child !== null && typeof child === 'object' && typeof child.type === 'string') {
615
+ collectPrunableElements(child, elements, childPath);
616
+ }
617
+ }
618
+ return elements;
619
+ }
620
+
621
+ /**
622
+ * `<style href precedence>` (React Float): plain CSS by href identity, outside
623
+ * the scope model.
624
+ *
625
+ * @param {any} node
626
+ * @returns {boolean}
627
+ */
628
+ function isFloatStyleResource(node) {
629
+ let hasHref = false;
630
+ let hasPrecedence = false;
631
+ for (const attr of node.openingElement?.attributes || []) {
632
+ if (attr?.type !== 'JSXAttribute' || attr.name?.type !== 'JSXIdentifier') continue;
633
+ if (attr.name.name === 'href') hasHref = true;
634
+ else if (attr.name.name === 'precedence') hasPrecedence = true;
635
+ }
636
+ return hasHref && hasPrecedence;
637
+ }
638
+
639
+ // --- sheets ------------------------------------------------------------------
640
+
641
+ /**
642
+ * @param {PassState} state
643
+ * @returns {number}
644
+ */
645
+ function nextOrder(state) {
646
+ state.sequence += 1;
647
+ return state.orderBase + state.sequence * 1e-6;
648
+ }
649
+
650
+ /**
651
+ * Render a scope's sheets into one injection and compute what its elements
652
+ * carry.
653
+ *
654
+ * @param {any[]} own the list's standalone blocks
655
+ * @param {PassState} state
656
+ * @param {any[]} items the list's other children — what the blocks reach
657
+ * @returns {ScopeEntry}
658
+ */
659
+ function prepareScope(own, state, items) {
660
+ const { ctx } = state;
661
+ /** @type {Array<{ node: any, sheet: any }>} */
662
+ const sheets = [];
663
+ for (const block of own) {
664
+ const sheet = (block.children || []).find((c) => c && c.type === 'StyleSheet');
665
+ if (sheet) sheets.push({ node: block, sheet });
666
+ }
667
+ let hash = null;
668
+ if (sheets.length > 0) {
669
+ hash = sheets[0].node.metadata?.styleScopeHash || sheets[0].sheet.hash || null;
670
+ const scopeItems = cloneAstNode(items);
671
+ // A fragment exposes root siblings without making the scope's container
672
+ // match ancestor selectors. Keep it on the private pruning tree.
673
+ const elements = collectPrunableElements(scopeItems, [], [createScopeRoot(scopeItems)]);
674
+ const prepared = sheets.map(({ node, sheet }) => {
675
+ const regionHash = node.metadata?.styleScopeHash || sheet.hash;
676
+ const clone = cloneAstNode(sheet);
677
+ clone.hash = hash;
678
+ // `analyzeCss` marks `:global(...)` selectors; pruning then marks the
679
+ // selectors that reach an element as scoped and used, exactly like the
680
+ // core transform — an unmatched rule renders as an `(unused)` comment.
681
+ analyzeCss(clone);
682
+ const styleClasses = new Map();
683
+ const topScopedClasses = new Map();
684
+ for (const element of elements) {
685
+ pruneCss(clone, element, styleClasses, topScopedClasses, regionHash);
686
+ }
687
+ return clone;
688
+ });
689
+ ctx.cssInjections.push({
690
+ hash,
691
+ css: renderStylesheets(prepared),
692
+ order: nextOrder(state),
693
+ // The authored `<style>` element(s) this stylesheet came from, so the
694
+ // emitted `injectStyle` maps back to them.
695
+ origins: sheets.map(({ node }) => node),
696
+ });
697
+ ctx.runtimeNeeded.add('injectStyle');
698
+ if (state.firstHash === null) state.firstHash = hash;
699
+ }
700
+ /** @type {Array<string | any>} */
701
+ const applied = [];
702
+ for (const block of own) {
703
+ for (const part of resolveAppliedParts(block, state)) {
704
+ if (typeof part !== 'string' || !applied.includes(part)) applied.push(part);
705
+ }
706
+ }
707
+ for (const part of applied) {
708
+ if (typeof part !== 'string') recordRuntimeApplied(part.object, state);
709
+ }
710
+ return { hash, applied };
711
+ }
712
+
713
+ /**
714
+ * The class parts a block's `apply` contributes: one literal per statically
715
+ * known hash of a same-module theme, otherwise a runtime `<target>.$class`
716
+ * read.
717
+ *
718
+ * @param {any} block
719
+ * @param {PassState} state
720
+ * @returns {Array<string | any>}
721
+ */
722
+ function resolveAppliedParts(block, state) {
723
+ const cached = state.appliedParts.get(block);
724
+ if (cached) return cached;
725
+ /** @type {Array<string | any>} */
726
+ const parts = [];
727
+ for (const resolution of block.metadata?.styleApplies ?? []) {
728
+ const staticClass = resolution.target ? staticStyleClass(resolution.target, state) : null;
729
+ if (staticClass !== null) {
730
+ for (const hash of staticClass.split(' ')) {
731
+ if (hash && !parts.includes(hash)) parts.push(hash);
732
+ }
733
+ continue;
734
+ }
735
+ parts.push(
736
+ state.tools.inheritOriginLoc(
737
+ b.member(cloneAstNode(resolution.expression), b.id('$class')),
738
+ resolution.expression,
739
+ ),
740
+ );
741
+ }
742
+ state.appliedParts.set(block, parts);
743
+ return parts;
744
+ }
745
+
746
+ /**
747
+ * The `$class` of an assigned block when every applied theme in its chain is
748
+ * a same-module block: applied hashes first (each once), own hash last.
749
+ *
750
+ * @param {any} block
751
+ * @param {PassState} state
752
+ * @returns {string | null}
753
+ */
754
+ function staticStyleClass(block, state) {
755
+ const cached = state.staticClasses.get(block);
756
+ if (cached !== undefined) return cached;
757
+ /** @type {string[]} */
758
+ const parts = [];
759
+ let result = '';
760
+ for (const resolution of block.metadata?.styleApplies ?? []) {
761
+ const applied = resolution.target ? staticStyleClass(resolution.target, state) : null;
762
+ if (applied === null) {
763
+ result = null;
764
+ break;
765
+ }
766
+ for (const hash of applied.split(' ')) {
767
+ if (hash && !parts.includes(hash)) parts.push(hash);
768
+ }
769
+ }
770
+ if (result !== null) {
771
+ const sheet = (block.children || []).find((c) => c && c.type === 'StyleSheet');
772
+ const own = sheet ? block.metadata?.styleScopeHash || sheet.hash : null;
773
+ if (own && !parts.includes(own)) parts.push(own);
774
+ result = parts.join(' ');
775
+ }
776
+ state.staticClasses.set(block, result);
777
+ return result;
778
+ }
779
+
780
+ /**
781
+ * `const theme = <style>…</style>` (or a property value / default export):
782
+ * inject the sheet at the declaration position and hand back the class-map
783
+ * object. A body-less `<style apply={…} />` exposes `$class` only.
784
+ *
785
+ * @param {any} styleNode
786
+ * @param {PassState} state
787
+ * @returns {any}
788
+ */
789
+ function lowerAssignedStyle(styleNode, state) {
790
+ const { ctx, tools } = state;
791
+ const applied = resolveAppliedParts(styleNode, state);
792
+ const sheet = (styleNode.children || []).find((c) => c && c.type === 'StyleSheet');
793
+ if (!sheet) {
794
+ // A body-less bundle has no sheet of its own, but on the server reading
795
+ // it must still inject the themes it applies: it is wrapped like any
796
+ // assigned block, with nothing to inject for itself.
797
+ return wrapServerStyleMap(
798
+ tools.inheritOriginLoc(buildStyleClassMap(new Map(), null, { applied }), styleNode),
799
+ null,
800
+ null,
801
+ styleNode,
802
+ state,
803
+ );
804
+ }
805
+ const hash = styleNode.metadata?.styleScopeHash || sheet.hash;
806
+ const clone = cloneAstNode(sheet);
807
+ clone.hash = hash;
808
+ // `analyzeCss` marks `:global(...)` selectors so the renderer leaves them
809
+ // unscoped; the render mode comes from the core analyzer's classification.
810
+ analyzeCss(clone);
811
+ prepareStylesheetForRender(
812
+ clone,
813
+ styleNode.metadata?.styleKind === 'theme' ? 'theme' : 'class-map',
814
+ );
815
+ const css = renderStylesheets([clone]);
816
+ ctx.cssInjections.push({
817
+ hash,
818
+ css,
819
+ order: nextOrder(state),
820
+ origin: styleNode,
821
+ });
822
+ ctx.runtimeNeeded.add('injectStyle');
823
+ // The class-map object is built loc-less by the core helper; it maps to the
824
+ // authored <style>.
825
+ return wrapServerStyleMap(
826
+ tools.inheritOriginLoc(createStyleClassMapFromStylesheet(clone, { applied }), styleNode),
827
+ hash,
828
+ css,
829
+ styleNode,
830
+ state,
831
+ );
832
+ }
833
+
834
+ /**
835
+ * On the server a module's assigned blocks only inject inside the bodies of
836
+ * that module's own components; a theme read from ANOTHER module would never
837
+ * reach the request collector. Wrap the map so property access injects its
838
+ * CSS (after the CSS of the themes it applies) into the active render.
839
+ *
840
+ * Every applied block is a dependency, whether it lives in this module or is
841
+ * imported: the class list inlines a same-module theme's hashes as literals,
842
+ * which says nothing about its CSS, so the wrapper touches the applied maps
843
+ * first (each of them a wrapper touching its own, so a chain injects
844
+ * transitively, applied before applier, each sheet once). A body-less bundle
845
+ * has no sheet (`null` id and css) and only forwards the touch.
846
+ *
847
+ * @param {any} map
848
+ * @param {string | null} hash
849
+ * @param {string | null} css
850
+ * @param {any} styleNode the authored block, whose `styleApplies` name the dependencies
851
+ * @param {PassState} state
852
+ * @returns {any}
853
+ */
854
+ function wrapServerStyleMap(map, hash, css, styleNode, state) {
855
+ const { ctx, tools } = state;
856
+ if (ctx.mode !== 'server') return map;
857
+ ctx.runtimeNeeded.add('styleMap');
858
+ /** @type {any[]} */
859
+ const dependencies = [];
860
+ /** @type {string[]} */
861
+ const seen = [];
862
+ for (const resolution of styleNode.metadata?.styleApplies ?? []) {
863
+ const key = expressionKey(resolution.expression);
864
+ if (seen.includes(key)) continue;
865
+ seen.push(key);
866
+ dependencies.push(
867
+ tools.inheritOriginLoc(cloneAstNode(resolution.expression), resolution.expression),
868
+ );
869
+ }
870
+ const literal = (value) =>
871
+ value === null ? b.literal(null, 'null') : b.literal(value, JSON.stringify(value));
872
+ return tools.inheritOriginLoc(
873
+ b.call(
874
+ '_$styleMap',
875
+ literal(hash),
876
+ literal(css),
877
+ map,
878
+ ...(dependencies.length > 0 ? [b.array(dependencies)] : []),
879
+ ),
880
+ map,
881
+ );
882
+ }
883
+
884
+ /**
885
+ * @param {any} expression the theme expression read at runtime (`theme` of `theme.$class`)
886
+ * @param {PassState} state
887
+ */
888
+ function recordRuntimeApplied(expression, state) {
889
+ const key = expressionKey(expression);
890
+ if (state.runtimeApplied.some((existing) => expressionKey(existing) === key)) return;
891
+ state.runtimeApplied.push(expression);
892
+ }
893
+
894
+ /**
895
+ * @param {any} node
896
+ * @returns {string}
897
+ */
898
+ function expressionKey(node) {
899
+ if (node?.type === 'Identifier') return node.name;
900
+ if (node?.type === 'MemberExpression' && !node.computed) {
901
+ return `${expressionKey(node.object)}.${expressionKey(node.property)}`;
902
+ }
903
+ return JSON.stringify(node, (key, value) => (SKIP_KEYS.has(key) ? undefined : value));
904
+ }
905
+
906
+ // --- class stamping ----------------------------------------------------------
907
+
908
+ /**
909
+ * @param {PassState} state
910
+ * @returns {{ hashes: string[], applied: Array<string | any> }}
911
+ */
912
+ function currentChain(state) {
913
+ /** @type {string[]} */
914
+ const hashes = [];
915
+ /** @type {Array<string | any>} */
916
+ const applied = [];
917
+ for (const entry of state.stack) {
918
+ if (entry.hash && !hashes.includes(entry.hash)) hashes.push(entry.hash);
919
+ for (const part of entry.applied) {
920
+ if (typeof part !== 'string' || (!applied.includes(part) && !hashes.includes(part))) {
921
+ applied.push(part);
922
+ }
923
+ }
924
+ }
925
+ return { hashes, applied };
926
+ }
927
+
928
+ /**
929
+ * Fold a sequence of class parts (literals and expressions) into one value: a
930
+ * string literal when everything is static, otherwise one template literal.
931
+ *
932
+ * @param {Array<string | any>} sequence
933
+ * @returns {any}
934
+ */
935
+ function buildClassValue(sequence) {
936
+ /** @type {any[]} */
937
+ const quasis = [];
938
+ /** @type {any[]} */
939
+ const expressions = [];
940
+ let text = '';
941
+ for (const part of sequence) {
942
+ if (typeof part === 'string') {
943
+ if (part) text = text ? `${text} ${part}` : part;
944
+ continue;
945
+ }
946
+ const between = expressions.length > 0;
947
+ quasis.push(b.quasi(text ? (between ? ` ${text} ` : `${text} `) : between ? ' ' : '', false));
948
+ expressions.push(part);
949
+ text = '';
950
+ }
951
+ if (expressions.length === 0) return b.literal(text, JSON.stringify(text));
952
+ quasis.push(b.quasi(text ? ` ${text}` : '', true));
953
+ return b.template(quasis, expressions);
954
+ }
955
+
956
+ /**
957
+ * Stamp the current scope chain on a host element, copy-on-write.
958
+ *
959
+ * @param {any} element
960
+ * @param {PassState} state
961
+ * @returns {any}
962
+ */
963
+ function addScopeClasses(element, state) {
964
+ const { hashes, applied } = currentChain(state);
965
+ if (hashes.length === 0 && applied.length === 0) return element;
966
+ const { ctx, tools } = state;
967
+ const openingElement = element.openingElement;
968
+ const attrs = openingElement?.attributes || [];
969
+ const index = attrs.findIndex(
970
+ (attr) =>
971
+ attr?.type === 'JSXAttribute' &&
972
+ attr.name?.type === 'JSXIdentifier' &&
973
+ (attr.name.name === 'class' || attr.name.name === 'className'),
974
+ );
975
+
976
+ /** @type {any[]} */
977
+ const chain = [...hashes, ...applied];
978
+ let newAttrs;
979
+ if (index === -1) {
980
+ // A synthesized class attribute maps to the element's opening tag; it is
981
+ // marked NOT AUTHORED so inspection does not claim the tag as its origin.
982
+ const value = buildClassValue(chain);
983
+ newAttrs = [
984
+ ...attrs,
985
+ tools.markSynthesizedAttr(
986
+ tools.inheritOriginLoc(
987
+ b.jsx_attribute(
988
+ b.jsx_id('class'),
989
+ value.type === 'Literal' ? value : b.jsx_expression_container(value),
990
+ ),
991
+ openingElement,
992
+ ),
993
+ ),
994
+ ];
995
+ } else {
996
+ const existing = attrs[index];
997
+ const value = existing.value;
998
+ let newAttr;
999
+ if (!value) {
1000
+ // The NAME is authored (`class`), the value is not.
1001
+ const built = buildClassValue(chain);
1002
+ newAttr = {
1003
+ ...existing,
1004
+ value: tools.markSynthesized(
1005
+ tools.inheritOriginLoc(
1006
+ built.type === 'Literal' ? built : b.jsx_expression_container(built),
1007
+ existing,
1008
+ ),
1009
+ ),
1010
+ };
1011
+ } else if (value.type === 'Literal' && typeof value.value === 'string') {
1012
+ const built = buildClassValue([value.value, ...chain]);
1013
+ newAttr = {
1014
+ ...existing,
1015
+ value:
1016
+ built.type === 'Literal'
1017
+ ? { ...value, value: built.value, raw: built.raw }
1018
+ : tools.inheritOriginLoc(b.jsx_expression_container(built), value),
1019
+ };
1020
+ } else {
1021
+ const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
1022
+ let base;
1023
+ if (isResolvedStyleValue(expression, tools)) {
1024
+ // `class={style(expr)}` already resolves to the chain plus the value.
1025
+ return element;
1026
+ }
1027
+ if (expression.type === 'Literal' && typeof expression.value === 'string') {
1028
+ base = expression.value;
1029
+ } else {
1030
+ // Normalize a dynamic value (clsx arrays/objects, null) BEFORE the
1031
+ // chain is appended so the template-literal slot is a plain string.
1032
+ ctx.runtimeNeeded.add('normalizeClass');
1033
+ base = tools.inheritOriginLoc(b.call('_$normalizeClass', expression), expression);
1034
+ }
1035
+ newAttr = {
1036
+ ...existing,
1037
+ value: tools.inheritOriginLoc(
1038
+ b.jsx_expression_container(buildClassValue([base, ...chain])),
1039
+ value,
1040
+ ),
1041
+ };
1042
+ }
1043
+ newAttrs = attrs.slice();
1044
+ newAttrs[index] = newAttr;
1045
+ }
1046
+ // The core helper mirrors the attribute list onto `element.attributes` in
1047
+ // every branch; keep that alias in sync on the rebuilt copy.
1048
+ const out = { ...element, openingElement: { ...openingElement, attributes: newAttrs } };
1049
+ if ('attributes' in element) out.attributes = newAttrs;
1050
+ return out;
1051
+ }
1052
+
1053
+ /**
1054
+ * Whether a class value IS a `style(expr)` — the whole value, through
1055
+ * parenthesized and TS wrapper expressions — and so already carries the chain.
1056
+ * A call that is only a part of the value was lowered to its value alone (see
1057
+ * walkStyleValue) and leaves the stamp to add the chain.
1058
+ *
1059
+ * @param {any} expression
1060
+ * @param {StyleScopeTools} tools
1061
+ * @returns {boolean}
1062
+ */
1063
+ function isResolvedStyleValue(expression, tools) {
1064
+ let current = expression;
1065
+ while (
1066
+ current?.type === 'ParenthesizedExpression' ||
1067
+ current?.type === 'TSAsExpression' ||
1068
+ current?.type === 'TSSatisfiesExpression' ||
1069
+ current?.type === 'TSNonNullExpression' ||
1070
+ current?.type === 'TSTypeAssertion'
1071
+ ) {
1072
+ current = current.expression;
1073
+ }
1074
+ return (
1075
+ current != null &&
1076
+ (tools.isStyleCall(current) || current.metadata?.tsrx_style_resolved === true)
1077
+ );
1078
+ }
1079
+
1080
+ /**
1081
+ * `{style(expr)}` → the scope chain plus the value: literals fold, dynamic
1082
+ * values concatenate at runtime (`'hash ' + expr`, so an absent value still
1083
+ * yields the chain).
1084
+ *
1085
+ * @param {any} node
1086
+ * @param {PassState} state
1087
+ * @returns {any}
1088
+ */
1089
+ function resolveStyleCall(node, state) {
1090
+ const { hashes, applied } = currentChain(state);
1091
+ const inner = walk(node.arguments[0], state, 'expression');
1092
+ if (hashes.length === 0 && applied.length === 0) return inner;
1093
+ const chain = [...hashes, ...applied];
1094
+ if (inner.type === 'Literal' && typeof inner.value === 'string') {
1095
+ const value = buildClassValue([...chain, inner.value]);
1096
+ return markResolvedStyle(
1097
+ state.tools.inheritOriginLoc(
1098
+ value.type === 'Literal' ? b.literal(value.value, value.raw) : value,
1099
+ node,
1100
+ ),
1101
+ );
1102
+ }
1103
+ // `(<chain> + ' ' + (expr))`: the chain must always be present.
1104
+ let prefix = null;
1105
+ let text = '';
1106
+ for (const part of chain) {
1107
+ if (typeof part === 'string') {
1108
+ text = text ? `${text} ${part}` : part;
1109
+ continue;
1110
+ }
1111
+ const literal = b.literal(text ? `${text} ` : ' ', JSON.stringify(text ? `${text} ` : ' '));
1112
+ prefix = prefix
1113
+ ? b.binary('+', b.binary('+', prefix, literal), part)
1114
+ : b.binary('+', literal, part);
1115
+ text = '';
1116
+ }
1117
+ const tail = b.literal(text ? ` ${text} ` : ' ', JSON.stringify(text ? ` ${text} ` : ' '));
1118
+ const expression = prefix
1119
+ ? b.binary('+', b.binary('+', prefix, tail), inner)
1120
+ : b.binary('+', b.literal(`${text} `, JSON.stringify(`${text} `)), inner);
1121
+ return markResolvedStyle(state.tools.inheritOriginLoc(expression, node));
1122
+ }
1123
+
1124
+ /**
1125
+ * A class value the pre-pass built from `style(expr)` already carries the
1126
+ * chain; the stamp must not append it again. Compiler-built nodes only.
1127
+ *
1128
+ * @param {any} node
1129
+ * @returns {any}
1130
+ */
1131
+ function markResolvedStyle(node) {
1132
+ node.metadata = { ...(node.metadata ?? {}), tsrx_style_resolved: true };
1133
+ return node;
1134
+ }