@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
package/README.md CHANGED
@@ -64,7 +64,32 @@ here and keeps package docs focused on the core parser API.
64
64
  (`import`, `prop`, `let`, `const`, `function`, `for_pattern`, …).
65
65
  - **AST utilities** — pattern walkers, identifier extraction, builders, location
66
66
  helpers, obfuscation helpers.
67
- - **CSS support** — `parseStyle`, `analyzeCss`, `renderStylesheets`.
67
+ - **CSS support** — `parseStyle`, `analyzeCss`, `renderStylesheets`. CSS node
68
+ offsets are relative to the style body; a sheet parsed with a `body` origin
69
+ (every sheet `parseModule` produces) records `sourceStart` and a file-relative
70
+ `loc`, and `analyzeCss` anchors its `:global` placement diagnostics on the
71
+ selector with file-relative positions. Pass `{ errors, comments }` to collect
72
+ them instead of throwing.
73
+ - **Scoped styles** — `analyzeTsrx` resolves every `<style>` block. A standalone
74
+ block is a child of an element or fragment and is scoped to its siblings: it
75
+ styles the items beside it and everything below them, never the element that
76
+ contains it, and the compiler adds a hash class to those elements so the block's
77
+ selectors match only there. A block is an output node, so a block that is the
78
+ lone output of a `@{ … }` or control-flow body is
79
+ `STYLE_STANDALONE_NEEDS_FRAGMENT`. Raw CSS is TSRX template syntax, so a block
80
+ with CSS in it outside every `@{ … }`/control-flow body is
81
+ `STYLE_STANDALONE_OUTSIDE_TEMPLATE`; plain-TSX `<style>{css}</style>` is an
82
+ ordinary element. Assigned blocks (`const theme = <style>…</style>`) are
83
+ classified as `theme` (exported, applied, or `$class` read) or `class-map`, and
84
+ `apply` targets are resolved through real bindings, declared before use. Results
85
+ ride on each block's `metadata` (`styleKind`, `styleApplies`, `styleApplied`,
86
+ `styleExported`) and on `program.metadata.styles`, and the analysis result
87
+ exposes `scopes`. Target compilers use `prepareStylesheetForRender(sheet, mode)`
88
+ with `mode: 'scope' | 'class-map' | 'theme'` (a boolean still means
89
+ `class-map`/`scope`) and `createStyleClassMapFromStylesheet(sheet, options)`,
90
+ whose object starts with `$class` and accepts `{ applied }` for composed themes.
91
+ Style diagnostics use the `STYLE_*` and `CSS_GLOBAL_PLACEMENT` codes in
92
+ `DIAGNOSTIC_CODES`.
68
93
  - **HTML helpers** — `isVoidElement`, `isBooleanAttribute`, `isDomProperty`,
69
94
  `validateNesting`.
70
95
  - **Event helpers** — delegated-event utilities, event-name normalization.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.64",
6
+ "version": "0.1.66",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -66,7 +66,9 @@
66
66
  "./test-harness/dep-scan": "./tests/shared/dep-scan.js",
67
67
  "./test-harness/runtime/*": "./tests/shared/runtime/*.js",
68
68
  "./test-harness/runtime/*.js": "./tests/shared/runtime/*.js",
69
- "./test-harness/runtime/*.tsrx": "./tests/shared/runtime/*.tsrx"
69
+ "./test-harness/runtime/*.tsrx": "./tests/shared/runtime/*.tsrx",
70
+ "./test-harness/scoped-styles-fixtures": "./tests/shared/scoped-styles-fixtures.js",
71
+ "./test-harness/style-syntax": "./tests/utils/fixtures/style-syntax.js"
70
72
  },
71
73
  "dependencies": {
72
74
  "@jridgewell/sourcemap-codec": "^1.5.5",
@@ -76,10 +78,9 @@
76
78
  "@types/estree": "^1.0.8",
77
79
  "acorn": "^8.17.0",
78
80
  "esrap": "^2.3.2",
79
- "is-reference": "^3.0.3",
80
81
  "magic-string": "^0.30.18",
81
82
  "zimmerframe": "^1.1.2",
82
- "@tsrx/runtime": "0.1.3"
83
+ "@tsrx/runtime": "0.1.5"
83
84
  },
84
85
  "devDependencies": {
85
86
  "@types/node": "^24.3.0",
@@ -92,6 +93,9 @@
92
93
  },
93
94
  "files": [
94
95
  "src",
95
- "types"
96
+ "types",
97
+ "tests/fixtures/scoped-styles",
98
+ "tests/shared/scoped-styles-fixtures.js",
99
+ "tests/utils/fixtures/style-syntax.js"
96
100
  ]
97
101
  }
@@ -1,6 +1,14 @@
1
1
  /** @import * as AST from 'estree' */
2
+ /** @import { CompileError } from '../../types/index' */
2
3
 
3
4
  import { walk } from 'zimmerframe';
5
+ import { error } from '../errors.js';
6
+ import { DIAGNOSTIC_CODES } from '../diagnostics.js';
7
+ import { css_node_source_position } from '../parse/style.js';
8
+ import {
9
+ TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR,
10
+ TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR,
11
+ } from './validation.js';
4
12
 
5
13
  /**
6
14
  * True if is `:global` without arguments
@@ -35,10 +43,41 @@ function is_global(relative_selector) {
35
43
  }
36
44
 
37
45
  /**
38
- * Analyze CSS and set metadata for global selectors
46
+ * Analyze CSS and set metadata for global selectors.
47
+ *
48
+ * `:global` placement problems are reported through `error()` with the
49
+ * `CSS_GLOBAL_PLACEMENT` code: pushed onto `errors` when given (and skipped
50
+ * over a suppressing comment when `comments` is given too), thrown otherwise.
51
+ * When `css` is a sheet parsed with a `body` origin (every sheet `parseModule`
52
+ * produces), the diagnostic carries file-relative `pos` / `end` / `loc`; the
53
+ * `fileName` defaults to the sheet's.
54
+ *
39
55
  * @param {AST.CSS.Node} css - The CSS AST
56
+ * @param {{
57
+ * filename?: string | null,
58
+ * errors?: CompileError[],
59
+ * comments?: AST.CommentWithLocation[],
60
+ * }} [options]
40
61
  */
41
- export function analyze_css(css) {
62
+ export function analyze_css(css, options = {}) {
63
+ const sheet = css.type === 'StyleSheet' ? css : null;
64
+ const filename = options.filename ?? sheet?.filename ?? null;
65
+
66
+ /**
67
+ * @param {string} message
68
+ * @param {AST.CSS.Node} node
69
+ */
70
+ function report(message, node) {
71
+ error(
72
+ message,
73
+ filename,
74
+ css_node_source_position(sheet, node),
75
+ options.errors,
76
+ options.comments,
77
+ DIAGNOSTIC_CODES.CSS_GLOBAL_PLACEMENT,
78
+ );
79
+ }
80
+
42
81
  walk(css, /** @type {{ rule: AST.CSS.Rule | null }} */ ({ rule: null }), {
43
82
  Rule(node, context) {
44
83
  node.metadata.parent_rule = context.state.rule;
@@ -98,7 +137,7 @@ export function analyze_css(css) {
98
137
  is_nested &&
99
138
  !(/** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]).args)
100
139
  ) {
101
- throw new Error(`A :global selector cannot be inside a pseudoclass.`);
140
+ report(TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR, global.selectors[0]);
102
141
  }
103
142
 
104
143
  const idx = node.children.indexOf(global);
@@ -107,9 +146,8 @@ export function analyze_css(css) {
107
146
  // ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
108
147
  for (let i = idx + 1; i < node.children.length; i++) {
109
148
  if (!is_global(node.children[i])) {
110
- throw new Error(
111
- `:global(...) can be at the start or end of a selector sequence, but not in the middle.`,
112
- );
149
+ report(TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR, first);
150
+ break;
113
151
  }
114
152
  }
115
153
  }
@@ -15,6 +15,8 @@ import {
15
15
  validate_forgotten_statement_container,
16
16
  validate_unsupported_lazy_assignment_position,
17
17
  } from './validation.js';
18
+ import { create_scopes, ScopeRoot } from '../scope.js';
19
+ import { analyze_styles } from './style-analyze.js';
18
20
 
19
21
  /**
20
22
  * Find the first authored lazy pattern along an assignment target's binding
@@ -220,5 +222,16 @@ export function analyze_tsrx(ast, filename, options = {}) {
220
222
 
221
223
  walk(ast, state, visitors);
222
224
 
223
- return { ast, errors, comments };
225
+ // Style `apply` targets resolve through real bindings. Scope diagnostics
226
+ // (duplicate declarations, reserved names) stay with the compilers that
227
+ // already report them, so this run collects into a private list.
228
+ const { scope, scopes } = create_scopes(ast, new ScopeRoot(), null, {
229
+ filename: /** @type {string} */ (filename ?? null),
230
+ collect: true,
231
+ errors: [],
232
+ comments,
233
+ });
234
+ const styles = analyze_styles(ast, scopes, state);
235
+
236
+ return { ast, errors, comments, scope, scopes, styles };
224
237
  }
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Module-level analysis of `<style>` blocks: which blocks are standalone
3
+ * (scoped to the template scope they sit in) and which are assigned
4
+ * (`const theme = <style>…</style>`), what every `apply` attribute resolves to,
5
+ * and whether an assigned block is a theme (exported or applied, D5) or a
6
+ * class map. Results are stamped on the style nodes' `metadata` so the target
7
+ * transforms — which clone nodes but share metadata — read one shape, and are
8
+ * summarized on `program.metadata.styles` for consumer compilers.
9
+ *
10
+ * Declared-before-use (D13 layer 1) is enforced here by source position and
11
+ * lexical visibility: same-module CSS is emitted in lexical order, so a theme
12
+ * declared after its applier would win the cascade instead of losing it.
13
+ *
14
+ * @import * as AST from 'estree'
15
+ * @import * as ESTreeJSX from 'estree-jsx'
16
+ * @import { Binding, ScopeInterface, StyleApplyResolution, StyleAnalysis, TSRXAnalysisState, Visitors } from '../../types/index'
17
+ */
18
+
19
+ import { walk } from 'zimmerframe';
20
+ import { DIAGNOSTIC_CODES } from '../diagnostics.js';
21
+ import { get_style_class_map_names, get_style_element_stylesheet } from '../transform/style-ref.js';
22
+ import { is_function_node, is_template_directive } from '../utils/ast.js';
23
+ import {
24
+ TSRX_STYLE_APPLY_DUPLICATE_ERROR,
25
+ TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
26
+ TSRX_STYLE_APPLY_VALUE_ERROR,
27
+ TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
28
+ TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
29
+ TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
30
+ TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
31
+ tsrx_style_apply_before_declaration_error,
32
+ tsrx_style_apply_target_error,
33
+ tsrx_style_unknown_attribute_error,
34
+ validate_style,
35
+ } from './validation.js';
36
+
37
+ /**
38
+ * `container_depth` counts the enclosing TSRX containers — `@{ … }` bodies
39
+ * and control-flow directives — where raw CSS in a standalone block is
40
+ * template syntax; native elements only bump `template_depth`.
41
+ *
42
+ * @typedef {{ function_depth: number, template_depth: number, container_depth: number }} StyleWalkState
43
+ */
44
+
45
+ /**
46
+ * `apply` sites resolve against ordinary JavaScript scoping, so the analyzer
47
+ * looks the target up from the nearest enclosing scope of the style block.
48
+ *
49
+ * @param {AST.Node[]} path
50
+ * @param {Map<AST.Node, ScopeInterface>} scopes
51
+ * @returns {ScopeInterface | null}
52
+ */
53
+ function nearest_scope(path, scopes) {
54
+ for (let i = path.length - 1; i >= 0; i -= 1) {
55
+ const scope = scopes.get(path[i]);
56
+ if (scope) return scope;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Names exported through `export { a, b as c }` and `export default a`.
63
+ *
64
+ * @param {AST.Program} ast
65
+ * @returns {Set<string>}
66
+ */
67
+ function collect_exported_names(ast) {
68
+ /** @type {Set<string>} */
69
+ const names = new Set();
70
+ for (const statement of ast.body) {
71
+ if (statement.type === 'ExportNamedDeclaration' && !statement.declaration) {
72
+ for (const specifier of statement.specifiers) {
73
+ if (specifier.local.type === 'Identifier') names.add(specifier.local.name);
74
+ }
75
+ } else if (
76
+ statement.type === 'ExportDefaultDeclaration' &&
77
+ statement.declaration.type === 'Identifier'
78
+ ) {
79
+ names.add(statement.declaration.name);
80
+ }
81
+ }
82
+ return names;
83
+ }
84
+
85
+ /**
86
+ * A style block is standalone when it is template content rather than a value:
87
+ * a child of a native element/fragment, the render output of a `@{ … }` body
88
+ * or a control-flow body, or a bare statement. Only the first placement is
89
+ * valid; the others are reported (5.1).
90
+ *
91
+ * @param {AST.Node[]} path
92
+ * @returns {boolean}
93
+ */
94
+ export function is_standalone_style_position(path) {
95
+ const parent = path.at(-1);
96
+ if (!parent) return true;
97
+ switch (parent.type) {
98
+ case 'JSXElement':
99
+ case 'JSXFragment':
100
+ return !!parent.metadata?.native_tsrx;
101
+ case 'JSXCodeBlock':
102
+ case 'BlockStatement':
103
+ case 'SwitchCase':
104
+ case 'Program':
105
+ case 'ExpressionStatement':
106
+ return true;
107
+ default:
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * @param {ESTreeJSX.JSXAttributeNode} attr
114
+ * @param {string} name
115
+ * @returns {boolean}
116
+ */
117
+ function is_named_attribute(attr, name) {
118
+ return (
119
+ attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier' && attr.name.name === name
120
+ );
121
+ }
122
+
123
+ /**
124
+ * @param {ESTreeJSX.JSXAttribute} attr
125
+ * @returns {AST.Expression | null}
126
+ */
127
+ function attribute_expression(attr) {
128
+ const value = attr.value;
129
+ if (!value || value.type !== 'JSXExpressionContainer') return null;
130
+ return value.expression.type === 'JSXEmptyExpression' ? null : value.expression;
131
+ }
132
+
133
+ /**
134
+ * @param {AST.Expression} expression
135
+ * @returns {string}
136
+ */
137
+ function describe_target(expression) {
138
+ if (expression.type === 'Identifier') return expression.name;
139
+ if (
140
+ expression.type === 'MemberExpression' &&
141
+ !expression.computed &&
142
+ expression.property.type === 'Identifier'
143
+ ) {
144
+ return `${describe_target(/** @type {AST.Expression} */ (expression.object))}.${expression.property.name}`;
145
+ }
146
+ return 'apply target';
147
+ }
148
+
149
+ /**
150
+ * @param {AST.Expression} expression
151
+ * @returns {AST.Identifier | null}
152
+ */
153
+ function member_root(expression) {
154
+ let current = expression;
155
+ while (current.type === 'MemberExpression' && !current.computed) {
156
+ current = /** @type {AST.Expression} */ (current.object);
157
+ }
158
+ return current.type === 'Identifier' ? current : null;
159
+ }
160
+
161
+ /**
162
+ * @param {Binding} binding
163
+ * @param {AST.Expression} expression the `apply` entry for the member chain
164
+ * @returns {AST.JSXStyleElement | null | undefined} `undefined` when the
165
+ * member does not name a style block of a module-local object
166
+ */
167
+ function resolve_local_member(binding, expression) {
168
+ if (expression.type !== 'MemberExpression' || expression.computed) return undefined;
169
+ if (expression.object.type !== 'Identifier' || expression.property.type !== 'Identifier') {
170
+ return undefined;
171
+ }
172
+ const object = binding.initial;
173
+ if (object?.type !== 'ObjectExpression') return undefined;
174
+ const name = expression.property.name;
175
+ for (const property of object.properties) {
176
+ if (
177
+ property.type === 'Property' &&
178
+ !property.computed &&
179
+ ((property.key.type === 'Identifier' && property.key.name === name) ||
180
+ (property.key.type === 'Literal' && property.key.value === name)) &&
181
+ property.value.type === 'JSXStyleElement'
182
+ ) {
183
+ return property.value;
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+
189
+ /**
190
+ * Whether any reference to the binding reads its `$class` property.
191
+ *
192
+ * @param {Binding | null} binding
193
+ * @returns {boolean}
194
+ */
195
+ function is_class_read(binding) {
196
+ if (!binding) return false;
197
+ return binding.references.some(({ node, path }) => {
198
+ const parent = path.at(-1);
199
+ if (parent?.type !== 'MemberExpression' || parent.object !== node) return false;
200
+ if (parent.computed) {
201
+ return parent.property.type === 'Literal' && parent.property.value === '$class';
202
+ }
203
+ return parent.property.type === 'Identifier' && parent.property.name === '$class';
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Run the module-level style analysis. `scopes` comes from `create_scopes`
209
+ * over the same program so target resolution uses real bindings.
210
+ *
211
+ * @param {AST.Program} ast
212
+ * @param {Map<AST.Node, ScopeInterface>} scopes
213
+ * @param {TSRXAnalysisState} state
214
+ * @returns {StyleAnalysis}
215
+ */
216
+ export function analyze_styles(ast, scopes, state) {
217
+ const exported_names = collect_exported_names(ast);
218
+ const errors = state.collect ? state.errors : undefined;
219
+ /** @type {AST.JSXStyleElement[]} */
220
+ const assigned = [];
221
+ /** @type {AST.JSXStyleElement[]} */
222
+ const standalone = [];
223
+
224
+ /**
225
+ * @param {string} message
226
+ * @param {string} code
227
+ * @param {AST.Node} node
228
+ */
229
+ const report = (message, code, node) => {
230
+ validate_style(message, code, node, state.filename, errors, state.comments);
231
+ };
232
+
233
+ /**
234
+ * @param {AST.Expression} expression
235
+ * @param {ScopeInterface | null} scope
236
+ * @param {StyleApplyResolution[]} resolutions
237
+ */
238
+ const resolve_apply_entry = (expression, scope, resolutions) => {
239
+ if (expression.type === 'ArrayExpression') {
240
+ for (const element of expression.elements) {
241
+ if (!element || element.type === 'SpreadElement') {
242
+ report(
243
+ tsrx_style_apply_target_error('apply entry'),
244
+ DIAGNOSTIC_CODES.STYLE_APPLY_TARGET,
245
+ element ?? expression,
246
+ );
247
+ continue;
248
+ }
249
+ resolve_apply_entry(element, scope, resolutions);
250
+ }
251
+ return;
252
+ }
253
+
254
+ const root = member_root(expression);
255
+ const name = describe_target(expression);
256
+ const binding = root && scope ? scope.get(root.name) : null;
257
+ if (!root || !binding) {
258
+ report(tsrx_style_apply_target_error(name), DIAGNOSTIC_CODES.STYLE_APPLY_TARGET, expression);
259
+ return;
260
+ }
261
+
262
+ if (binding.declaration_kind === 'import') {
263
+ resolutions.push({ expression, target: null, kind: 'import' });
264
+ return;
265
+ }
266
+
267
+ /** @type {AST.JSXStyleElement | null | undefined} */
268
+ let target;
269
+ if (expression.type === 'Identifier') {
270
+ target = binding.initial?.type === 'JSXStyleElement' ? binding.initial : undefined;
271
+ } else {
272
+ target = resolve_local_member(binding, expression);
273
+ }
274
+
275
+ if (!target) {
276
+ report(tsrx_style_apply_target_error(name), DIAGNOSTIC_CODES.STYLE_APPLY_TARGET, expression);
277
+ return;
278
+ }
279
+
280
+ if (/** @type {number} */ (binding.node.start) > /** @type {number} */ (root.start)) {
281
+ report(
282
+ tsrx_style_apply_before_declaration_error(name),
283
+ DIAGNOSTIC_CODES.STYLE_APPLY_BEFORE_DECLARATION,
284
+ root,
285
+ );
286
+ return;
287
+ }
288
+
289
+ target.metadata.styleApplied = true;
290
+ resolutions.push({ expression, target, kind: 'local' });
291
+ };
292
+
293
+ walk(
294
+ /** @type {AST.Node} */ (ast),
295
+ /** @type {StyleWalkState} */ ({ function_depth: 0, template_depth: 0, container_depth: 0 }),
296
+ /** @type {Visitors<AST.Node, StyleWalkState>} */ ({
297
+ _(node, { state: walk_state, next }) {
298
+ if (is_function_node(node)) {
299
+ next({ ...walk_state, function_depth: walk_state.function_depth + 1 });
300
+ return;
301
+ }
302
+ if (node.type === 'JSXCodeBlock' || is_template_directive(node)) {
303
+ // `@else if` chains and `@catch` clauses are children of the
304
+ // directive node, so one bump covers every branch body.
305
+ next({
306
+ ...walk_state,
307
+ template_depth: walk_state.template_depth + 1,
308
+ container_depth: walk_state.container_depth + 1,
309
+ });
310
+ return;
311
+ }
312
+ if (
313
+ (node.type === 'JSXElement' || node.type === 'JSXFragment') &&
314
+ node.metadata?.native_tsrx
315
+ ) {
316
+ next({ ...walk_state, template_depth: walk_state.template_depth + 1 });
317
+ return;
318
+ }
319
+ next();
320
+ },
321
+
322
+ JSXStyleElement(node, { path, state: walk_state, next }) {
323
+ const is_standalone = is_standalone_style_position(path);
324
+ const inside_head = path.some(
325
+ (ancestor) =>
326
+ ancestor.type === 'JSXElement' &&
327
+ ancestor.openingElement.name.type === 'JSXIdentifier' &&
328
+ ancestor.openingElement.name.name === 'head',
329
+ );
330
+ const attributes = node.openingElement.attributes;
331
+ const is_resource = attributes.some((attr) => is_named_attribute(attr, 'href'));
332
+
333
+ /** @type {ESTreeJSX.JSXAttribute | null} */
334
+ let apply_attr = null;
335
+ for (const attr of attributes) {
336
+ if (attr.type !== 'JSXAttribute') continue;
337
+ if (is_named_attribute(attr, 'apply')) {
338
+ if (apply_attr) {
339
+ report(
340
+ TSRX_STYLE_APPLY_DUPLICATE_ERROR,
341
+ DIAGNOSTIC_CODES.STYLE_APPLY_DUPLICATE,
342
+ attr,
343
+ );
344
+ continue;
345
+ }
346
+ apply_attr = attr;
347
+ continue;
348
+ }
349
+ if (is_named_attribute(attr, 'ref') || inside_head || is_resource) continue;
350
+ const attr_name =
351
+ attr.name.type === 'JSXIdentifier'
352
+ ? attr.name.name
353
+ : `${attr.name.namespace.name}:${attr.name.name.name}`;
354
+ report(
355
+ tsrx_style_unknown_attribute_error(attr_name),
356
+ DIAGNOSTIC_CODES.STYLE_UNKNOWN_ATTRIBUTE,
357
+ attr,
358
+ );
359
+ }
360
+
361
+ /** @type {StyleApplyResolution[]} */
362
+ const resolutions = [];
363
+ if (apply_attr) {
364
+ const expression = attribute_expression(apply_attr);
365
+ if (!expression) {
366
+ report(TSRX_STYLE_APPLY_VALUE_ERROR, DIAGNOSTIC_CODES.STYLE_APPLY_VALUE, apply_attr);
367
+ } else if (inside_head || is_resource) {
368
+ report(
369
+ TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
370
+ DIAGNOSTIC_CODES.STYLE_APPLY_UNSUPPORTED_HOST,
371
+ apply_attr,
372
+ );
373
+ } else {
374
+ resolve_apply_entry(expression, nearest_scope(path, scopes), resolutions);
375
+ }
376
+ }
377
+ node.metadata.styleApplies = resolutions;
378
+
379
+ if (is_standalone) {
380
+ if (!inside_head && !is_resource) {
381
+ const parent = path.at(-1);
382
+ const in_children_list =
383
+ parent?.type === 'JSXElement' || parent?.type === 'JSXFragment';
384
+ if (walk_state.function_depth === 0 && walk_state.template_depth === 0) {
385
+ report(
386
+ TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
387
+ DIAGNOSTIC_CODES.STYLE_STANDALONE_AT_MODULE_SCOPE,
388
+ node,
389
+ );
390
+ } else if (!in_children_list) {
391
+ // A block is an output node: as the lone output of a `@{ … }` or
392
+ // control-flow body, or as a statement, it styles nothing. Beside
393
+ // another output it is already the parser's multiple-outputs error.
394
+ report(
395
+ TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
396
+ DIAGNOSTIC_CODES.STYLE_STANDALONE_NEEDS_FRAGMENT,
397
+ node,
398
+ );
399
+ } else if (walk_state.container_depth === 0 && !node.openingElement.selfClosing) {
400
+ // Raw CSS in `<style>` is TSRX template syntax: outside every
401
+ // `@{ … }` and control-flow body the file is plain TSX, where a
402
+ // `<style>` takes an expression child instead. A self-closing
403
+ // `<style apply={…} />` holds no CSS text and is ordinary JSX.
404
+ report(
405
+ TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
406
+ DIAGNOSTIC_CODES.STYLE_STANDALONE_OUTSIDE_TEMPLATE,
407
+ node,
408
+ );
409
+ }
410
+ standalone.push(node);
411
+ }
412
+ } else {
413
+ assigned.push(node);
414
+ const parent = path.at(-1);
415
+ const grandparent = path.at(-2);
416
+ /** @type {AST.VariableDeclarator | null} */
417
+ let declarator = null;
418
+ if (parent?.type === 'VariableDeclarator') {
419
+ declarator = parent;
420
+ } else if (parent?.type === 'Property' && grandparent?.type === 'ObjectExpression') {
421
+ const holder = path.at(-3);
422
+ if (holder?.type === 'VariableDeclarator') declarator = holder;
423
+ }
424
+ const declared_name = declarator?.id.type === 'Identifier' ? declarator.id.name : null;
425
+ const declaration_index = declarator ? path.indexOf(declarator) : -1;
426
+ const export_parent = declaration_index > 0 ? path[declaration_index - 2] : null;
427
+ node.metadata.styleExported =
428
+ parent?.type === 'ExportDefaultDeclaration' ||
429
+ export_parent?.type === 'ExportNamedDeclaration' ||
430
+ (declared_name !== null && exported_names.has(declared_name));
431
+ // Reading `theme.$class` opts an element (or a child component's
432
+ // element, through a prop) into the block's whole stylesheet, so
433
+ // such a block is a theme: every selector stays, like `apply`.
434
+ node.metadata.styleClassRead =
435
+ declared_name !== null &&
436
+ is_class_read(nearest_scope(path, scopes)?.get(declared_name) ?? null);
437
+
438
+ const stylesheet = get_style_element_stylesheet(node);
439
+ if (stylesheet && get_style_class_map_names(stylesheet).includes('$class')) {
440
+ report(
441
+ TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
442
+ DIAGNOSTIC_CODES.STYLE_RESERVED_CLASS_KEY,
443
+ node,
444
+ );
445
+ }
446
+ }
447
+
448
+ next();
449
+ },
450
+ }),
451
+ );
452
+
453
+ for (const node of assigned) {
454
+ node.metadata.styleKind =
455
+ node.metadata.styleExported || node.metadata.styleApplied || node.metadata.styleClassRead
456
+ ? 'theme'
457
+ : 'class-map';
458
+ }
459
+
460
+ /** @type {StyleAnalysis} */
461
+ const styles = { assigned, standalone };
462
+ /** @type {{ metadata?: { styles?: StyleAnalysis } }} */ (ast).metadata = {
463
+ .../** @type {{ metadata?: object }} */ (ast).metadata,
464
+ styles,
465
+ };
466
+ return styles;
467
+ }