@tsrx/core 0.1.65 → 0.1.67

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 (55) 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/prune.js +36 -9
  6. package/src/analyze/style-analyze.js +467 -0
  7. package/src/analyze/validation.js +57 -0
  8. package/src/diagnostics.js +22 -0
  9. package/src/index.js +18 -0
  10. package/src/parse/style.js +97 -7
  11. package/src/plugin.js +145 -47
  12. package/src/scope.js +1 -1
  13. package/src/transform/jsx/index.js +94 -417
  14. package/src/transform/jsx/style-scopes.js +866 -0
  15. package/src/transform/scoping.js +129 -79
  16. package/src/transform/segments.js +16 -4
  17. package/src/transform/style-ref.js +74 -13
  18. package/src/transform/stylesheet.js +2 -1
  19. package/src/utils/is-reference.js +59 -0
  20. package/tests/fixtures/scoped-styles/README.md +71 -0
  21. package/tests/fixtures/scoped-styles/apply-forms.expected.json +18 -0
  22. package/tests/fixtures/scoped-styles/apply-forms.tsrx +69 -0
  23. package/tests/fixtures/scoped-styles/assigned-positions.expected.json +29 -0
  24. package/tests/fixtures/scoped-styles/assigned-positions.tsrx +90 -0
  25. package/tests/fixtures/scoped-styles/class-opt-in.expected.json +13 -0
  26. package/tests/fixtures/scoped-styles/class-opt-in.tsrx +39 -0
  27. package/tests/fixtures/scoped-styles/control-flow-else-if.expected.json +11 -0
  28. package/tests/fixtures/scoped-styles/control-flow-else-if.tsrx +26 -0
  29. package/tests/fixtures/scoped-styles/control-flow.expected.json +17 -0
  30. package/tests/fixtures/scoped-styles/control-flow.tsrx +102 -0
  31. package/tests/fixtures/scoped-styles/cross-module-apply.expected.json +14 -0
  32. package/tests/fixtures/scoped-styles/cross-module-apply.tsrx +48 -0
  33. package/tests/fixtures/scoped-styles/element-rooted-templates.expected.json +12 -0
  34. package/tests/fixtures/scoped-styles/element-rooted-templates.tsrx +33 -0
  35. package/tests/fixtures/scoped-styles/precedence.expected.json +11 -0
  36. package/tests/fixtures/scoped-styles/precedence.tsrx +45 -0
  37. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.expected.json +12 -0
  38. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.tsrx +46 -0
  39. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.expected.json +9 -0
  40. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.tsrx +24 -0
  41. package/tests/fixtures/scoped-styles/search-panel.expected.json +11 -0
  42. package/tests/fixtures/scoped-styles/search-panel.tsrx +48 -0
  43. package/tests/fixtures/scoped-styles/sibling-combinators.expected.json +16 -0
  44. package/tests/fixtures/scoped-styles/sibling-combinators.tsrx +42 -0
  45. package/tests/fixtures/scoped-styles/sibling-scope.expected.json +11 -0
  46. package/tests/fixtures/scoped-styles/sibling-scope.tsrx +44 -0
  47. package/tests/fixtures/scoped-styles/sibling-scopes.expected.json +12 -0
  48. package/tests/fixtures/scoped-styles/sibling-scopes.tsrx +51 -0
  49. package/tests/fixtures/scoped-styles/theme-composition.expected.json +14 -0
  50. package/tests/fixtures/scoped-styles/theme-composition.tsrx +45 -0
  51. package/tests/fixtures/scoped-styles/theme-diamond.expected.json +10 -0
  52. package/tests/fixtures/scoped-styles/theme-diamond.tsrx +18 -0
  53. package/tests/shared/scoped-styles-fixtures.js +67 -0
  54. package/tests/utils/fixtures/style-syntax.js +519 -0
  55. package/types/index.d.ts +85 -0
@@ -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
+ }
@@ -32,6 +32,48 @@ export const TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR =
32
32
  "This TSRX template output is unused. Return it, assign it to a value that is rendered, or make it part of the rendered output of a function '@{...}' body.";
33
33
  export const TSRX_UNSUPPORTED_LAZY_ASSIGNMENT_POSITION_ERROR =
34
34
  'Lazy destructuring assignments require a directly lazy target as a standalone statement inside a program, block, TSRX code block, or switch case.';
35
+ export const TSRX_STYLE_APPLY_VALUE_ERROR =
36
+ "The 'apply' attribute of a <style> block requires an expression value: apply={theme} or apply={[a, b]}.";
37
+ export const TSRX_STYLE_APPLY_DUPLICATE_ERROR =
38
+ "A <style> block accepts a single 'apply' attribute; pass several themes as an array: apply={[a, b]}.";
39
+ export const TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR =
40
+ "The 'apply' attribute is only supported on scoped <style> blocks, not on <head> styles or resource styles.";
41
+ export const TSRX_STYLE_RESERVED_CLASS_KEY_ERROR =
42
+ "'$class' is reserved on assigned <style> blocks for the block's scope hash; rename the '.$class' selector.";
43
+ export const TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR =
44
+ 'A standalone <style> block is only allowed inside a template scope. At module scope assign it: const theme = <style>…</style>.';
45
+ export const TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR =
46
+ 'A standalone <style> block must be a child of an element or a fragment. Wrap it with the output it styles in a fragment: <><style>…</style><div>…</div></>.';
47
+ export const TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR =
48
+ 'A standalone <style> block with CSS text is TSRX template syntax and needs an enclosing @{ … } body or an @if/@for/@switch/@try body. In plain TSX give <style> an expression child instead: <style>{css}</style>. To declare a reusable block here, assign it: const theme = <style>…</style>.';
49
+ export const TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR =
50
+ 'A :global selector cannot be inside a pseudoclass.';
51
+ export const TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR =
52
+ ':global(...) can be at the start or end of a selector sequence, but not in the middle.';
53
+
54
+ /**
55
+ * @param {string} name
56
+ * @returns {string}
57
+ */
58
+ export function tsrx_style_apply_target_error(name) {
59
+ return `'${name}' is not a style block. An 'apply' target must be a variable, import, or member holding an assigned <style> block.`;
60
+ }
61
+
62
+ /**
63
+ * @param {string} name
64
+ * @returns {string}
65
+ */
66
+ export function tsrx_style_apply_before_declaration_error(name) {
67
+ return `'${name}' is applied before its declaration. Declare the style block before the block that applies it.`;
68
+ }
69
+
70
+ /**
71
+ * @param {string} name
72
+ * @returns {string}
73
+ */
74
+ export function tsrx_style_unknown_attribute_error(name) {
75
+ return `Unknown <style> attribute '${name}'. Scoped style blocks accept 'ref' and 'apply'.`;
76
+ }
35
77
 
36
78
  const invalid_nestings = {
37
79
  // <p> cannot contain block-level elements
@@ -434,3 +476,18 @@ export function validate_nesting(element, context, errors) {
434
476
  }
435
477
  }
436
478
  }
479
+
480
+ /**
481
+ * Report a style diagnostic through the shared error channel so editors get
482
+ * positions and `@tsrx-ignore` applies.
483
+ *
484
+ * @param {string} message
485
+ * @param {string} code
486
+ * @param {AST.Node} node
487
+ * @param {string | null} filename
488
+ * @param {CompileError[] | undefined} errors
489
+ * @param {AST.CommentWithLocation[] | undefined} comments
490
+ */
491
+ export function validate_style(message, code, node, filename, errors, comments) {
492
+ error(message, filename, node, errors, comments, code);
493
+ }
@@ -6,4 +6,26 @@ export const DIAGNOSTIC_CODES = {
6
6
  TEMPLATE_RETURN_STATEMENT: 'tsrx-template-return-statement',
7
7
  FORGOTTEN_STATEMENT_CONTAINER: 'tsrx-forgotten-statement-container',
8
8
  UNSUPPORTED_LAZY_ASSIGNMENT_POSITION: 'tsrx-unsupported-lazy-assignment-position',
9
+ /** `<style apply>` carries no expression value. */
10
+ STYLE_APPLY_VALUE: 'tsrx-style-apply-value',
11
+ /** An `apply` entry is not an identifier, member, or array of those, or does not resolve to a style block. */
12
+ STYLE_APPLY_TARGET: 'tsrx-style-apply-target',
13
+ /** An `apply` target is declared after the applying block in source order. */
14
+ STYLE_APPLY_BEFORE_DECLARATION: 'tsrx-style-apply-before-declaration',
15
+ /** Two `apply` attributes on one `<style>` block. */
16
+ STYLE_APPLY_DUPLICATE: 'tsrx-style-apply-duplicate',
17
+ /** `apply` on a `<head>` style or a resource (`href`) style. */
18
+ STYLE_APPLY_UNSUPPORTED_HOST: 'tsrx-style-apply-unsupported-host',
19
+ /** An assigned style block authors a `.$class` class selector. */
20
+ STYLE_RESERVED_CLASS_KEY: 'tsrx-style-reserved-class-key',
21
+ /** A standalone `<style>` block at module scope. */
22
+ STYLE_STANDALONE_AT_MODULE_SCOPE: 'tsrx-style-standalone-at-module-scope',
23
+ /** A standalone `<style>` block with CSS text outside any `@{ … }` or control-flow body. */
24
+ STYLE_STANDALONE_OUTSIDE_TEMPLATE: 'tsrx-style-standalone-outside-template',
25
+ /** A standalone `<style>` block in a statement slot: the lone output of a `@{ … }` or control-flow body, or a statement. */
26
+ STYLE_STANDALONE_NEEDS_FRAGMENT: 'tsrx-style-standalone-needs-fragment',
27
+ /** A `<style>` attribute other than `ref` and `apply`. */
28
+ STYLE_UNKNOWN_ATTRIBUTE: 'tsrx-style-unknown-attribute',
29
+ /** `:global` used where the scoping rules do not allow it. */
30
+ CSS_GLOBAL_PLACEMENT: 'tsrx-css-global-placement',
9
31
  };
package/src/index.js CHANGED
@@ -186,6 +186,7 @@ export {
186
186
  collect_style_ref_attributes as collectStyleRefAttributes,
187
187
  create_style_class_map as createStyleClassMap,
188
188
  create_style_class_map_from_stylesheet as createStyleClassMapFromStylesheet,
189
+ build_style_class_map as buildStyleClassMap,
189
190
  create_style_ref_setup_statements as createStyleRefSetupStatements,
190
191
  get_style_element_stylesheet as getStyleElementStylesheet,
191
192
  } from './transform/style-ref.js';
@@ -251,6 +252,7 @@ export {
251
252
  // Analyze
252
253
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
253
254
  export { prune_css as pruneCss } from './analyze/prune.js';
255
+ export { create_scope_root as createScopeRoot } from './transform/jsx/style-scopes.js';
254
256
  export {
255
257
  TSRX_DO_WHILE_STATEMENT_ERROR,
256
258
  TSRX_FORGOTTEN_STATEMENT_CONTAINER_ERROR,
@@ -279,4 +281,20 @@ export {
279
281
  validate_forgotten_statement_container as validateForgottenStatementContainer,
280
282
  validate_nesting as validateNesting,
281
283
  is_template_value_position as isTemplateValuePosition,
284
+ TSRX_STYLE_APPLY_VALUE_ERROR,
285
+ TSRX_STYLE_APPLY_DUPLICATE_ERROR,
286
+ TSRX_STYLE_APPLY_UNSUPPORTED_HOST_ERROR,
287
+ TSRX_STYLE_RESERVED_CLASS_KEY_ERROR,
288
+ TSRX_STYLE_STANDALONE_AT_MODULE_SCOPE_ERROR,
289
+ TSRX_STYLE_STANDALONE_OUTSIDE_TEMPLATE_ERROR,
290
+ TSRX_STYLE_STANDALONE_NEEDS_FRAGMENT_ERROR,
291
+ TSRX_CSS_GLOBAL_NESTED_IN_PSEUDOCLASS_ERROR,
292
+ TSRX_CSS_GLOBAL_MIDDLE_PLACEMENT_ERROR,
293
+ tsrx_style_apply_target_error as tsrxStyleApplyTargetError,
294
+ tsrx_style_apply_before_declaration_error as tsrxStyleApplyBeforeDeclarationError,
295
+ tsrx_style_unknown_attribute_error as tsrxStyleUnknownAttributeError,
282
296
  } from './analyze/validation.js';
297
+ export {
298
+ analyze_styles as analyzeStyles,
299
+ is_standalone_style_position as isStandaloneStylePosition,
300
+ } from './analyze/style-analyze.js';