@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
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.65",
6
+ "version": "0.1.67",
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.4"
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
  }
@@ -422,18 +422,14 @@ function can_render_dynamic_content(element, check_classes = false) {
422
422
  function get_possible_element_siblings(node, direction, adjacent_only) {
423
423
  /** @type {Map<AST.TSRXJSXElement, boolean>} */
424
424
  const siblings = new Map();
425
- const parent = get_element_parent(node);
425
+ const container = get_sibling_container(node);
426
426
 
427
- if (!parent) {
427
+ if (container === null) {
428
428
  return siblings;
429
429
  }
430
430
 
431
- // Get the container that holds the siblings
432
- const container = node_children(parent);
433
431
  const node_index = container.indexOf(node);
434
432
 
435
- if (node_index === -1) return siblings;
436
-
437
433
  // Determine which siblings to check based on direction
438
434
  let start, end, step;
439
435
  if (direction === FORWARD) {
@@ -532,9 +528,8 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
532
528
  sibling_matched = true;
533
529
  } else {
534
530
  // Check if there are any elements after this component that could match the remaining selectors
535
- const parent = get_element_parent(node);
536
- if (parent) {
537
- const container = node_children(parent);
531
+ const container = get_sibling_container(node);
532
+ if (container !== null) {
538
533
  const component_index = container.indexOf(possible_sibling);
539
534
 
540
535
  // For adjacent combinator, only check immediate next element
@@ -578,6 +573,38 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi
578
573
  return true;
579
574
  }
580
575
  }
576
+ /**
577
+ * The children list an element sits in — that of the nearest ancestor on its
578
+ * path whose children include it, which is its parent element, a fragment
579
+ * (an authored `<>…</>`, a control-flow branch's output, or the synthetic
580
+ * root of a style scope), or `null` when the element has no container.
581
+ *
582
+ * Sibling combinators (`+`, `~`) read this list. Ancestor combinators keep
583
+ * using {@link get_element_parent}: a fragment is not an element, and a
584
+ * scope's root fragment stands for the container that a scoped block never
585
+ * styles.
586
+ *
587
+ * @param {AST.TSRXElementNode} node
588
+ * @returns {AST.Node[] | null}
589
+ */
590
+ function get_sibling_container(node) {
591
+ const path = node.metadata?.path;
592
+ if (!path || !path.length) {
593
+ return null;
594
+ }
595
+
596
+ let i = path.length;
597
+
598
+ while (i--) {
599
+ const children = node_children(path[i]);
600
+ if (children.includes(node)) {
601
+ return children;
602
+ }
603
+ }
604
+
605
+ return null;
606
+ }
607
+
581
608
  /**
582
609
  * @param {AST.TSRXElementNode} node
583
610
  * @returns {AST.TSRXJSXElement | null}