@tsrx/core 0.1.64 → 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,519 @@
1
+ /**
2
+ * Table-driven parser spec for the RFC "sibling-scoped `<style>` blocks,
3
+ * `$class`, `apply`" syntax. It is replayed by `tests/utils/parser.test.js`
4
+ * against the acorn parser and doubles as the porting spec for the Rust parser
5
+ * (`oxc-tsrx`), so it is deliberately dependency-free: every case is plain
6
+ * data plus a tiny `locate(ast)` function that walks from the `Program` to the
7
+ * node whose shape is described.
8
+ *
9
+ * Shape vocabulary (all plain objects, matched structurally by the test):
10
+ *
11
+ * - `{ type: 'JSXStyleElement', selfClosing, attributes, apply?, children, css,
12
+ * hasScopeHash, closingElement }`
13
+ * - `attributes`: attribute names on `openingElement.attributes`, in order.
14
+ * - `apply`: the node type of the `apply={…}` expression, when present.
15
+ * - `children`: `[]` for a self-closed block, `['StyleSheet']` for a body.
16
+ * - `css`: the raw CSS text (`''` when self-closed).
17
+ * - `hasScopeHash`: whether `metadata.styleScopeHash` is set.
18
+ * - `closingElement`: whether `closingElement` is present.
19
+ * - `{ type: 'JSXElement', name, children? }` / `{ type: 'JSXFragment', children }`
20
+ * - `children`, when given, lists the element's children in order. A
21
+ * `<style>` whose first non-whitespace child character is `{` is an
22
+ * ordinary `JSXElement` named `style` with expression-container children
23
+ * (plain-TSX `<style>{css}</style>`), never a `JSXStyleElement`.
24
+ * - `{ type: 'JSXCodeBlock', body, render }` where `body` lists the setup
25
+ * statements in source order and `render` is the single output node or
26
+ * `null`. A `<style>` block is an output node: beside another output it is
27
+ * the multiple-outputs error, and the valid placement is inside a fragment
28
+ * or element.
29
+ * - `{ type: 'JSXIfExpression', consequent, alternate }`,
30
+ * `{ type: 'JSXForExpression', body, empty }`,
31
+ * `{ type: 'JSXSwitchExpression', cases: [{ test, consequent }] }`,
32
+ * `{ type: 'JSXTryExpression', block, pending, handler }`
33
+ * where each clause is the statement list of its block (`null` when absent).
34
+ * - Any other `{ type }` matches on `type` alone (e.g. a setup statement or a
35
+ * `JSXExpressionContainer` child).
36
+ *
37
+ * Negative cases carry `error: { message, start, end }` instead of `expected`
38
+ * (or alongside it, when the recovered tree is also specified): `start`/`end`
39
+ * are the offsets of the reported range in `source`.
40
+ *
41
+ * @typedef {{
42
+ * type: 'JSXStyleElement',
43
+ * selfClosing: boolean,
44
+ * attributes: string[],
45
+ * apply?: string,
46
+ * children: string[],
47
+ * css: string,
48
+ * hasScopeHash: boolean,
49
+ * closingElement: boolean,
50
+ * }} StyleShape
51
+ * @typedef {{ type: 'JSXElement', name: string, children?: Shape[] }} ElementShape
52
+ * @typedef {{ type: 'JSXFragment', children: Shape[] }} FragmentShape
53
+ * @typedef {{ type: 'JSXCodeBlock', body: Shape[], render: Shape | null }} CodeBlockShape
54
+ * @typedef {{ type: 'JSXIfExpression', consequent: Shape[], alternate: Shape[] | null }} IfShape
55
+ * @typedef {{ type: 'JSXForExpression', body: Shape[], empty: Shape[] | null }} ForShape
56
+ * @typedef {{ test: string | null, consequent: Shape[] }} SwitchCaseShape
57
+ * @typedef {{ type: 'JSXSwitchExpression', cases: SwitchCaseShape[] }} SwitchShape
58
+ * @typedef {{
59
+ * type: 'JSXTryExpression',
60
+ * block: Shape[],
61
+ * pending: Shape[] | null,
62
+ * handler: Shape[] | null,
63
+ * }} TryShape
64
+ * @typedef {{ type: 'VariableDeclaration' | 'ExpressionStatement' | 'JSXExpressionContainer' }} StatementShape
65
+ * @typedef {StyleShape
66
+ * | ElementShape
67
+ * | FragmentShape
68
+ * | CodeBlockShape
69
+ * | IfShape
70
+ * | ForShape
71
+ * | SwitchShape
72
+ * | TryShape
73
+ * | StatementShape} Shape
74
+ *
75
+ * The parsed `Program` is walked untyped on purpose: the table must not depend
76
+ * on the JS parser's AST typings so it can be lifted verbatim into an issue.
77
+ * @typedef {(ast: any) => unknown} Locate
78
+ * @typedef {{ message: string, start?: number, end?: number }} ExpectedError
79
+ * @typedef {{ name: string, source: string, locate: Locate, expected: Shape, error?: undefined }} PositiveCase
80
+ * @typedef {{ name: string, source: string, locate: Locate, error: ExpectedError, expected?: Shape }} NegativeCase
81
+ * @typedef {PositiveCase | NegativeCase} StyleSyntaxCase
82
+ */
83
+
84
+ /**
85
+ * A self-closed `<style … />`: no CSS body, no scope hash.
86
+ *
87
+ * @param {string[]} [attributes]
88
+ * @param {string} [apply]
89
+ * @returns {StyleShape}
90
+ */
91
+ const style_self = (attributes = [], apply = undefined) => ({
92
+ type: 'JSXStyleElement',
93
+ selfClosing: true,
94
+ attributes,
95
+ ...(apply ? { apply } : {}),
96
+ children: [],
97
+ css: '',
98
+ hasScopeHash: false,
99
+ closingElement: false,
100
+ });
101
+
102
+ /**
103
+ * A bodied `<style>…</style>`: parsed sheet, raw CSS, scope hash.
104
+ *
105
+ * @param {string} css
106
+ * @param {string[]} [attributes]
107
+ * @param {string} [apply]
108
+ * @returns {StyleShape}
109
+ */
110
+ const style_body = (css, attributes = [], apply = undefined) => ({
111
+ type: 'JSXStyleElement',
112
+ selfClosing: false,
113
+ attributes,
114
+ ...(apply ? { apply } : {}),
115
+ children: ['StyleSheet'],
116
+ css,
117
+ hasScopeHash: true,
118
+ closingElement: true,
119
+ });
120
+
121
+ /**
122
+ * @param {string} name
123
+ * @param {Shape[]} [children]
124
+ * @returns {ElementShape}
125
+ */
126
+ const element = (name, children = undefined) => ({
127
+ type: 'JSXElement',
128
+ name,
129
+ ...(children ? { children } : {}),
130
+ });
131
+
132
+ /**
133
+ * The ordinary TSX `<style>{css}</style>`: a `JSXElement` named `style` whose
134
+ * children are the listed expression containers.
135
+ *
136
+ * @param {number} [containers]
137
+ * @returns {ElementShape}
138
+ */
139
+ const style_host = (containers = 1) =>
140
+ element(
141
+ 'style',
142
+ Array.from({ length: containers }, () => ({ type: 'JSXExpressionContainer' })),
143
+ );
144
+
145
+ /**
146
+ * @param {Shape[]} body
147
+ * @param {Shape | null} render
148
+ * @returns {CodeBlockShape}
149
+ */
150
+ const code_block = (body, render) => ({ type: 'JSXCodeBlock', body, render });
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Locators: walk from the parsed `Program` to the node under test.
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /**
157
+ * The `JSXCodeBlock` body of `function App() @{ … }` (the first statement).
158
+ * @type {(ast: any) => any}
159
+ */
160
+ const component_block = (ast) => ast.body[0].body;
161
+
162
+ /**
163
+ * The argument of `return` in `function App() { return …; }`.
164
+ * @type {(ast: any) => any}
165
+ */
166
+ const returned = (ast) => ast.body[0].body.body[0].argument;
167
+
168
+ /**
169
+ * The initializer of `export const x = …;` (the first statement).
170
+ * @type {Locate}
171
+ */
172
+ const exported_init = (ast) => ast.body[0].declaration.declarations[0].init;
173
+
174
+ /**
175
+ * The program's first statement.
176
+ * @type {Locate}
177
+ */
178
+ const first_statement = (ast) => ast.body[0];
179
+
180
+ const CSS = '.a { color: red; }';
181
+
182
+ /** @type {StyleSyntaxCase[]} */
183
+ export const STYLE_SYNTAX_CASES = [
184
+ // -- self-closing forms ---------------------------------------------------
185
+ {
186
+ name: 'self-closing <style /> without apply',
187
+ source: `function App() @{ <><style /><div /></> }`,
188
+ locate: (ast) => component_block(ast).render.children[0],
189
+ expected: style_self(),
190
+ },
191
+ {
192
+ name: 'self-closing <style apply={theme} />',
193
+ source: `function App() @{ <><style apply={theme} /><div /></> }`,
194
+ locate: (ast) => component_block(ast).render.children[0],
195
+ expected: style_self(['apply'], 'Identifier'),
196
+ },
197
+ {
198
+ name: 'self-closing <style apply={[a, b]} />',
199
+ source: `function App() @{ <><style apply={[a, b]} /><div /></> }`,
200
+ locate: (ast) => component_block(ast).render.children[0],
201
+ expected: style_self(['apply'], 'ArrayExpression'),
202
+ },
203
+ {
204
+ name: 'self-closing <style apply={ns.dark} />',
205
+ source: `function App() @{ <><style apply={ns.dark} /><div /></> }`,
206
+ locate: (ast) => component_block(ast).render.children[0],
207
+ expected: style_self(['apply'], 'MemberExpression'),
208
+ },
209
+ {
210
+ name: 'self-closing <style ref={r} apply={theme} /> keeps every attribute in order',
211
+ source: `function App() @{ <><style ref={r} apply={theme} /><div /></> }`,
212
+ locate: (ast) => component_block(ast).render.children[0],
213
+ expected: style_self(['ref', 'apply'], 'Identifier'),
214
+ },
215
+
216
+ // -- bodied forms -----------------------------------------------------------
217
+ {
218
+ name: 'bodied <style apply={t}>…</style> keeps its sheet, css, scope hash and apply',
219
+ source: `function App() @{ <><style apply={t}>${CSS}</style><div /></> }`,
220
+ locate: (ast) => component_block(ast).render.children[0],
221
+ expected: style_body(CSS, ['apply'], 'Identifier'),
222
+ },
223
+ {
224
+ name: 'bodied <style>…</style> without attributes',
225
+ source: `function App() { return <style>${CSS}</style>; }`,
226
+ locate: returned,
227
+ expected: style_body(CSS),
228
+ },
229
+
230
+ // -- `@{ … }` code-block bodies ---------------------------------------------
231
+ // A `@{ … }` body holds setup statements and exactly ONE output node, and a
232
+ // `<style>` block is an output node like any other: beside another output
233
+ // it is the ordinary multiple-outputs error (reported on the later node),
234
+ // and as the lone output it parses but the analyzer reports
235
+ // `tsrx-style-standalone-needs-fragment`. The valid placement is inside a
236
+ // fragment or element (see the fragment forms below).
237
+ {
238
+ name: 'style before the output node in a @{} body is the multiple-outputs error',
239
+ // 0 1 2 3 4 5
240
+ // 0123456789012345678901234567890123456789012345678901234567
241
+ source: `function App() @{ const x = 1; <style apply={a} /> <div /> }`,
242
+ locate: component_block,
243
+ error: {
244
+ message:
245
+ "A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
246
+ start: 51,
247
+ end: 58,
248
+ },
249
+ // Recovery: the last output node becomes `render`; the block stays in
250
+ // `body` in source order.
251
+ expected: code_block(
252
+ [{ type: 'VariableDeclaration' }, style_self(['apply'], 'Identifier')],
253
+ element('div'),
254
+ ),
255
+ },
256
+ {
257
+ name: 'style after the output node in a @{} body is the multiple-outputs error',
258
+ // 0 1 2 3 4 5
259
+ // 0123456789012345678901234567890123456789012345678901234567890
260
+ source: `function App() @{ <div /> <style>${CSS}</style> }`,
261
+ locate: component_block,
262
+ error: {
263
+ message:
264
+ "A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
265
+ start: 26,
266
+ end: 59,
267
+ },
268
+ expected: code_block([element('div')], style_body(CSS)),
269
+ },
270
+ {
271
+ name: 'only a style and no output node in a @{} body parses (the analyzer rejects it)',
272
+ source: `function App() @{ <style>${CSS}</style> }`,
273
+ locate: component_block,
274
+ expected: code_block([], style_body(CSS)),
275
+ },
276
+ {
277
+ name: 'a fragment holding the style and the output node is the valid @{} form',
278
+ source: `function App() @{ const x = 1; <><style apply={a} /><div /></> }`,
279
+ locate: component_block,
280
+ expected: code_block([{ type: 'VariableDeclaration' }], {
281
+ type: 'JSXFragment',
282
+ children: [style_self(['apply'], 'Identifier'), element('div')],
283
+ }),
284
+ },
285
+ {
286
+ name: 'nested @{} with its own style inside a fragment',
287
+ source: `function App() @{ <><style apply={a} /><div>@{ <><style apply={b} /><span /></> }</div></> }`,
288
+ locate: (ast) => component_block(ast).render.children[1].children[0],
289
+ expected: code_block([], {
290
+ type: 'JSXFragment',
291
+ children: [style_self(['apply'], 'Identifier'), element('span')],
292
+ }),
293
+ },
294
+
295
+ {
296
+ name: 'assigned @{} block: a theme in its setup and an applying fragment output',
297
+ source: `const something = @{
298
+ const theme = <style>${CSS}</style>;
299
+ <>
300
+ <style apply={theme}>${CSS}</style>
301
+ <div />
302
+ </>
303
+ };`,
304
+ locate: (ast) => ast.body[0].declarations[0].init,
305
+ expected: code_block([{ type: 'VariableDeclaration' }], {
306
+ type: 'JSXFragment',
307
+ children: [style_body(CSS, ['apply'], 'Identifier'), element('div')],
308
+ }),
309
+ },
310
+
311
+ // -- directive bodies -------------------------------------------------------
312
+ // Every control-flow body renders one output node too: a block beside it
313
+ // is the multiple-outputs error, and the valid form wraps both in a
314
+ // fragment. (`@switch` case bodies do not report multiple outputs today,
315
+ // so only their fragment form is listed.)
316
+ {
317
+ name: 'style beside the output node in an @if consequent is the multiple-outputs error',
318
+ // 0 1 2 3 4 5
319
+ // 012345678901234567890123456789012345678901234567890123456
320
+ source: `function App() @{ @if (ok) { <style apply={a} /> <b /> } }`,
321
+ locate: (ast) => component_block(ast).render,
322
+ error: {
323
+ message:
324
+ "A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
325
+ start: 49,
326
+ end: 54,
327
+ },
328
+ expected: {
329
+ type: 'JSXIfExpression',
330
+ consequent: [style_self(['apply'], 'Identifier'), element('b')],
331
+ alternate: null,
332
+ },
333
+ },
334
+ {
335
+ name: 'only a style inside an @if consequent parses (the analyzer rejects it)',
336
+ source: `function App() @{ @if (ok) { <style apply={a} /> } }`,
337
+ locate: (ast) => component_block(ast).render,
338
+ expected: {
339
+ type: 'JSXIfExpression',
340
+ consequent: [style_self(['apply'], 'Identifier')],
341
+ alternate: null,
342
+ },
343
+ },
344
+ {
345
+ name: 'fragments holding style and output inside @if consequent and @else',
346
+ source: `function App() @{ @if (ok) { <><style apply={a} /><b /></> } @else { <><i /><style apply={c} /></> } }`,
347
+ locate: (ast) => component_block(ast).render,
348
+ expected: {
349
+ type: 'JSXIfExpression',
350
+ consequent: [
351
+ { type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('b')] },
352
+ ],
353
+ alternate: [
354
+ { type: 'JSXFragment', children: [element('i'), style_self(['apply'], 'Identifier')] },
355
+ ],
356
+ },
357
+ },
358
+ {
359
+ name: 'style beside the output node in a @for body is the multiple-outputs error',
360
+ // 0 1 2 3 4 5 6 7
361
+ // 012345678901234567890123456789012345678901234567890123456789012345678901
362
+ source: `function App() @{ @for (const x of xs) { <style apply={a} /> <b>{x}</b> } }`,
363
+ locate: (ast) => component_block(ast).render,
364
+ error: {
365
+ message:
366
+ "A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
367
+ start: 61,
368
+ end: 71,
369
+ },
370
+ expected: {
371
+ type: 'JSXForExpression',
372
+ body: [style_self(['apply'], 'Identifier'), element('b')],
373
+ empty: null,
374
+ },
375
+ },
376
+ {
377
+ name: 'fragments holding style and output inside @for body and @empty',
378
+ source: `function App() @{ @for (const x of xs) { <><style apply={a} /><b>{x}</b></> } @empty { <><i /><style apply={c} /></> } }`,
379
+ locate: (ast) => component_block(ast).render,
380
+ expected: {
381
+ type: 'JSXForExpression',
382
+ body: [
383
+ { type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('b')] },
384
+ ],
385
+ empty: [
386
+ { type: 'JSXFragment', children: [element('i'), style_self(['apply'], 'Identifier')] },
387
+ ],
388
+ },
389
+ },
390
+ {
391
+ name: 'fragments holding style and output inside @switch case and default',
392
+ source: `function App() @{ @switch (k) { @case 1: { <><style apply={a} /><b /></> } @default: { <><i /><style apply={c} /></> } } }`,
393
+ locate: (ast) => component_block(ast).render,
394
+ expected: {
395
+ type: 'JSXSwitchExpression',
396
+ cases: [
397
+ {
398
+ test: 'Literal',
399
+ consequent: [
400
+ {
401
+ type: 'JSXFragment',
402
+ children: [style_self(['apply'], 'Identifier'), element('b')],
403
+ },
404
+ ],
405
+ },
406
+ {
407
+ test: null,
408
+ consequent: [
409
+ {
410
+ type: 'JSXFragment',
411
+ children: [element('i'), style_self(['apply'], 'Identifier')],
412
+ },
413
+ ],
414
+ },
415
+ ],
416
+ },
417
+ },
418
+ {
419
+ name: 'style beside the output node in a @try block is the multiple-outputs error',
420
+ // 0 1 2 3 4 5
421
+ // 0123456789012345678901234567890123456789012345678901
422
+ source: `function App() @{ @try { <style apply={a} /> <b /> } @catch (e) { <i /> } }`,
423
+ locate: (ast) => component_block(ast).render,
424
+ error: {
425
+ message:
426
+ "A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
427
+ start: 45,
428
+ end: 50,
429
+ },
430
+ expected: {
431
+ type: 'JSXTryExpression',
432
+ block: [style_self(['apply'], 'Identifier'), element('b')],
433
+ pending: null,
434
+ handler: [element('i')],
435
+ },
436
+ },
437
+ {
438
+ name: 'fragments holding style and output inside @try, @pending and @catch',
439
+ source: `function App() @{ @try { <><style apply={a} /><b /></> } @pending { <><style apply={p} /><u /></> } @catch (e) { <><i /><style apply={c} /></> } }`,
440
+ locate: (ast) => component_block(ast).render,
441
+ expected: {
442
+ type: 'JSXTryExpression',
443
+ block: [
444
+ { type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('b')] },
445
+ ],
446
+ pending: [
447
+ { type: 'JSXFragment', children: [style_self(['apply'], 'Identifier'), element('u')] },
448
+ ],
449
+ handler: [
450
+ { type: 'JSXFragment', children: [element('i'), style_self(['apply'], 'Identifier')] },
451
+ ],
452
+ },
453
+ },
454
+
455
+ // -- fragments --------------------------------------------------------------
456
+ {
457
+ name: 'multiple sibling style blocks inside one fragment',
458
+ source: `function App() { return <><style apply={a} /><style>${CSS}</style><div /></>; }`,
459
+ locate: returned,
460
+ expected: {
461
+ type: 'JSXFragment',
462
+ children: [style_self(['apply'], 'Identifier'), style_body(CSS), element('div')],
463
+ },
464
+ },
465
+
466
+ // -- expression-child <style> (plain TSX) -----------------------------------
467
+ {
468
+ name: '<style>{css}</style> is an ordinary JSXElement in a plain-TSX return',
469
+ source: `function App() { return <style>{css}</style>; }`,
470
+ locate: returned,
471
+ expected: style_host(),
472
+ },
473
+ {
474
+ name: '<style>{css}</style> stays an ordinary JSXElement inside a @{} body',
475
+ source: `function App() @{ <section><style>{css}</style><div /></section> }`,
476
+ locate: (ast) => component_block(ast).render,
477
+ expected: element('section', [style_host(), element('div')]),
478
+ },
479
+ {
480
+ name: '<style> with whitespace before its first { child is an ordinary JSXElement',
481
+ source: `function App() { return <section><style>\n\t{css}\n</style></section>; }`,
482
+ locate: (ast) => returned(ast).children[0],
483
+ expected: style_host(),
484
+ },
485
+ {
486
+ name: '<style> with two expression children is an ordinary JSXElement',
487
+ source: `function App() { return <style>{reset}{theme}</style>; }`,
488
+ locate: returned,
489
+ expected: style_host(2),
490
+ },
491
+ {
492
+ name: 'a { inside CSS text after other text still parses as a bodied block',
493
+ source: `function App() @{ <><style>.a{color:red}</style><div /></> }`,
494
+ locate: (ast) => component_block(ast).render.children[0],
495
+ expected: style_body('.a{color:red}'),
496
+ },
497
+
498
+ // -- module scope -----------------------------------------------------------
499
+ {
500
+ name: 'module-scope assigned self-closed block',
501
+ source: `export const theme = <style apply={[a, b]} />;`,
502
+ locate: exported_init,
503
+ expected: style_self(['apply'], 'ArrayExpression'),
504
+ },
505
+ {
506
+ name: 'module-scope bare <style> still parses without a parser error',
507
+ source: `<style>${CSS}</style>;\nexport function App() { return <div />; }`,
508
+ locate: first_statement,
509
+ expected: style_body(CSS),
510
+ },
511
+
512
+ // -- <head> -----------------------------------------------------------------
513
+ {
514
+ name: 'head <style> has no scope hash',
515
+ source: `function App() { return <head><style>body { margin: 0; }</style></head>; }`,
516
+ locate: (ast) => returned(ast).children[0],
517
+ expected: { ...style_body('body { margin: 0; }'), hasScopeHash: false },
518
+ },
519
+ ];
package/types/index.d.ts CHANGED
@@ -154,6 +154,24 @@ export interface BaseNodeMetaData {
154
154
  delegated?: boolean;
155
155
  returned_tsrx_return?: AST.ReturnStatement;
156
156
  styleScopeHash?: string;
157
+ /** Resolved `apply` entries of a `<style>` block (set by the style analyzer). */
158
+ styleApplies?: StyleApplyResolution[];
159
+ /** An assigned block is the target of some `apply` in its module. */
160
+ styleApplied?: boolean;
161
+ /** An assigned block is exported from its module. */
162
+ styleExported?: boolean;
163
+ /** An assigned block's `$class` is read somewhere in its module (an element or a child prop opts into the theme). */
164
+ styleClassRead?: boolean;
165
+ /** How an assigned block renders: `theme` keeps every selector, `class-map` prunes (D4/D5). */
166
+ styleKind?: 'theme' | 'class-map';
167
+ /** The transform's style pre-pass already rendered this assigned block's sheet. */
168
+ tsrx_style_prepared?: boolean;
169
+ /** Resolved `$class` parts of a block's applied themes (literals or `theme.$class` reads). */
170
+ tsrx_style_class_parts?: Array<string | AST.Expression>;
171
+ /** Style `ref` setup statements for a scope whose root is this native fragment/element. */
172
+ tsrx_style_ref_statements?: AST.Statement[];
173
+ /** Accumulated scope classes of an element (see transform/scoping.js). */
174
+ tsrx_scope_class?: ScopeClassParts;
157
175
  css?: {
158
176
  scopedClasses: TopScopedClasses;
159
177
  hash: string;
@@ -173,6 +191,8 @@ export interface BaseNodeMetaData {
173
191
  /** The current var scope contains a lazy `var` binding in a JavaScript loop header. */
174
192
  has_lazy_var_loop_descendants?: boolean;
175
193
  disable_verification?: boolean;
194
+ /** Map this synthesized identifier's borrowed source span for diagnostics only (no hover/navigation). */
195
+ verify_only?: boolean;
176
196
  /** Identifiers whose source ranges also map to this generated identifier. */
177
197
  extra_source_mappings?: Array<(AST.Identifier | AST.PrivateIdentifier) & AST.NodeWithLocation>;
178
198
  generated_setup_declarations?: AST.Statement[];
@@ -452,6 +472,7 @@ declare module 'estree' {
452
472
 
453
473
  interface JSXCodeBlock extends AST.BaseExpression {
454
474
  type: 'JSXCodeBlock';
475
+ /** Setup statements plus any `<style>` siblings of the output node (D3), in source order. */
455
476
  body: AST.Statement[];
456
477
  render: AST.Node | null;
457
478
  metadata: BaseNodeMetaData;
@@ -460,6 +481,10 @@ declare module 'estree' {
460
481
 
461
482
  interface JSXStyleElement extends Omit<AST.TSRXJSXElement, 'type' | 'children'> {
462
483
  type: 'JSXStyleElement';
484
+ /**
485
+ * The parsed body, or empty for a self-closed `<style apply={…} />`
486
+ * (`openingElement.selfClosing`), which has no CSS and no scope hash.
487
+ */
463
488
  children: AST.CSS.StyleSheet[];
464
489
  css?: string;
465
490
  unclosed?: boolean;
@@ -755,6 +780,7 @@ declare module 'estree' {
755
780
 
756
781
  export namespace CSS {
757
782
  export interface BaseNode extends AST.NodeWithMaybeComments {
783
+ /** Offset into the style body (`StyleSheet.source`), not the file. */
758
784
  start: number;
759
785
  end: number;
760
786
  loc?: AST.SourceLocation;
@@ -765,6 +791,14 @@ declare module 'estree' {
765
791
  children: Array<Atrule | Rule>;
766
792
  source: string;
767
793
  hash: string;
794
+ /** The file the `<style>` body was parsed from. */
795
+ filename?: string;
796
+ /**
797
+ * File offset of `source[0]`, set when the sheet was parsed with a body
798
+ * origin (every sheet `parseModule` produces). `start` / `end` stay
799
+ * body-relative; `loc` is then the body's file-relative location.
800
+ */
801
+ sourceStart?: number;
768
802
  }
769
803
 
770
804
  export interface Atrule extends BaseNode {
@@ -1537,6 +1571,57 @@ export interface TSRXAnalysisResult {
1537
1571
  ast: AST.Program;
1538
1572
  errors: CompileError[];
1539
1573
  comments: AST.CommentWithLocation[];
1574
+ /** Module scope built by `createScopes` for the same program. */
1575
+ scope: ScopeInterface;
1576
+ scopes: Map<AST.Node, ScopeInterface>;
1577
+ styles: StyleAnalysis;
1578
+ }
1579
+
1580
+ /**
1581
+ * One resolved entry of a `<style apply={…}>` attribute. Holds AST nodes only
1582
+ * (no bindings, whose reference paths point back up the tree) so the analyzed
1583
+ * program stays acyclic for consumers that clone or serialize it.
1584
+ */
1585
+ export interface StyleApplyResolution {
1586
+ /** The authored entry (an identifier or member expression). */
1587
+ expression: AST.Expression;
1588
+ /** The same-module assigned block the entry names, or `null` for an import (runtime `$class`). */
1589
+ target: AST.JSXStyleElement | null;
1590
+ /** Whether the entry resolves to a same-module block or to an import. */
1591
+ kind: 'local' | 'import';
1592
+ }
1593
+
1594
+ /** Module-level summary produced by the style analyzer (`program.metadata.styles`). */
1595
+ export interface StyleAnalysis {
1596
+ /** `const theme = <style>…</style>` blocks, in source order. */
1597
+ assigned: AST.JSXStyleElement[];
1598
+ /** Blocks that scope the template they sit in, in source order. */
1599
+ standalone: AST.JSXStyleElement[];
1600
+ }
1601
+
1602
+ /** Options for the class map object built for an assigned or `ref`-exposed style block. */
1603
+ export interface StyleClassMapOptions {
1604
+ /** `$class` parts of applied themes, in order: literals for static classes, expressions for runtime reads. */
1605
+ applied?: Array<string | AST.Expression>;
1606
+ /** Override the own hash (`null` for a body-less `<style apply />`). */
1607
+ hash?: string | null;
1608
+ }
1609
+
1610
+ /** How `prepareStylesheetForRender` treats a sheet's selectors (D4). */
1611
+ export type StyleRenderMode = 'scope' | 'class-map' | 'theme';
1612
+
1613
+ /**
1614
+ * The classes a scope pre-pass stamped on one element, kept apart from the
1615
+ * authored value so nested scopes append to one attribute value instead of
1616
+ * nesting template literals: `base statics… applies…`.
1617
+ */
1618
+ export interface ScopeClassParts {
1619
+ /** The authored class value, if any. */
1620
+ base: AST.Expression | null;
1621
+ /** Scope hashes, outermost first. */
1622
+ hashes: string[];
1623
+ /** Applied theme classes: literals when statically known, else `theme.$class` reads. */
1624
+ applied: Array<string | AST.Expression>;
1540
1625
  }
1541
1626
 
1542
1627
  /**