@tsrx/core 0.1.48 → 0.1.50

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.
@@ -1,3 +1,4 @@
1
+ /** @import * as AST from 'estree' */
1
2
  /** @import * as ESTreeJSX from 'estree-jsx' */
2
3
 
3
4
  /**
@@ -104,7 +105,7 @@ export function is_hoist_safe_jsx_attribute(attribute) {
104
105
  }
105
106
 
106
107
  /**
107
- * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
108
+ * @param {AST.TSRXJSXElement | AST.TSRXJSXFragment} node
108
109
  * @returns {boolean}
109
110
  */
110
111
  export function is_hoist_safe_jsx_node(node) {
@@ -8,6 +8,11 @@
8
8
  * textually after the mutation.
9
9
  */
10
10
 
11
+ /** @import * as AST from 'estree' */
12
+ /** @import * as ESTreeJSX from 'estree-jsx' */
13
+
14
+ import * as b from '../utils/builders.js';
15
+
11
16
  /**
12
17
  * Returns true when the body contains a non-JSX statement that appears
13
18
  * after a JSX child. In that case JSX children must be captured at their
@@ -17,8 +22,8 @@
17
22
  * The `is_jsx_child` predicate is target-specific — each target recognizes
18
23
  * its JSX-bearing child nodes and template-control expressions.
19
24
  *
20
- * @param {any[]} body_nodes
21
- * @param {(node: any) => boolean} is_jsx_child
25
+ * @param {AST.Node[]} body_nodes
26
+ * @param {(node: AST.Node) => boolean} is_jsx_child
22
27
  * @returns {boolean}
23
28
  */
24
29
  export function is_interleaved_body(body_nodes, is_jsx_child) {
@@ -35,11 +40,12 @@ export function is_interleaved_body(body_nodes, is_jsx_child) {
35
40
 
36
41
  /**
37
42
  * Only JSX nodes that evaluate to a single expression can be hoisted into a
38
- * `const`. Static text children (`JSXText`) are inert and don't need
39
- * capturing their position relative to mutations doesn't change output.
43
+ * `const`. Static text children (`JSXText`) and comment-only containers
44
+ * (`{/* *\/}`) are inert and don't need capturing — their position relative
45
+ * to mutations doesn't change output, and neither has an expression to bind.
40
46
  *
41
- * @param {any} jsx
42
- * @returns {boolean}
47
+ * @param {AST.Node | null | undefined} jsx
48
+ * @returns {jsx is ESTreeJSX.JSXCapturableChild}
43
49
  */
44
50
  export function is_capturable_jsx_child(jsx) {
45
51
  if (!jsx) return false;
@@ -48,7 +54,8 @@ export function is_capturable_jsx_child(jsx) {
48
54
  // into a const would evaluate them once.
49
55
  if (jsx.metadata?.tsrx_reactive_block === true) return false;
50
56
  const t = jsx.type;
51
- return t === 'JSXElement' || t === 'JSXFragment' || t === 'JSXExpressionContainer';
57
+ if (t === 'JSXExpressionContainer') return jsx.expression.type !== 'JSXEmptyExpression';
58
+ return t === 'JSXElement' || t === 'JSXFragment';
52
59
  }
53
60
 
54
61
  /**
@@ -58,45 +65,24 @@ export function is_capturable_jsx_child(jsx) {
58
65
  * statements in source order and uses the reference in place of the JSX
59
66
  * child inside the returned fragment.
60
67
  *
61
- * @param {any} jsx
68
+ * @param {ESTreeJSX.JSXCapturableChild} jsx
62
69
  * @param {number} capture_index
63
- * @returns {{ declaration: any, reference: any }}
70
+ * @param {(id: AST.Identifier, init: AST.Expression) => AST.Identifier} [anchor_id] gives the
71
+ * capture's NAME an authored origin — the only anchorable token when the captured
72
+ * expression itself starts with punctuation
73
+ * @returns {{ declaration: AST.VariableDeclaration, reference: ESTreeJSX.JSXExpressionContainer }}
64
74
  */
65
- export function capture_jsx_child(jsx, capture_index) {
75
+ export function capture_jsx_child(jsx, capture_index, anchor_id) {
66
76
  const name = `_tsrx_child_${capture_index}`;
67
77
  const init = jsx.type === 'JSXExpressionContainer' ? jsx.expression : jsx;
68
78
 
69
- const declaration = /** @type {any} */ ({
70
- type: 'VariableDeclaration',
71
- kind: 'const',
72
- declarations: [
73
- /** @type {any} */ ({
74
- type: 'VariableDeclarator',
75
- id: /** @type {any} */ ({
76
- type: 'Identifier',
77
- name,
78
- metadata: { path: [] },
79
- }),
80
- init,
81
- metadata: { path: [] },
82
- }),
83
- ],
84
- metadata: { path: [] },
85
- });
79
+ const declaration = b.const(anchor_id ? anchor_id(b.id(name), init) : b.id(name), init);
86
80
 
87
81
  // NOTE: JSXExpressionContainer nodes are intentionally created without
88
82
  // loc — they're synthetic wrappers whose source positions don't
89
83
  // correspond to source-map entries and adding loc causes Volar mapping
90
84
  // failures.
91
- const reference = /** @type {any} */ ({
92
- type: 'JSXExpressionContainer',
93
- expression: /** @type {any} */ ({
94
- type: 'Identifier',
95
- name,
96
- metadata: { path: [] },
97
- }),
98
- metadata: { path: [] },
99
- });
85
+ const reference = b.jsx_expression_container(b.id(name));
100
86
 
101
87
  return { declaration, reference };
102
88
  }
@@ -5,6 +5,8 @@
5
5
  * `.foo.hash`) match after rendering.
6
6
  */
7
7
 
8
+ /** @import * as AST from 'estree' */
9
+
8
10
  import { walk } from 'zimmerframe';
9
11
  import * as b from '../utils/builders.js';
10
12
  import { mark_class_map_selectors } from './style-ref.js';
@@ -91,8 +93,8 @@ function is_unreachable_via_class_map(complex_selector, path) {
91
93
  }
92
94
 
93
95
  /**
94
- * @param {any} node
95
- * @returns {boolean}
96
+ * @param {AST.Node | null | undefined} node
97
+ * @returns {node is AST.JSXStyleElement}
96
98
  */
97
99
  export function is_style_element(node) {
98
100
  return !!node && node.type === 'JSXStyleElement';
@@ -68,6 +68,7 @@ import { should_preserve_comment } from '../comment-utils.js';
68
68
  import { has_location } from '../utils/ast.js';
69
69
 
70
70
  const LAZY_PARAM_IDENTIFIER_REGEX = /^__lazy\d+$/;
71
+ const RETURN_KEYWORD = 'return';
71
72
 
72
73
  /**
73
74
  * @param {string} value
@@ -1506,18 +1507,31 @@ export function convert_source_map_to_mappings(
1506
1507
  visit(node.argument);
1507
1508
  }
1508
1509
 
1509
- if (node.type === 'ReturnStatement' && has_location(node)) {
1510
+ // Map only the `return` KEYWORD: a whole-statement mapping is too broad
1511
+ // and shadows the finer mappings of everything inside it.
1512
+ //
1513
+ // That clamp is only meaningful when the author actually wrote the
1514
+ // keyword there. A SYNTHESIZED return — every template arm in a
1515
+ // `.tsrx` file (`@case`/`@default`/`@empty`/`@else` bodies, a `@{ … }`
1516
+ // body) — carries the arm's authored range instead, whose text is the
1517
+ // arm's own syntax. Clamping that to six characters pairs an arbitrary
1518
+ // slice of source (`@defau`) with an arbitrary slice of output
1519
+ // (`defaul`), which then wins any narrowest-match lookup. A
1520
+ // synthesized return has no authored keyword to point at, so it
1521
+ // contributes no mapping.
1522
+ if (
1523
+ node.type === 'ReturnStatement' &&
1524
+ has_location(node) &&
1525
+ source.startsWith(RETURN_KEYWORD, node.start)
1526
+ ) {
1510
1527
  const mapping = get_mapping_from_node(
1511
1528
  node,
1512
1529
  src_to_gen_map,
1513
1530
  gen_line_offsets,
1514
1531
  mapping_data_verify_only,
1515
1532
  );
1516
- // We're only mapping the 'return' keyword, otherwise the mapping would be too broad
1517
- // and likely may cause issues with partial mappings of something inside the return statement that we need
1518
- const return_keyword_length = 'return'.length;
1519
- mapping.lengths = [return_keyword_length];
1520
- mapping.generatedLengths = [return_keyword_length];
1533
+ mapping.lengths = [RETURN_KEYWORD.length];
1534
+ mapping.generatedLengths = [RETURN_KEYWORD.length];
1521
1535
 
1522
1536
  mappings.push(mapping);
1523
1537
  }
package/src/utils/ast.js CHANGED
@@ -20,6 +20,43 @@
20
20
 
21
21
  import * as b from './builders.js';
22
22
 
23
+ /**
24
+ * @param {unknown} value
25
+ * @returns {value is AST.Node}
26
+ */
27
+ export function is_ast_node(value) {
28
+ return !!value && typeof value === 'object' && 'type' in value;
29
+ }
30
+
31
+ /**
32
+ * The child nodes reachable from `node`'s own properties, flattening node
33
+ * arrays. Positional and metadata keys are skipped: they never hold children,
34
+ * and `metadata` in particular can hold memoized lowerings of nodes that are
35
+ * already reachable elsewhere in the tree.
36
+ *
37
+ * @param {AST.Node} node
38
+ * @param {string} [skip_key] an extra own key to skip
39
+ * @returns {AST.Node[]}
40
+ */
41
+ export function child_nodes(node, skip_key) {
42
+ /** @type {AST.Node[]} */
43
+ const children = [];
44
+ const entries = /** @type {AST.TraversableAstNode} */ (node);
45
+ for (const key of Object.keys(entries)) {
46
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
47
+ if (key === skip_key) continue;
48
+ const value = entries[key];
49
+ if (Array.isArray(value)) {
50
+ for (const item of value) {
51
+ if (is_ast_node(item)) children.push(item);
52
+ }
53
+ } else if (is_ast_node(value)) {
54
+ children.push(value);
55
+ }
56
+ }
57
+ return children;
58
+ }
59
+
23
60
  /**
24
61
  * @template {object} T
25
62
  * @param {T | null | undefined} node
@@ -605,13 +605,17 @@ export function ts_type_literal(members, loc_info) {
605
605
  }
606
606
 
607
607
  /**
608
+ * `@types/estree`'s `Statement` union has no TS declarations in it, so the
609
+ * result is typed as both: a `type X = …` alias occupies a statement slot
610
+ * everywhere the transforms emit one.
611
+ *
608
612
  * @param {AST.Identifier} id
609
613
  * @param {AST.Node} type_annotation
610
614
  * @param {AST.NodeWithLocation} [loc_info]
611
- * @returns {AST.TSTypeAliasDeclaration}
615
+ * @returns {AST.TSTypeAliasDeclaration & AST.Statement}
612
616
  */
613
617
  export function ts_type_alias(id, type_annotation, loc_info) {
614
- const node = /** @type {AST.TSTypeAliasDeclaration} */ ({
618
+ const node = /** @type {AST.TSTypeAliasDeclaration & AST.Statement} */ ({
615
619
  type: 'TSTypeAliasDeclaration',
616
620
  id,
617
621
  typeParameters: undefined,
@@ -1090,8 +1094,8 @@ export function try_builder(block, handler = null, finalizer = null, pending = n
1090
1094
  }
1091
1095
 
1092
1096
  /**
1093
- * @param {AST.Pattern | null} param
1094
- * @param {AST.Pattern | null} reset_param
1097
+ * @param {AST.Pattern | null | undefined} param
1098
+ * @param {AST.Pattern | null | undefined} reset_param
1095
1099
  * @param {AST.BlockStatement} body
1096
1100
  * @param {AST.NodeWithLocation} [loc_info]
1097
1101
  * @return {AST.CatchClause}
@@ -1100,8 +1104,8 @@ export function catch_clause_builder(param, reset_param, body, loc_info) {
1100
1104
  /** @type {AST.CatchClause} */
1101
1105
  const node = {
1102
1106
  type: 'CatchClause',
1103
- param,
1104
- resetParam: reset_param,
1107
+ param: param ?? null,
1108
+ resetParam: reset_param ?? null,
1105
1109
  body,
1106
1110
  metadata: { path: [] },
1107
1111
  };
@@ -1147,7 +1151,7 @@ export function jsx_attribute(name, value = null, shorthand = false, loc_info) {
1147
1151
  * @param {boolean} [self_closing]
1148
1152
  * @param {ESTreeJSX.JSXOpeningElement['typeArguments']} [type_arguments]
1149
1153
  * @param {AST.NodeWithLocation} [loc_info]
1150
- * @returns {ESTreeJSX.JSXOpeningElement}
1154
+ * @returns {ESTreeJSX.TSRXJSXOpeningElement}
1151
1155
  */
1152
1156
  export function jsx_opening_element(
1153
1157
  name,
@@ -1156,7 +1160,7 @@ export function jsx_opening_element(
1156
1160
  type_arguments = undefined,
1157
1161
  loc_info,
1158
1162
  ) {
1159
- const node = /** @type {ESTreeJSX.JSXOpeningElement} */ ({
1163
+ const node = /** @type {ESTreeJSX.TSRXJSXOpeningElement} */ ({
1160
1164
  type: 'JSXOpeningElement',
1161
1165
  name,
1162
1166
  attributes,
@@ -1173,10 +1177,10 @@ export function jsx_opening_element(
1173
1177
  *
1174
1178
  * @param {ESTreeJSX.TSRXJSXClosingElement['name']} name
1175
1179
  * @param {AST.NodeWithLocation} [loc_info]
1176
- * @returns {ESTreeJSX.JSXClosingElement}
1180
+ * @returns {ESTreeJSX.TSRXJSXClosingElement}
1177
1181
  */
1178
1182
  export function jsx_closing_element(name, loc_info) {
1179
- const node = /** @type {ESTreeJSX.JSXClosingElement} */ ({
1183
+ const node = /** @type {ESTreeJSX.TSRXJSXClosingElement} */ ({
1180
1184
  type: 'JSXClosingElement',
1181
1185
  name,
1182
1186
  metadata: { path: [] },
@@ -1191,11 +1195,14 @@ export function jsx_closing_element(name, loc_info) {
1191
1195
  * derived from an existing source node, use `jsx_element` (which spreads
1192
1196
  * the source's name and metadata).
1193
1197
  *
1194
- * @param {ESTreeJSX.JSXOpeningElement} opening_element
1195
- * @param {ESTreeJSX.JSXClosingElement | null} [closing_element]
1196
- * @param {ESTreeJSX.JSXElement['children']} [children]
1198
+ * Carries the parser's widened TSRX shape (dynamic-tag names, lowered template
1199
+ * children) — see {@link jsx_element}.
1200
+ *
1201
+ * @param {ESTreeJSX.TSRXJSXOpeningElement} opening_element
1202
+ * @param {ESTreeJSX.TSRXJSXClosingElement | null} [closing_element]
1203
+ * @param {AST.TSRXJSXElement['children']} [children]
1197
1204
  * @param {AST.NodeWithLocation} [loc_info]
1198
- * @returns {ESTreeJSX.JSXElement}
1205
+ * @returns {AST.TSRXJSXElement}
1199
1206
  */
1200
1207
  export function jsx_element_fresh(
1201
1208
  opening_element,
@@ -1203,7 +1210,7 @@ export function jsx_element_fresh(
1203
1210
  children = [],
1204
1211
  loc_info,
1205
1212
  ) {
1206
- const node = /** @type {ESTreeJSX.JSXElement} */ ({
1213
+ const node = /** @type {AST.TSRXJSXElement} */ ({
1207
1214
  type: 'JSXElement',
1208
1215
  openingElement: opening_element,
1209
1216
  closingElement: closing_element,
@@ -1,6 +1,5 @@
1
1
  export type RequireAllOrNone<T, K extends keyof T> =
2
- | (T & Required<Pick<T, K>>)
3
- | (T & { [P in K]?: never });
2
+ (T & Required<Pick<T, K>>) | (T & { [P in K]?: never });
4
3
 
5
4
  export type RequiredPresent<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
6
5
 
package/types/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import type { Parse } from './parse.js';
5
5
  import type * as ESRap from 'esrap';
6
6
  import type { Position } from 'acorn';
7
7
  import type { RequireAllOrNone } from './helpers';
8
+ import type { Context as ZimmerframeContext } from 'zimmerframe';
8
9
  import type {
9
10
  JsxPlatform,
10
11
  JsxPlatformHooks,
@@ -27,7 +28,8 @@ export { createJsxTransform };
27
28
  /** Result of extracting a branch body into a generated helper component. */
28
29
  export interface JsxHelperComponent {
29
30
  setup_statements: AST.Statement[];
30
- component_element: ESTreeJSX.JSXElement;
31
+ /** The parser's widened TSRX element shape — see {@link AST.TSRXJSXElement}. */
32
+ component_element: AST.TSRXJSXElement;
31
33
  }
32
34
 
33
35
  export function collectStyleRefAttributes(
@@ -86,7 +88,7 @@ export interface CompileOptions {
86
88
  }
87
89
 
88
90
  export type NameSpace = 'html' | 'svg' | 'mathml';
89
- interface BaseNodeMetaData {
91
+ export interface BaseNodeMetaData {
90
92
  scoped?: boolean;
91
93
  path: AST.Node[];
92
94
  has_template?: boolean;
@@ -105,6 +107,8 @@ interface BaseNodeMetaData {
105
107
  commentContainerId?: number;
106
108
  parenthesized?: boolean;
107
109
  native_tsrx?: boolean;
110
+ /** The function's body came from a `@{ … }` code block that has been lowered. */
111
+ native_tsrx_body?: boolean;
108
112
  tsrx_generated_wrapper?: boolean;
109
113
  native_tsrx_template_block?: boolean;
110
114
  dynamicElement?: boolean;
@@ -155,9 +159,19 @@ interface BaseNodeMetaData {
155
159
  disable_verification?: boolean;
156
160
  extra_source_mappings?: AST.NodeWithLocation[];
157
161
  generated_setup_declarations?: AST.Statement[];
162
+ /** Helper components lifted out of a component; read back by `expand_component_helpers`. */
163
+ generated_helpers?: AST.Statement[];
164
+ /** Module-level static JSX hoisted out of a component. */
165
+ generated_statics?: AST.Statement[];
158
166
  has_unmappable_value?: boolean;
159
167
  synthetic_ref?: boolean;
160
168
  tsrx_reactive_block?: boolean;
169
+ /** Generated dynamic-tag render-block closure, not a user component boundary. */
170
+ tsrx_dynamic_wrapper?: boolean;
171
+ /** Scoped-class definition sites, for editor definitions/hover on `style.x`. */
172
+ styleClasses?: StyleClasses;
173
+ /** Top-level scoped classes collected while pruning the component's CSS. */
174
+ topScopedClasses?: TopScopedClasses;
161
175
  vapor_pending_fallback?: ESTreeJSX.JSXRenderNode;
162
176
  lazy_param_binding_mappings?: Array<{
163
177
  source: AST.Identifier;
@@ -165,7 +179,7 @@ interface BaseNodeMetaData {
165
179
  }>;
166
180
  }
167
181
 
168
- interface FunctionMetaData extends BaseNodeMetaData {
182
+ export interface FunctionMetaData extends BaseNodeMetaData {
169
183
  native_tsrx?: boolean;
170
184
  native_tsrx_function?: boolean;
171
185
  is_method?: boolean;
@@ -173,11 +187,7 @@ interface FunctionMetaData extends BaseNodeMetaData {
173
187
  has_lazy_descendants?: boolean;
174
188
  /** The component's extracted `<style>` stylesheet (element-level scoped-class info lives on BaseNodeMetaData's `css`). */
175
189
  component_css?: AST.CSS.StyleSheet | null;
176
- /** Top-level scoped classes collected while pruning the component's CSS. */
177
- topScopedClasses?: TopScopedClasses;
178
190
  synthetic_children?: boolean;
179
- generated_helpers?: AST.Statement[];
180
- generated_statics?: AST.Statement[];
181
191
  }
182
192
 
183
193
  // Strip parent, loc, and range from TSESTree nodes to match @sveltejs/acorn-typescript output
@@ -405,6 +415,8 @@ declare module 'estree' {
405
415
  test: AST.Expression;
406
416
  consequent: AST.Statement;
407
417
  alternate: AST.Statement | null;
418
+ /** Span of the `@else` keyword; only present when `alternate` is. */
419
+ alternateKeyword?: AST.NodeWithLocation | null;
408
420
  metadata: BaseNodeMetaData;
409
421
  }
410
422
 
@@ -415,6 +427,11 @@ declare module 'estree' {
415
427
  index?: AST.Identifier | null;
416
428
  key?: AST.Expression | null;
417
429
  empty?: AST.BlockStatement | null;
430
+ /**
431
+ * Span of the `@empty` keyword; only present when `empty` is. The clause's
432
+ * block starts at its `{`, so this is the only pointer to the keyword text.
433
+ */
434
+ emptyKeyword?: AST.NodeWithLocation | null;
418
435
  metadata: BaseNodeMetaData;
419
436
  }
420
437
 
@@ -461,10 +478,7 @@ declare module 'estree' {
461
478
 
462
479
  /** A `@if`/`@for`/`@switch`/`@try` template control-flow directive. */
463
480
  type JSXTemplateDirective =
464
- | JSXIfExpression
465
- | JSXForExpression
466
- | JSXSwitchExpression
467
- | JSXTryExpression;
481
+ JSXIfExpression | JSXForExpression | JSXSwitchExpression | JSXTryExpression;
468
482
 
469
483
  /** A statement-form template directive after its parser node has been retyped. */
470
484
  type JSXTemplateStatement =
@@ -525,16 +539,27 @@ declare module 'estree' {
525
539
  interface TryStatement {
526
540
  statementType?: 'TryStatement';
527
541
  pending?: AST.BlockStatement | null;
542
+ /** Span of the `@pending` keyword; only present when `pending` is. */
543
+ pendingKeyword?: AST.NodeWithLocation | null;
544
+ /** Span of the `@catch` keyword; only present when `handler` is. */
545
+ handlerKeyword?: AST.NodeWithLocation | null;
528
546
  }
529
547
 
530
548
  interface IfStatement {
531
549
  statementType?: 'IfStatement';
550
+ /** Span of the `@else` keyword; only present when `alternate` is. */
551
+ alternateKeyword?: AST.NodeWithLocation | null;
532
552
  }
533
553
 
534
554
  interface SwitchStatement {
535
555
  statementType?: 'SwitchStatement';
536
556
  }
537
557
 
558
+ interface SwitchCase {
559
+ /** Span of the arm's `@case`/`@default` keyword. */
560
+ keyword?: AST.NodeWithLocation | null;
561
+ }
562
+
538
563
  interface CatchClause {
539
564
  resetParam?: AST.Pattern | null;
540
565
  }
@@ -544,6 +569,8 @@ declare module 'estree' {
544
569
  index?: AST.Identifier | null;
545
570
  key?: AST.Expression | null;
546
571
  empty?: AST.BlockStatement | null;
572
+ /** Span of the `@empty` keyword; only present when `empty` is. */
573
+ emptyKeyword?: AST.NodeWithLocation | null;
547
574
  }
548
575
 
549
576
  interface ImportDeclaration {
@@ -796,8 +823,21 @@ declare module 'estree-jsx' {
796
823
  /** A node that can be returned from a platform hook into a JSX render slot. */
797
824
  type JSXRenderNode = AST.Expression | JSXExpressionContainer | JSXText | JSXSpreadChild;
798
825
 
799
- /** A JSX child produced by the transform's render-body lowering. */
800
- type JSXRenderChild = JSXElement | JSXFragment | JSXExpressionContainer | JSXText;
826
+ /**
827
+ * A JSX child produced by the transform's render-body lowering. Elements and
828
+ * fragments carry the parser's widened TSRX shape, which plain estree-jsx
829
+ * elements are assignable to.
830
+ */
831
+ type JSXRenderChild = AST.TSRXJSXElement | AST.TSRXJSXFragment | JSXExpressionContainer | JSXText;
832
+
833
+ /**
834
+ * A JSX child that evaluates to a single expression, so it can be captured
835
+ * into a `const` at its source position.
836
+ */
837
+ type JSXCapturableChild =
838
+ | AST.TSRXJSXElement
839
+ | AST.TSRXJSXFragment
840
+ | (JSXExpressionContainer & { expression: AST.Expression });
801
841
 
802
842
  /** An attribute accepted by and emitted from the shared JSX transformer. */
803
843
  type JSXAttributeNode = JSXAttribute | JSXSpreadAttribute;
@@ -810,9 +850,7 @@ declare module 'estree-jsx' {
810
850
 
811
851
  /** A child accepted while TSRX JSX is being lowered to standard ESTree JSX. */
812
852
  type JSXTransformChild =
813
- | JSXElement['children'][number]
814
- | AST.TSRXJSXElement
815
- | AST.TSRXJSXFragment;
853
+ JSXElement['children'][number] | AST.TSRXJSXElement | AST.TSRXJSXFragment;
816
854
 
817
855
  interface JSXAttribute {
818
856
  shorthand: boolean;
@@ -1679,6 +1717,14 @@ export interface TransformClientState extends BaseState {
1679
1717
  ref_target_type?: AST.TypeNode;
1680
1718
  }
1681
1719
 
1720
+ /** Accumulator for the helper components and statics a component lift produces. */
1721
+ export interface JsxHelperState {
1722
+ base_name: string;
1723
+ next_id: number;
1724
+ helpers: AST.Statement[];
1725
+ statics: AST.Statement[];
1726
+ }
1727
+
1682
1728
  /** Override zimmerframe types and provide our own */
1683
1729
  /**
1684
1730
  * Where stock `@types/estree-jsx` and the TSRX parser shapes share a `type`
@@ -1748,6 +1794,13 @@ export type VisitorClientContext = TransformClientContext & {
1748
1794
  value_position?: boolean;
1749
1795
  };
1750
1796
 
1797
+ /**
1798
+ * The zimmerframe visitor context the JSX transform's visitors receive. Walked
1799
+ * over the `Node` union rather than `Program`, since only that union admits a
1800
+ * visitor per node type.
1801
+ */
1802
+ export type JsxVisitorContext = ZimmerframeContext<AST.Node, JsxTransformContext>;
1803
+
1751
1804
  /**
1752
1805
  * Delegated event result
1753
1806
  */
@@ -1766,6 +1819,16 @@ export type TopScopedClasses = Map<
1766
1819
 
1767
1820
  export type StyleClasses = Map<string, AST.MemberExpression['property']>;
1768
1821
 
1822
+ /**
1823
+ * The scoped-CSS work for one native TSRX node: its stylesheet, the `style.x`
1824
+ * ref attributes that reference it, and a hash-annotated copy of the node.
1825
+ */
1826
+ export interface JsxStyleContext {
1827
+ css: AST.CSS.StyleSheet;
1828
+ style_refs: ESTreeJSX.JSXAttribute[];
1829
+ fragment: AST.NativeTSRXNode;
1830
+ }
1831
+
1769
1832
  /**
1770
1833
  * Event handling types
1771
1834
  */
@@ -1,7 +1,7 @@
1
1
  import type * as AST from 'estree';
2
2
  import type * as ESTreeJSX from 'estree-jsx';
3
3
  import type { RawSourceMap } from 'source-map';
4
- import type { CompileError, JsxHelperComponent } from './index';
4
+ import type { CompileError, JsxHelperComponent, JsxHelperState } from './index';
5
5
 
6
6
  /**
7
7
  * Result returned by a JSX platform transform (React, Preact, Solid).
@@ -53,12 +53,7 @@ export interface JsxTransformContext {
53
53
  stylesheets: AST.CSS.StyleSheet[];
54
54
  type_only_style_anchors: AST.Statement[];
55
55
  module_scoped_hook_components: boolean;
56
- helper_state: {
57
- base_name: string;
58
- next_id: number;
59
- helpers: AST.Statement[];
60
- statics: AST.Statement[];
61
- } | null;
56
+ helper_state: JsxHelperState | null;
62
57
  hook_helpers_enabled: boolean;
63
58
  available_bindings: Map<string, AST.Identifier>;
64
59
  lazy_next_id: number;
@@ -75,6 +70,12 @@ export interface JsxTransformContext {
75
70
  comments: AST.CommentWithLocation[] | undefined;
76
71
  /** True when emitting a type-only virtual TSX module; preserves lazy destructuring patterns. */
77
72
  typeOnly: boolean;
73
+ /**
74
+ * True when generated nodes may be anchored on the directive keyword that
75
+ * produced them. Off by default; the emitted code and its source map are
76
+ * unchanged when clear.
77
+ */
78
+ inspect: boolean;
78
79
  }
79
80
 
80
81
  /**
@@ -121,6 +122,15 @@ export interface JsxTransformOptions {
121
122
  * bindings.
122
123
  */
123
124
  typeOnly?: boolean;
125
+ /**
126
+ * Anchor generated nodes on the directive keyword that produced them, so
127
+ * navigation tooling can trace an authored `@for` / `@empty` to the code it
128
+ * became. Off by default — a directive is lowered away entirely, so nothing
129
+ * in the source map otherwise reaches its keyword. With the flag clear the
130
+ * output bytes and every mapping derived from them are unaffected, which is
131
+ * why the editor pipeline never asks for it.
132
+ */
133
+ inspect?: boolean;
124
134
  }
125
135
 
126
136
  /**
@@ -303,7 +313,7 @@ export interface JsxPlatformHooks {
303
313
  * otherwise statically safe. Targets can use this to keep runtime-sensitive
304
314
  * JSX, such as component invocations, inside render/setup execution.
305
315
  */
306
- canHoistStaticNode?: (node: ESTreeJSX.JSXElement, ctx: JsxTransformContext) => boolean;
316
+ canHoistStaticNode?: (node: AST.TSRXJSXElement, ctx: JsxTransformContext) => boolean;
307
317
  /**
308
318
  * Custom validation for a component body that uses top-level `await`.
309
319
  * Default: enforce `validation.requireUseServerForAwait`. Solid rejects
package/types/parse.d.ts CHANGED
@@ -1677,8 +1677,7 @@ export namespace Parse {
1677
1677
  * Parse JSX namespaced name (ns:name)
1678
1678
  */
1679
1679
  jsx_parseNamespacedName():
1680
- | ESTreeJSX.JSXNamespacedName
1681
- | ReturnType<Parser['jsx_parseIdentifier']>;
1680
+ ESTreeJSX.JSXNamespacedName | ReturnType<Parser['jsx_parseIdentifier']>;
1682
1681
 
1683
1682
  /**
1684
1683
  * Parse JSX element name (identifier, member, namespaced, or a dynamic
@@ -1695,8 +1694,7 @@ export namespace Parse {
1695
1694
  * @returns Attribute value (expression, string, or element)
1696
1695
  */
1697
1696
  jsx_parseAttributeValue():
1698
- | ESTreeJSX.JSXExpressionContainer
1699
- | ReturnType<Parser['parseExprAtom']>;
1697
+ ESTreeJSX.JSXExpressionContainer | ReturnType<Parser['parseExprAtom']>;
1700
1698
 
1701
1699
  /**
1702
1700
  * Parse JSX empty expression (for {})
@@ -14,11 +14,7 @@ export type RefValue<T = Element> =
14
14
  | undefined;
15
15
 
16
16
  export type MergeableRef<T> =
17
- | MergeableRefCallback<T>
18
- | MergeableRefObject<T>
19
- | MergeableVueRef<T>
20
- | null
21
- | undefined;
17
+ MergeableRefCallback<T> | MergeableRefObject<T> | MergeableVueRef<T> | null | undefined;
22
18
 
23
19
  export function mergeRefs<T = any>(...refs: Array<MergeableRef<T>>): (node: T | null) => () => void;
24
20
  export function isRefProp(value: unknown): boolean;