@tsrx/core 0.1.47 → 0.1.49

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
@@ -310,6 +320,7 @@ declare module 'estree' {
310
320
  // Include TypeScript node types and TSRX-specific nodes in NodeMap
311
321
  interface NodeMap {
312
322
  JSXSpreadChild: ESTreeJSX.JSXSpreadChild;
323
+ TSRXImportDeclaration: TSRXImportDeclaration;
313
324
  TSRXJSXElement: TSRXJSXElement;
314
325
  TSRXJSXFragment: TSRXJSXFragment;
315
326
  TSRXJSXOpeningElement: ESTreeJSX.TSRXJSXOpeningElement;
@@ -404,6 +415,8 @@ declare module 'estree' {
404
415
  test: AST.Expression;
405
416
  consequent: AST.Statement;
406
417
  alternate: AST.Statement | null;
418
+ /** Span of the `@else` keyword; only present when `alternate` is. */
419
+ alternateKeyword?: AST.NodeWithLocation | null;
407
420
  metadata: BaseNodeMetaData;
408
421
  }
409
422
 
@@ -414,6 +427,11 @@ declare module 'estree' {
414
427
  index?: AST.Identifier | null;
415
428
  key?: AST.Expression | null;
416
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;
417
435
  metadata: BaseNodeMetaData;
418
436
  }
419
437
 
@@ -460,10 +478,7 @@ declare module 'estree' {
460
478
 
461
479
  /** A `@if`/`@for`/`@switch`/`@try` template control-flow directive. */
462
480
  type JSXTemplateDirective =
463
- | JSXIfExpression
464
- | JSXForExpression
465
- | JSXSwitchExpression
466
- | JSXTryExpression;
481
+ JSXIfExpression | JSXForExpression | JSXSwitchExpression | JSXTryExpression;
467
482
 
468
483
  /** A statement-form template directive after its parser node has been retyped. */
469
484
  type JSXTemplateStatement =
@@ -524,16 +539,27 @@ declare module 'estree' {
524
539
  interface TryStatement {
525
540
  statementType?: 'TryStatement';
526
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;
527
546
  }
528
547
 
529
548
  interface IfStatement {
530
549
  statementType?: 'IfStatement';
550
+ /** Span of the `@else` keyword; only present when `alternate` is. */
551
+ alternateKeyword?: AST.NodeWithLocation | null;
531
552
  }
532
553
 
533
554
  interface SwitchStatement {
534
555
  statementType?: 'SwitchStatement';
535
556
  }
536
557
 
558
+ interface SwitchCase {
559
+ /** Span of the arm's `@case`/`@default` keyword. */
560
+ keyword?: AST.NodeWithLocation | null;
561
+ }
562
+
537
563
  interface CatchClause {
538
564
  resetParam?: AST.Pattern | null;
539
565
  }
@@ -543,10 +569,19 @@ declare module 'estree' {
543
569
  index?: AST.Identifier | null;
544
570
  key?: AST.Expression | null;
545
571
  empty?: AST.BlockStatement | null;
572
+ /** Span of the `@empty` keyword; only present when `empty` is. */
573
+ emptyKeyword?: AST.NodeWithLocation | null;
546
574
  }
547
575
 
548
576
  interface ImportDeclaration {
549
577
  importKind: TSESTree.ImportDeclaration['importKind'];
578
+ phase?: 'defer' | null;
579
+ }
580
+ interface TSRXImportDeclaration extends Omit<ImportDeclaration, 'source'> {
581
+ source: AST.Literal | AST.Identifier;
582
+ }
583
+ interface ImportExpression {
584
+ phase?: 'defer' | null;
550
585
  }
551
586
  interface ImportSpecifier {
552
587
  importKind: TSESTree.ImportSpecifier['importKind'];
@@ -788,8 +823,21 @@ declare module 'estree-jsx' {
788
823
  /** A node that can be returned from a platform hook into a JSX render slot. */
789
824
  type JSXRenderNode = AST.Expression | JSXExpressionContainer | JSXText | JSXSpreadChild;
790
825
 
791
- /** A JSX child produced by the transform's render-body lowering. */
792
- 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 });
793
841
 
794
842
  /** An attribute accepted by and emitted from the shared JSX transformer. */
795
843
  type JSXAttributeNode = JSXAttribute | JSXSpreadAttribute;
@@ -802,9 +850,7 @@ declare module 'estree-jsx' {
802
850
 
803
851
  /** A child accepted while TSRX JSX is being lowered to standard ESTree JSX. */
804
852
  type JSXTransformChild =
805
- | JSXElement['children'][number]
806
- | AST.TSRXJSXElement
807
- | AST.TSRXJSXFragment;
853
+ JSXElement['children'][number] | AST.TSRXJSXElement | AST.TSRXJSXFragment;
808
854
 
809
855
  interface JSXAttribute {
810
856
  shorthand: boolean;
@@ -1391,7 +1437,7 @@ export interface AnalysisResult {
1391
1437
  component_metadata: Array<{ id: string }>;
1392
1438
  metadata: {
1393
1439
  serverImportsPresent: boolean;
1394
- serverImportDeclarations: AST.ImportDeclaration[];
1440
+ serverImportDeclarations: AST.TSRXImportDeclaration[];
1395
1441
  serverModule: AST.TSModuleDeclaration | null;
1396
1442
  };
1397
1443
  errors: CompileError[];
@@ -1671,6 +1717,14 @@ export interface TransformClientState extends BaseState {
1671
1717
  ref_target_type?: AST.TypeNode;
1672
1718
  }
1673
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
+
1674
1728
  /** Override zimmerframe types and provide our own */
1675
1729
  /**
1676
1730
  * Where stock `@types/estree-jsx` and the TSRX parser shapes share a `type`
@@ -1740,6 +1794,13 @@ export type VisitorClientContext = TransformClientContext & {
1740
1794
  value_position?: boolean;
1741
1795
  };
1742
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
+
1743
1804
  /**
1744
1805
  * Delegated event result
1745
1806
  */
@@ -1758,6 +1819,16 @@ export type TopScopedClasses = Map<
1758
1819
 
1759
1820
  export type StyleClasses = Map<string, AST.MemberExpression['property']>;
1760
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
+
1761
1832
  /**
1762
1833
  * Event handling types
1763
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