@tsrx/core 0.1.65 → 0.1.66

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 (52) hide show
  1. package/README.md +26 -1
  2. package/package.json +9 -5
  3. package/src/analyze/css-analyze.js +44 -6
  4. package/src/analyze/index.js +14 -1
  5. package/src/analyze/style-analyze.js +467 -0
  6. package/src/analyze/validation.js +57 -0
  7. package/src/diagnostics.js +22 -0
  8. package/src/index.js +17 -0
  9. package/src/parse/style.js +97 -7
  10. package/src/plugin.js +145 -47
  11. package/src/scope.js +1 -1
  12. package/src/transform/jsx/index.js +94 -417
  13. package/src/transform/jsx/style-scopes.js +842 -0
  14. package/src/transform/scoping.js +129 -79
  15. package/src/transform/segments.js +16 -4
  16. package/src/transform/style-ref.js +74 -13
  17. package/src/transform/stylesheet.js +2 -1
  18. package/src/utils/is-reference.js +59 -0
  19. package/tests/fixtures/scoped-styles/README.md +71 -0
  20. package/tests/fixtures/scoped-styles/apply-forms.expected.json +18 -0
  21. package/tests/fixtures/scoped-styles/apply-forms.tsrx +69 -0
  22. package/tests/fixtures/scoped-styles/assigned-positions.expected.json +29 -0
  23. package/tests/fixtures/scoped-styles/assigned-positions.tsrx +90 -0
  24. package/tests/fixtures/scoped-styles/class-opt-in.expected.json +13 -0
  25. package/tests/fixtures/scoped-styles/class-opt-in.tsrx +39 -0
  26. package/tests/fixtures/scoped-styles/control-flow-else-if.expected.json +11 -0
  27. package/tests/fixtures/scoped-styles/control-flow-else-if.tsrx +26 -0
  28. package/tests/fixtures/scoped-styles/control-flow.expected.json +17 -0
  29. package/tests/fixtures/scoped-styles/control-flow.tsrx +102 -0
  30. package/tests/fixtures/scoped-styles/cross-module-apply.expected.json +14 -0
  31. package/tests/fixtures/scoped-styles/cross-module-apply.tsrx +48 -0
  32. package/tests/fixtures/scoped-styles/element-rooted-templates.expected.json +12 -0
  33. package/tests/fixtures/scoped-styles/element-rooted-templates.tsrx +33 -0
  34. package/tests/fixtures/scoped-styles/precedence.expected.json +11 -0
  35. package/tests/fixtures/scoped-styles/precedence.tsrx +45 -0
  36. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.expected.json +12 -0
  37. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.tsrx +46 -0
  38. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.expected.json +9 -0
  39. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.tsrx +24 -0
  40. package/tests/fixtures/scoped-styles/search-panel.expected.json +11 -0
  41. package/tests/fixtures/scoped-styles/search-panel.tsrx +48 -0
  42. package/tests/fixtures/scoped-styles/sibling-scope.expected.json +11 -0
  43. package/tests/fixtures/scoped-styles/sibling-scope.tsrx +44 -0
  44. package/tests/fixtures/scoped-styles/sibling-scopes.expected.json +12 -0
  45. package/tests/fixtures/scoped-styles/sibling-scopes.tsrx +51 -0
  46. package/tests/fixtures/scoped-styles/theme-composition.expected.json +14 -0
  47. package/tests/fixtures/scoped-styles/theme-composition.tsrx +45 -0
  48. package/tests/fixtures/scoped-styles/theme-diamond.expected.json +10 -0
  49. package/tests/fixtures/scoped-styles/theme-diamond.tsrx +18 -0
  50. package/tests/shared/scoped-styles-fixtures.js +67 -0
  51. package/tests/utils/fixtures/style-syntax.js +519 -0
  52. package/types/index.d.ts +85 -0
@@ -0,0 +1,842 @@
1
+ /**
2
+ * Style scope pre-pass: the one place that decides which `<style>` blocks
3
+ * belong to which scope, in what order their CSS is emitted, and which
4
+ * classes every element carries.
5
+ *
6
+ * A scope is the children list of a native element or fragment that holds at
7
+ * least one standalone block. A block styles the other children of its list
8
+ * and everything below them; it never styles the element that contains it,
9
+ * nor any ancestor. Every block of a scope shares the scope hash (the first
10
+ * bodied block's position-derived hash), and every element carries the hashes
11
+ * of all its enclosing scopes, outer first, followed by the classes of every
12
+ * applied theme. Children lists nested in a scope that hold blocks are nested
13
+ * scopes. Statement lists — `@{ … }` bodies and control-flow bodies — hold no
14
+ * standalone blocks (a block there is an output node, and the analyzer reports
15
+ * it); they are only searched for nested templates and assigned blocks.
16
+ *
17
+ * CSS is emitted in lexical pre-order: a scope's sheets form one contiguous
18
+ * group placed where its first block sits, before the sheets of the scopes
19
+ * and assigned blocks nested in it, after the assigned blocks declared before
20
+ * it. Running once, before the target walker, makes this order an invariant
21
+ * rather than a property of the walker's traversal.
22
+ *
23
+ * The pass is copy-on-write over the parsed AST: nodes it does not change are
24
+ * returned as-is, and node metadata is shared with the parser nodes so later
25
+ * passes (and the editor mappings) see the same annotations.
26
+ *
27
+ * @import * as AST from 'estree'
28
+ * @import * as ESTreeJSX from 'estree-jsx'
29
+ * @import { CompileError, JsxTransformContext as TransformContext } from '../../../types/index'
30
+ */
31
+
32
+ import { analyze_css } from '../../analyze/css-analyze.js';
33
+ import { prune_css } from '../../analyze/prune.js';
34
+ import { DIAGNOSTIC_CODES } from '../../diagnostics.js';
35
+ import { error } from '../../errors.js';
36
+ import * as b from '../../utils/builders.js';
37
+ import {
38
+ child_nodes,
39
+ has_location,
40
+ is_ast_node,
41
+ is_function_node,
42
+ is_style_element,
43
+ is_template_directive,
44
+ node_children,
45
+ } from '../../utils/ast.js';
46
+ import {
47
+ add_scope_classes,
48
+ is_composite_jsx_element,
49
+ prepare_stylesheet_for_render,
50
+ } from '../scoping.js';
51
+ import {
52
+ collect_style_ref_attributes,
53
+ create_style_class_map,
54
+ create_style_ref_setup_statements,
55
+ get_style_element_stylesheet,
56
+ } from '../style-ref.js';
57
+ import { clone_ast_node, create_generated_identifier } from './ast-builders.js';
58
+ import { set_node_path_metadata } from './helpers.js';
59
+
60
+ /**
61
+ * @typedef {'statement' | 'expression'} DescendMode
62
+ * @typedef {{
63
+ * ctx: TransformContext,
64
+ * class_attr_name: 'class' | 'className',
65
+ * static_classes: Map<AST.JSXStyleElement, string | null>,
66
+ * ref_sink: AST.Statement[],
67
+ * }} StyleScopeState `ref_sink` collects the `<style ref>` setup statements
68
+ * of children-list scopes until the nearest statement slot (the statement
69
+ * list holding the template, or an expression-position root) places them.
70
+ * @typedef {{ hash: string | null, applied: Array<string | AST.Expression>, ref_statements: AST.Statement[] }} ScopeStyles
71
+ */
72
+
73
+ /**
74
+ * Run the pre-pass over a program. Returns the rewritten program (the same
75
+ * object when nothing changed) and fills `ctx.stylesheets` in emission order.
76
+ *
77
+ * @param {AST.Program} program
78
+ * @param {TransformContext} ctx
79
+ * @returns {AST.Program}
80
+ */
81
+ export function prepare_style_scopes(program, ctx) {
82
+ /** @type {StyleScopeState} */
83
+ const state = {
84
+ ctx,
85
+ class_attr_name:
86
+ ctx.platform.jsx.classAttrName ?? (ctx.platform.jsx.rewriteClassAttr ? 'className' : 'class'),
87
+ static_classes: new Map(),
88
+ ref_sink: [],
89
+ };
90
+ // Module scope is not a template scope: a standalone block here is an
91
+ // analyzer error, and its statements are only searched for scopes.
92
+ const body = map_list(program.body, (statement) => descend(statement, state, 'statement'));
93
+ return body === program.body ? program : { ...program, body };
94
+ }
95
+
96
+ /**
97
+ * @template {AST.Node} T
98
+ * @param {T[]} list
99
+ * @param {(item: T) => T} map
100
+ * @returns {T[]}
101
+ */
102
+ function map_list(list, map) {
103
+ let out = list;
104
+ for (let i = 0; i < list.length; i += 1) {
105
+ const item = list[i];
106
+ const next = is_ast_node(item) ? map(item) : item;
107
+ if (next !== item) {
108
+ if (out === list) out = list.slice();
109
+ out[i] = next;
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+
115
+ /**
116
+ * @param {AST.Node | null | undefined} node
117
+ * @returns {node is AST.TSRXJSXElement | AST.TSRXJSXFragment}
118
+ */
119
+ function is_native_template_node(node) {
120
+ return (
121
+ (node?.type === 'JSXElement' || node?.type === 'JSXFragment') && !!node.metadata?.native_tsrx
122
+ );
123
+ }
124
+
125
+ /**
126
+ * A statement-list item that renders a template and may host nested scopes.
127
+ * Setup statements do not.
128
+ *
129
+ * @param {AST.Node} node
130
+ * @returns {boolean}
131
+ */
132
+ function is_render_item(node) {
133
+ return (
134
+ is_native_template_node(node) || node.type === 'JSXCodeBlock' || is_template_directive(node)
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Copy-on-write search for nested scopes and assigned blocks below a node
140
+ * that is not itself template content of the current scope.
141
+ *
142
+ * @template {AST.Node} T
143
+ * @param {T} node
144
+ * @param {StyleScopeState} state
145
+ * @param {DescendMode} mode whether `node` sits in a statement slot or holds a value
146
+ * @returns {T}
147
+ */
148
+ function descend(node, state, mode) {
149
+ if (!is_ast_node(node)) return node;
150
+
151
+ if (is_style_element(node)) {
152
+ // A block holding a value is an assigned block. A block in a statement
153
+ // slot outside any template scope was reported by the analyzer; it is
154
+ // left alone so editor output stays analyzable.
155
+ if (mode === 'expression') prepare_assigned_style(node, state);
156
+ return node;
157
+ }
158
+
159
+ if (node.type === 'JSXCodeBlock') {
160
+ return /** @type {T} */ (process_code_block(node, state));
161
+ }
162
+
163
+ if (is_template_directive(node)) {
164
+ return /** @type {T} */ (process_directive(node, state));
165
+ }
166
+
167
+ if (is_native_template_node(node)) {
168
+ // A native template in expression or statement position roots a scope of
169
+ // its own (an assigned template, a returned fragment, a branch value).
170
+ return /** @type {T} */ (process_root(node, state));
171
+ }
172
+
173
+ if (node.type === 'ExpressionStatement') {
174
+ const expression = descend(node.expression, state, 'statement');
175
+ return expression === node.expression ? node : { ...node, expression };
176
+ }
177
+
178
+ return rewrite_children(node, state, (child, key) =>
179
+ descend(child, state, is_statement_list_key(node, key) ? 'statement' : 'expression'),
180
+ );
181
+ }
182
+
183
+ /**
184
+ * @param {AST.Node} node
185
+ * @param {string} key
186
+ * @returns {boolean}
187
+ */
188
+ function is_statement_list_key(node, key) {
189
+ if (key === 'body') return node.type === 'BlockStatement' || node.type === 'Program';
190
+ if (key === 'consequent') return node.type === 'SwitchCase';
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Copy-on-write map over a node's child properties.
196
+ *
197
+ * @template {AST.Node} T
198
+ * @param {T} node
199
+ * @param {StyleScopeState} state
200
+ * @param {(child: AST.Node, key: string) => AST.Node} map
201
+ * @returns {T}
202
+ */
203
+ function rewrite_children(node, state, map) {
204
+ /** @type {Record<string, unknown>} */
205
+ const source = /** @type {Record<string, unknown>} */ (node);
206
+ let out = source;
207
+ for (const key of Object.keys(source)) {
208
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
209
+ continue;
210
+ }
211
+ const value = source[key];
212
+ if (Array.isArray(value)) {
213
+ const next = map_list(/** @type {AST.Node[]} */ (value), (child) => map(child, key));
214
+ if (next !== value) {
215
+ if (out === source) out = { ...source };
216
+ out[key] = next;
217
+ }
218
+ } else if (is_ast_node(value)) {
219
+ const next = map(value, key);
220
+ if (next !== value) {
221
+ if (out === source) out = { ...source };
222
+ out[key] = next;
223
+ }
224
+ }
225
+ }
226
+ return /** @type {T} */ (out);
227
+ }
228
+
229
+ /**
230
+ * A native element or fragment that already sits inside a scope. Its own
231
+ * children list is a scope when it holds standalone blocks: the blocks style
232
+ * the other children and their descendants — never the node itself — and are
233
+ * stripped (type-only output keeps emptied stand-ins so the editor can map
234
+ * the tags). Expression containers and attribute values inside are roots of
235
+ * their own.
236
+ *
237
+ * @template {AST.TSRXJSXElement | AST.TSRXJSXFragment} T
238
+ * @param {T} node
239
+ * @param {StyleScopeState} state
240
+ * @returns {T}
241
+ */
242
+ function descend_template_children(node, state) {
243
+ const children = node_children(node);
244
+ const own = collect_own_blocks(children);
245
+ const scope = own.length > 0 ? prepare_scope(own, children, own[0], state) : null;
246
+ if (scope && scope.ref_statements.length > 0) {
247
+ state.ref_sink.push(...scope.ref_statements);
248
+ }
249
+
250
+ let out = rewrite_children(node, state, (child, key) =>
251
+ key === 'children' ? child : descend(child, state, 'expression'),
252
+ );
253
+
254
+ /** @type {AST.Node[]} */
255
+ const next_children = [];
256
+ let changed = false;
257
+ for (const child of /** @type {AST.Node[]} */ (node.children)) {
258
+ if (!is_ast_node(child)) {
259
+ next_children.push(child);
260
+ continue;
261
+ }
262
+ if (scope && is_style_element(child) && own.includes(child)) {
263
+ changed = true;
264
+ if (state.ctx.typeOnly) next_children.push(type_only_style(child));
265
+ continue;
266
+ }
267
+ const stamped = scope ? stamp(child, scope, state) : child;
268
+ const next = is_native_template_node(stamped)
269
+ ? descend_template_children(stamped, state)
270
+ : is_style_element(stamped)
271
+ ? stamped
272
+ : descend(stamped, state, 'statement');
273
+ if (next !== child) changed = true;
274
+ next_children.push(next);
275
+ }
276
+ if (changed) out = { ...out, children: next_children };
277
+ return out;
278
+ }
279
+
280
+ /**
281
+ * `@{ … }`: the setup statements are searched for nested templates and
282
+ * assigned blocks; the render slot holds the template. `ref` maps of the
283
+ * scopes inside the template are appended to the body, after the setup that
284
+ * declares their targets.
285
+ *
286
+ * @param {AST.JSXCodeBlock} node
287
+ * @param {StyleScopeState} state
288
+ * @returns {AST.JSXCodeBlock}
289
+ */
290
+ function process_code_block(node, state) {
291
+ const body = map_list(node.body, (item) => descend(item, state, 'statement'));
292
+ if (!node.render) return body === node.body ? node : { ...node, body };
293
+ const { statements, result: render } = with_ref_sink(state, () =>
294
+ descend_list_item(/** @type {AST.Node} */ (node.render), state),
295
+ );
296
+ if (body === node.body && render === node.render && statements.length === 0) return node;
297
+ return {
298
+ ...node,
299
+ body: /** @type {AST.Statement[]} */ ([...body, ...statements]),
300
+ render,
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Each branch body of a directive is a statement list holding one output node.
306
+ *
307
+ * @param {AST.JSXTemplateDirective} node
308
+ * @param {StyleScopeState} state
309
+ * @returns {AST.JSXTemplateDirective}
310
+ */
311
+ function process_directive(node, state) {
312
+ return rewrite_children(node, state, (child, key) => {
313
+ if (child.type === 'BlockStatement') return process_block_body(child, state);
314
+ if (child.type === 'CatchClause') {
315
+ const body = process_block_body(child.body, state);
316
+ return body === child.body ? child : { ...child, body };
317
+ }
318
+ if (child.type === 'SwitchCase') {
319
+ const consequent = process_statements(child.consequent, state);
320
+ return consequent === child.consequent
321
+ ? child
322
+ : { ...child, consequent: /** @type {AST.Statement[]} */ (consequent) };
323
+ }
324
+ // `@else if (…) { … }` parses as a plain `IfStatement` alternate; its
325
+ // bodies are branch bodies like the directive's own.
326
+ if (key === 'alternate' && (is_template_directive(child) || child.type === 'IfStatement')) {
327
+ return process_directive(/** @type {AST.JSXTemplateDirective} */ (child), state);
328
+ }
329
+ return descend(child, state, 'expression');
330
+ });
331
+ }
332
+
333
+ /**
334
+ * @param {AST.BlockStatement} block
335
+ * @param {StyleScopeState} state
336
+ * @returns {AST.BlockStatement}
337
+ */
338
+ function process_block_body(block, state) {
339
+ const body = process_statements(block.body, state);
340
+ return body === block.body ? block : { ...block, body: /** @type {AST.Statement[]} */ (body) };
341
+ }
342
+
343
+ /**
344
+ * A native element/fragment in expression or statement position: its own
345
+ * children list is treated like any other, and the `ref` maps of the scopes
346
+ * inside it are left on its metadata because it has no statement slot of its
347
+ * own — the lowering that turns the root into statements picks them up.
348
+ *
349
+ * @template {AST.TSRXJSXElement | AST.TSRXJSXFragment} T
350
+ * @param {T} node
351
+ * @param {StyleScopeState} state
352
+ * @returns {T}
353
+ */
354
+ function process_root(node, state) {
355
+ const { statements, result: out } = with_ref_sink(state, () =>
356
+ descend_template_children(node, state),
357
+ );
358
+ if (statements.length > 0) {
359
+ const previous = out.metadata.tsrx_style_ref_statements ?? [];
360
+ out.metadata.tsrx_style_ref_statements = [...previous, ...statements];
361
+ }
362
+ return out;
363
+ }
364
+
365
+ /**
366
+ * Run `fn` with a fresh `ref` sink and return what it collected.
367
+ *
368
+ * @template T
369
+ * @param {StyleScopeState} state
370
+ * @param {() => T} fn
371
+ * @returns {{ statements: AST.Statement[], result: T }}
372
+ */
373
+ function with_ref_sink(state, fn) {
374
+ const previous = state.ref_sink;
375
+ /** @type {AST.Statement[]} */
376
+ const statements = [];
377
+ state.ref_sink = statements;
378
+ try {
379
+ return { statements, result: fn() };
380
+ } finally {
381
+ state.ref_sink = previous;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * A statement list holding setup statements and at most one output node:
387
+ * the `ref` maps of the scopes inside the output node surface in the slot
388
+ * before it.
389
+ *
390
+ * @param {AST.Node[]} nodes
391
+ * @param {StyleScopeState} state
392
+ * @returns {AST.Node[]}
393
+ */
394
+ function process_statements(nodes, state) {
395
+ /** @type {AST.Node[][]} */
396
+ const out = nodes.map((item) => {
397
+ if (!is_render_item(item)) return [descend(item, state, 'statement')];
398
+ const { statements, result } = with_ref_sink(state, () => descend_list_item(item, state));
399
+ return [...statements, result];
400
+ });
401
+ const changed = out.some((parts, i) => parts.length !== 1 || parts[0] !== nodes[i]);
402
+ return changed ? out.flat() : nodes;
403
+ }
404
+
405
+ /**
406
+ * @param {AST.Node} item
407
+ * @param {StyleScopeState} state
408
+ * @returns {AST.Node}
409
+ */
410
+ function descend_list_item(item, state) {
411
+ if (is_native_template_node(item)) return descend_template_children(item, state);
412
+ if (is_style_element(item)) return item;
413
+ return descend(item, state, 'statement');
414
+ }
415
+
416
+ /**
417
+ * The standalone blocks a children list owns: its own style items, in source
418
+ * order. Blocks inside the items' subtrees belong to the lists they sit in.
419
+ *
420
+ * @param {AST.Node[]} nodes
421
+ * @returns {AST.JSXStyleElement[]}
422
+ */
423
+ export function collect_own_blocks(nodes) {
424
+ return nodes
425
+ .filter(is_style_element)
426
+ .sort((a, b) => /** @type {number} */ (a.start) - /** @type {number} */ (b.start));
427
+ }
428
+
429
+ /**
430
+ * Render a scope's sheets and compute what its elements carry.
431
+ *
432
+ * @param {AST.JSXStyleElement[]} own
433
+ * @param {AST.Node[]} render_items the list's items (and, through them, their subtrees)
434
+ * @param {AST.Node} holder the node whose metadata accumulates the scope's class
435
+ * map: the scope's first block, so two scopes never share a map
436
+ * @param {StyleScopeState} state
437
+ * @returns {ScopeStyles}
438
+ */
439
+ function prepare_scope(own, render_items, holder, state) {
440
+ const { ctx } = state;
441
+ /** @type {Array<[AST.JSXStyleElement, AST.CSS.StyleSheet]>} */
442
+ const sheets = [];
443
+ for (const block of own) {
444
+ const sheet = get_style_element_stylesheet(block);
445
+ if (sheet) sheets.push([block, sheet]);
446
+ }
447
+ const hash = sheets.length > 0 ? sheets[0][1].hash : null;
448
+ const refs = collect_style_ref_attributes(own);
449
+ const elements = collect_css_prunable_elements(render_items, [], ctx);
450
+
451
+ /** @type {AST.CSS.StyleSheet | null} */
452
+ let first_sheet = null;
453
+ for (const [block, sheet] of sheets) {
454
+ const region_hash = sheet.hash;
455
+ sheet.hash = /** @type {string} */ (hash);
456
+ if (!analyze_scope_css(block, sheet, state)) continue;
457
+ apply_css_definition_metadata(holder, sheet, elements, refs.length > 0, region_hash);
458
+ ctx.stylesheets.push(sheet);
459
+ first_sheet ??= sheet;
460
+ }
461
+
462
+ const applied = applies_of(own, state);
463
+
464
+ /** @type {AST.Statement[]} */
465
+ let ref_statements = [];
466
+ if (refs.length > 0) {
467
+ ref_statements = create_style_ref_setup_statements(
468
+ refs,
469
+ create_style_class_map(holder, first_sheet, { applied, hash }),
470
+ {
471
+ allowMutableRefTarget: ctx.platform.jsx.multiRefStrategy === 'array',
472
+ createTempIdentifier: () => create_generated_identifier(create_style_ref_temp_name(ctx)),
473
+ },
474
+ );
475
+ }
476
+
477
+ return { hash, applied, ref_statements };
478
+ }
479
+
480
+ /**
481
+ * @param {AST.JSXStyleElement[]} blocks
482
+ * @param {StyleScopeState} state
483
+ * @returns {Array<string | AST.Expression>}
484
+ */
485
+ function applies_of(blocks, state) {
486
+ return blocks.flatMap((block) => resolve_style_applies(block, state));
487
+ }
488
+
489
+ /**
490
+ * @param {TransformContext} ctx
491
+ * @returns {string}
492
+ */
493
+ function create_style_ref_temp_name(ctx) {
494
+ ctx.local_statement_component_index += 1;
495
+ return `_tsrx_style_ref_${ctx.local_statement_component_index}`;
496
+ }
497
+
498
+ /**
499
+ * `analyze_css` reports `:global` placement through a coded fatal error with
500
+ * CSS-relative positions; re-anchor it on the block so editors can place it,
501
+ * and keep going in collect mode.
502
+ *
503
+ * @param {AST.JSXStyleElement} block
504
+ * @param {AST.CSS.StyleSheet} sheet
505
+ * @param {StyleScopeState} state
506
+ * @returns {boolean} whether the sheet is usable
507
+ */
508
+ function analyze_scope_css(block, sheet, state) {
509
+ try {
510
+ analyze_css(sheet);
511
+ return true;
512
+ } catch (thrown) {
513
+ const compile_error = /** @type {CompileError} */ (thrown);
514
+ if (compile_error?.code !== DIAGNOSTIC_CODES.CSS_GLOBAL_PLACEMENT) throw thrown;
515
+ error(
516
+ compile_error.message,
517
+ state.ctx.filename,
518
+ block,
519
+ state.ctx.collect ? state.ctx.errors : undefined,
520
+ state.ctx.comments,
521
+ compile_error.code,
522
+ );
523
+ return false;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Prune the sheet against the scope's elements and record the class map the
529
+ * scope exposes (through `ref`) on the holder's metadata.
530
+ *
531
+ * @param {AST.Node} holder
532
+ * @param {AST.CSS.StyleSheet} css
533
+ * @param {AST.TSRXJSXElement[]} elements
534
+ * @param {boolean} export_top_scoped_classes
535
+ * @param {string} region_hash
536
+ * @returns {void}
537
+ */
538
+ export function apply_css_definition_metadata(
539
+ holder,
540
+ css,
541
+ elements,
542
+ export_top_scoped_classes,
543
+ region_hash = css.hash,
544
+ ) {
545
+ const metadata = holder.metadata || (holder.metadata = { path: [] });
546
+ const style_classes = metadata.styleClasses || (metadata.styleClasses = new Map());
547
+ const top_scoped_classes = metadata.topScopedClasses || new Map();
548
+
549
+ const prune = () => {
550
+ for (const element of elements) {
551
+ prune_css(css, element, style_classes, top_scoped_classes, region_hash);
552
+ }
553
+ };
554
+
555
+ prune();
556
+
557
+ if (export_top_scoped_classes) {
558
+ for (const [class_name, class_info] of top_scoped_classes) {
559
+ style_classes.set(class_name, class_info.selector ?? class_info);
560
+ }
561
+ prune();
562
+ }
563
+
564
+ if (top_scoped_classes.size > 0) {
565
+ metadata.topScopedClasses = top_scoped_classes;
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Pruning runs before the walker stamps paths onto template nodes, so each
571
+ * collected element gets its ancestor chain (`metadata.path`) here —
572
+ * descendant/sibling selector matching in `prune_css` reads it. The scope's
573
+ * own elements and those of nested scopes are collected; a component
574
+ * boundary stops the walk.
575
+ *
576
+ * @param {AST.Node | AST.Node[]} value
577
+ * @param {AST.TSRXJSXElement[]} [elements]
578
+ * @param {TransformContext | null} [transform_context]
579
+ * @param {AST.Node[]} [path]
580
+ * @returns {AST.TSRXJSXElement[]}
581
+ */
582
+ export function collect_css_prunable_elements(
583
+ value,
584
+ elements = [],
585
+ transform_context = null,
586
+ path = [],
587
+ ) {
588
+ if (Array.isArray(value)) {
589
+ for (const child of value) {
590
+ collect_css_prunable_elements(child, elements, transform_context, path);
591
+ }
592
+ return elements;
593
+ }
594
+
595
+ if (is_function_node(value) && value.metadata?.tsrx_dynamic_wrapper !== true) {
596
+ return elements;
597
+ }
598
+
599
+ if (is_style_host_element(value)) return elements;
600
+
601
+ if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
602
+ if (!is_style_element(value)) {
603
+ set_node_path_metadata(value, path);
604
+ elements.push(value);
605
+ }
606
+ }
607
+
608
+ const child_path = [...path, value];
609
+
610
+ for (const child of child_nodes(value, 'css')) {
611
+ collect_css_prunable_elements(child, elements, transform_context, child_path);
612
+ }
613
+
614
+ return elements;
615
+ }
616
+
617
+ /**
618
+ * The class parts a block's `apply` contributes: a literal for a same-module
619
+ * theme whose class is statically known, otherwise a runtime `<target>.$class`
620
+ * read. Memoized on the block's metadata for the assigned-block lowering.
621
+ *
622
+ * @param {AST.JSXStyleElement} block
623
+ * @param {StyleScopeState} state
624
+ * @returns {Array<string | AST.Expression>}
625
+ */
626
+ export function resolve_style_applies(block, state) {
627
+ if (block.metadata.tsrx_style_class_parts) return block.metadata.tsrx_style_class_parts;
628
+ /** @type {Array<string | AST.Expression>} */
629
+ const parts = [];
630
+ for (const resolution of block.metadata.styleApplies ?? []) {
631
+ const static_class = resolution.target ? static_style_class(resolution.target, state) : null;
632
+ if (static_class !== null) {
633
+ // One entry per hash so a diamond (`a` applied directly and through
634
+ // `b`) stamps each hash once (D12, static dedupe).
635
+ for (const hash of static_class.split(' ')) {
636
+ if (hash && !parts.includes(hash)) parts.push(hash);
637
+ }
638
+ continue;
639
+ }
640
+ parts.push(
641
+ b.member(clone_ast_node(resolution.expression, !state.ctx.typeOnly), b.id('$class')),
642
+ );
643
+ }
644
+ block.metadata.tsrx_style_class_parts = parts;
645
+ return parts;
646
+ }
647
+
648
+ /**
649
+ * The `$class` value of an assigned block when every applied theme in its
650
+ * chain is a same-module block: applied classes first, own hash last (D6).
651
+ *
652
+ * @param {AST.JSXStyleElement} block
653
+ * @param {StyleScopeState} state
654
+ * @returns {string | null}
655
+ */
656
+ export function static_style_class(block, state) {
657
+ const cached = state.static_classes.get(block);
658
+ if (cached !== undefined) return cached;
659
+ /** @type {string[]} */
660
+ const parts = [];
661
+ /** @type {string | null} */
662
+ let result = '';
663
+ for (const resolution of block.metadata.styleApplies ?? []) {
664
+ const applied = resolution.target ? static_style_class(resolution.target, state) : null;
665
+ if (applied === null) {
666
+ result = null;
667
+ break;
668
+ }
669
+ for (const hash of applied.split(' ')) {
670
+ if (hash && !parts.includes(hash)) parts.push(hash);
671
+ }
672
+ }
673
+ if (result !== null) {
674
+ const sheet = get_style_element_stylesheet(block);
675
+ if (sheet && !parts.includes(sheet.hash)) parts.push(sheet.hash);
676
+ result = parts.join(' ');
677
+ }
678
+ state.static_classes.set(block, result);
679
+ return result;
680
+ }
681
+
682
+ /**
683
+ * Render an assigned block's sheet at its declaration position so it lands
684
+ * in lexical order with the scopes around it; the walker's visitor builds the
685
+ * class map object later from the same sheet.
686
+ *
687
+ * @param {AST.JSXStyleElement} node
688
+ * @param {StyleScopeState} state
689
+ * @returns {void}
690
+ */
691
+ function prepare_assigned_style(node, state) {
692
+ if (node.metadata.tsrx_style_prepared) return;
693
+ node.metadata.tsrx_style_prepared = true;
694
+ resolve_style_applies(node, state);
695
+ const sheet = get_style_element_stylesheet(node);
696
+ if (!sheet) return;
697
+ if (!analyze_scope_css(node, sheet, state)) return;
698
+ state.ctx.stylesheets.push(
699
+ prepare_stylesheet_for_render(
700
+ sheet,
701
+ node.metadata.styleKind === 'theme' ? 'theme' : 'class-map',
702
+ ),
703
+ );
704
+ }
705
+
706
+ /**
707
+ * Stamp the scope's classes on every element in the subtree, through nested
708
+ * scopes (they append their own later) but not through function boundaries.
709
+ *
710
+ * @template {AST.Node} T
711
+ * @param {T} node
712
+ * @param {ScopeStyles} scope
713
+ * @param {StyleScopeState} state
714
+ * @returns {T}
715
+ */
716
+ function stamp(node, scope, state) {
717
+ if (scope.hash === null && scope.applied.length === 0) return node;
718
+ return /** @type {T} */ (stamp_node(node, scope, state));
719
+ }
720
+
721
+ /**
722
+ * @param {AST.Node} node
723
+ * @param {ScopeStyles} scope
724
+ * @param {StyleScopeState} state
725
+ * @returns {AST.Node}
726
+ */
727
+ function stamp_node(node, scope, state) {
728
+ if (!is_ast_node(node)) return node;
729
+ if (is_function_node(node) && node.metadata?.tsrx_dynamic_wrapper !== true) return node;
730
+ if (is_style_element(node) || is_style_host_element(node)) return node;
731
+
732
+ // Composite components get no hash (their host elements belong to their own
733
+ // scope); parser-native dynamic tags (`<{expr}>`) render host elements.
734
+ /** @type {AST.Node} */
735
+ const out =
736
+ node.type === 'JSXElement' && (!is_composite_jsx_element(node) || node.metadata?.dynamicElement)
737
+ ? add_scope_classes(
738
+ /** @type {AST.TSRXJSXElement} */ (node),
739
+ scope.hash ? [scope.hash] : [],
740
+ scope.applied,
741
+ state.class_attr_name,
742
+ )
743
+ : node;
744
+ return rewrite_children(out, state, (child) => stamp_node(child, scope, state));
745
+ }
746
+
747
+ /**
748
+ * `<style>{css}</style>` is an ordinary host element (Rule C): it holds no
749
+ * scoped CSS, so it is neither stamped nor matched by selectors, like a
750
+ * `<style>` block.
751
+ *
752
+ * @param {AST.Node} node
753
+ * @returns {boolean}
754
+ */
755
+ function is_style_host_element(node) {
756
+ return (
757
+ node.type === 'JSXElement' &&
758
+ !!node.metadata?.native_tsrx &&
759
+ node.openingElement.name.type === 'JSXIdentifier' &&
760
+ node.openingElement.name.name === 'style'
761
+ );
762
+ }
763
+
764
+ /**
765
+ * The synthesized `$class` read of a type-only `apply` target borrows the
766
+ * target's position for verification only, so a TypeScript error on it (the
767
+ * target is not a style object) lands on the authored identifier rather than
768
+ * on the closing brace, while hover and navigation on that identifier stay
769
+ * the target's own.
770
+ *
771
+ * @param {AST.Node} target
772
+ * @returns {AST.Identifier}
773
+ */
774
+ function type_only_class_read(target) {
775
+ const id = b.id('$class', has_location(target) ? target : undefined);
776
+ if (has_location(target)) {
777
+ id.metadata = {
778
+ ...id.metadata,
779
+ verify_only: true,
780
+ source_length: target.end - target.start,
781
+ };
782
+ }
783
+ return id;
784
+ }
785
+
786
+ /**
787
+ * The type-only stand-in for a scoped block: no CSS body, and `apply`
788
+ * rewritten to a `data-` attribute reading `$class` of each target so
789
+ * TypeScript checks the target is a style object while the tag keeps its
790
+ * source position.
791
+ *
792
+ * @param {AST.JSXStyleElement} block
793
+ * @returns {AST.JSXStyleElement}
794
+ */
795
+ export function type_only_style(block) {
796
+ const attributes = block.openingElement.attributes.map((attr) => {
797
+ if (
798
+ attr.type !== 'JSXAttribute' ||
799
+ attr.name.type !== 'JSXIdentifier' ||
800
+ attr.name.name !== 'apply' ||
801
+ attr.value?.type !== 'JSXExpressionContainer' ||
802
+ attr.value.expression.type === 'JSXEmptyExpression'
803
+ ) {
804
+ return attr;
805
+ }
806
+ const expression = attr.value.expression;
807
+ const value =
808
+ expression.type === 'ArrayExpression'
809
+ ? b.array(
810
+ expression.elements.map((element) =>
811
+ element && element.type !== 'SpreadElement'
812
+ ? b.member(clone_ast_node(element), type_only_class_read(element))
813
+ : element,
814
+ ),
815
+ )
816
+ : b.member(clone_ast_node(expression), type_only_class_read(expression));
817
+ // The renamed attribute is synthesized text: leave it unmapped so the
818
+ // authored `apply` token serves no misleading hover.
819
+ return {
820
+ ...attr,
821
+ name: b.jsx_id('data-tsrx-apply'),
822
+ value: { ...attr.value, expression: value },
823
+ };
824
+ });
825
+ // An unclosed `<style>` has no closing element. The stand-in still has to
826
+ // be valid TSX or the virtual file loses every mapping after the tag.
827
+ // Self-closing apply tags already print as `<style … />` — leave those.
828
+ const closingElement =
829
+ block.closingElement ??
830
+ (block.openingElement.selfClosing
831
+ ? null
832
+ : b.jsx_closing_element(
833
+ clone_ast_node(block.openingElement.name),
834
+ has_location(block.openingElement) ? block.openingElement : undefined,
835
+ ));
836
+ return {
837
+ ...block,
838
+ children: [],
839
+ openingElement: { ...block.openingElement, attributes },
840
+ closingElement,
841
+ };
842
+ }