@tsrx/core 0.1.24 → 0.1.26

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.
@@ -112,6 +112,12 @@ export function is_hoist_safe_jsx_node(node) {
112
112
  return node.children.every(is_hoist_safe_jsx_child);
113
113
  }
114
114
 
115
+ // Lowered dynamic tags reference a component-scoped const and resolve at
116
+ // runtime — never static, never hoistable.
117
+ if (/** @type {any} */ (node).metadata?.dynamicElement === true) {
118
+ return false;
119
+ }
120
+
115
121
  return (
116
122
  node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
117
123
  node.children.every(is_hoist_safe_jsx_child)
@@ -43,6 +43,10 @@ export function is_interleaved_body(body_nodes, is_jsx_child) {
43
43
  */
44
44
  export function is_capturable_jsx_child(jsx) {
45
45
  if (!jsx) return false;
46
+ // Reactive-block containers (dynamic tags) must stay expression children
47
+ // so the host JSX compiler wraps them in a render block; capturing them
48
+ // into a const would evaluate them once.
49
+ if (jsx.metadata?.tsrx_reactive_block === true) return false;
46
50
  const t = jsx.type;
47
51
  return t === 'JSXElement' || t === 'JSXFragment' || t === 'JSXExpressionContainer';
48
52
  }
@@ -75,15 +75,19 @@ export function annotate_with_hash(
75
75
  ) {
76
76
  if (!node || typeof node !== 'object') return node;
77
77
  if (
78
- node.type === 'FunctionDeclaration' ||
79
- node.type === 'FunctionExpression' ||
80
- node.type === 'ArrowFunctionExpression'
78
+ (node.type === 'FunctionDeclaration' ||
79
+ node.type === 'FunctionExpression' ||
80
+ node.type === 'ArrowFunctionExpression') &&
81
+ // Generated dynamic-tag wrappers are render-block closures, not user
82
+ // component boundaries — the element inside still belongs to this
83
+ // component's scoped CSS.
84
+ node.metadata?.tsrx_dynamic_wrapper !== true
81
85
  ) {
82
86
  return node;
83
87
  }
84
88
 
85
89
  if (node.type === 'JSXElement') {
86
- if (!is_composite_jsx_element(node) || node.metadata?.runtime_dynamic_element) {
90
+ if (!is_composite_jsx_element(node) || node.metadata?.dynamicElement) {
87
91
  add_hash_class_to_jsx_element(node, hash, jsx_class_attr_name);
88
92
  }
89
93
  if (Array.isArray(node.children)) {
@@ -27,6 +27,7 @@
27
27
  generated: string;
28
28
  loc: AST.SourceLocation;
29
29
  metadata: PluginActionOverrides;
30
+ generatedLoc?: AST.SourceLocation;
30
31
  end_loc?: AST.SourceLocation;
31
32
  sourceLength?: number;
32
33
  mappingData?: Partial<VolarCodeMapping['data']>;
@@ -387,7 +388,8 @@ export function convert_source_map_to_mappings(
387
388
  * @returns {{ line: number; column: number }}
388
389
  */
389
390
  function get_generated_position_for_token(token) {
390
- const key = `${token.loc.start.line}:${token.loc.start.column}`;
391
+ const generated_loc = token.generatedLoc ?? token.loc;
392
+ const key = `${generated_loc.start.line}:${generated_loc.start.column}`;
391
393
  const positions = src_to_gen_map.get(key);
392
394
  if (!positions || positions.length === 0) {
393
395
  throw new Error(`No source map entry for position "${key}"`);
@@ -478,6 +480,32 @@ export function convert_source_map_to_mappings(
478
480
  }
479
481
  }
480
482
 
483
+ /**
484
+ * @param {any} generated_node
485
+ * @returns {void}
486
+ */
487
+ function add_extra_source_mapping_tokens(generated_node) {
488
+ if (!generated_node?.loc || !Array.isArray(generated_node.metadata?.extra_source_mappings)) {
489
+ return;
490
+ }
491
+
492
+ for (const source_node of generated_node.metadata.extra_source_mappings) {
493
+ if (!source_node?.loc) continue;
494
+
495
+ tokens.push({
496
+ source: source_node.name ?? generated_node.name,
497
+ generated: generated_node.name,
498
+ loc: source_node.loc,
499
+ generatedLoc: generated_node.loc,
500
+ metadata: {},
501
+ sourceLength:
502
+ typeof source_node.start === 'number' && typeof source_node.end === 'number'
503
+ ? source_node.end - source_node.start
504
+ : undefined,
505
+ });
506
+ }
507
+ }
508
+
481
509
  // We have to visit everything in generated order to maintain correct indices
482
510
 
483
511
  walk(ast, null, {
@@ -521,6 +549,7 @@ export function convert_source_map_to_mappings(
521
549
  token.mappingData = { ...mapping_data, verification: false };
522
550
  }
523
551
  tokens.push(token);
552
+ add_extra_source_mapping_tokens(node);
524
553
 
525
554
  if (Array.isArray(node.metadata?.lazy_param_binding_mappings)) {
526
555
  for (const binding_mapping of node.metadata.lazy_param_binding_mappings) {
@@ -558,6 +587,7 @@ export function convert_source_map_to_mappings(
558
587
  token.mappingData = { ...mapping_data, verification: false };
559
588
  }
560
589
  tokens.push(token);
590
+ add_extra_source_mapping_tokens(node);
561
591
  }
562
592
  return; // Leaf node, don't traverse further
563
593
  } else if (node.type === 'Literal') {
package/types/index.d.ts CHANGED
@@ -84,7 +84,7 @@ interface BaseNodeMetaData {
84
84
  parenthesized?: boolean;
85
85
  native_tsrx?: boolean;
86
86
  native_tsrx_template_block?: boolean;
87
- runtime_dynamic_element?: boolean;
87
+ dynamicElement?: boolean;
88
88
  templateMode?: 'script' | 'template';
89
89
  script_only?: boolean;
90
90
  tsrxDirective?: 'if' | 'for' | 'switch' | 'try';
@@ -118,7 +118,6 @@ interface BaseNodeMetaData {
118
118
  interface FunctionMetaData extends BaseNodeMetaData {
119
119
  native_tsrx?: boolean;
120
120
  native_tsrx_function?: boolean;
121
- hook_split?: boolean;
122
121
  is_method?: boolean;
123
122
  tracked?: boolean;
124
123
  has_lazy_descendants?: boolean;
@@ -183,7 +182,6 @@ declare module 'estree' {
183
182
 
184
183
  interface BlockStatement {
185
184
  metadata: BaseNodeMetaData & {
186
- hook_split_block?: boolean;
187
185
  native_return_block?: boolean;
188
186
  native_tsrx_template_block?: boolean;
189
187
  allows_native_return?: boolean;
@@ -301,13 +299,14 @@ declare module 'estree' {
301
299
 
302
300
  interface Element extends AST.BaseExpression {
303
301
  type: 'Element';
304
- id: AST.Identifier | AST.MemberExpression | AST.Literal;
302
+ id: AST.Expression;
305
303
  attributes: Array<Attribute | SpreadAttribute>;
306
304
  children: AST.Node[];
307
305
  openingElement: ESTreeJSX.JSXOpeningElement;
308
306
  closingElement: ESTreeJSX.JSXClosingElement | null;
309
307
  selfClosing?: boolean;
310
308
  unclosed?: boolean;
309
+ isDynamic?: boolean;
311
310
  css?: string;
312
311
  metadata: BaseNodeMetaData;
313
312
  start: number;
@@ -729,6 +728,7 @@ declare module 'estree-jsx' {
729
728
  interface JSXExpressionContainer {
730
729
  text?: boolean;
731
730
  style?: boolean;
731
+ isDynamic?: boolean;
732
732
  }
733
733
 
734
734
  interface JSXMemberExpression {
@@ -39,6 +39,8 @@ export interface JsxTransformContext {
39
39
  needs_normalize_spread_props: boolean;
40
40
  needs_normalize_spread_props_for_ref_attr: boolean;
41
41
  needs_fragment: boolean;
42
+ needs_dynamic_element: boolean;
43
+ needs_dynamic_factory: boolean;
42
44
  needs_for_of_iterable: boolean;
43
45
  needs_iteration_value_type: boolean;
44
46
  stylesheets: AST.CSS.StyleSheet[];
@@ -53,8 +55,6 @@ export interface JsxTransformContext {
53
55
  hook_helpers_enabled: boolean;
54
56
  available_bindings: Map<string, AST.Identifier>;
55
57
  lazy_next_id: number;
56
- /** Scope map used to resolve runtime Dynamic imports for scoped CSS pruning. */
57
- runtime_dynamic_scopes: Map<any, any> | null;
58
58
  inside_element_child?: boolean;
59
59
  /** Full source text for source-aware diagnostics. */
60
60
  source: string;
@@ -102,7 +102,7 @@ export interface JsxTransformOptions {
102
102
  comments?: AST.CommentWithLocation[];
103
103
  /**
104
104
  * Override whether hook-isolation helper components are emitted directly at
105
- * module scope. React runtime compilation enables this, while editor tooling
105
+ * module scope. Some runtime targets enable this, while editor tooling
106
106
  * can disable it to preserve lexical `typeof` helper prop types.
107
107
  */
108
108
  moduleScopedHookComponents?: boolean;
@@ -160,15 +160,10 @@ export interface JsxPlatformHooks {
160
160
  /**
161
161
  * Emit hook-isolation helper components as unique module-scope declarations
162
162
  * instead of lazily creating and caching them from the parent component body.
163
- * React enables this so generated branches stay compatible with the React
164
- * Compiler's Rules of Hooks validation.
163
+ * Targets that use compiler-generated branch helpers enable this so helper
164
+ * declarations are shared across renders.
165
165
  */
166
166
  moduleScopedHookComponents?: boolean;
167
- /**
168
- * Split ordinary uppercase function component bodies when an early
169
- * conditional return would make later React/Preact hooks conditional.
170
- */
171
- componentBodyHookHelpers?: boolean;
172
167
  /**
173
168
  * Inject module-level imports after the main walk. Default: import
174
169
  * `Suspense` from `platform.imports.suspense` and `TsrxErrorBoundary`
@@ -321,11 +316,30 @@ export interface JsxPlatform {
321
316
  */
322
317
  suspense: string;
323
318
  /**
324
- * Module that exports the target runtime `Dynamic` component. When set,
325
- * the shared JSX transform treats imported `Dynamic` elements with an
326
- * `is` prop as runtime-dynamic for scoped CSS pruning.
319
+ * Module the type-only transform imports the `Dynamic` component type
320
+ * from when lowering dynamic tags to `<TsrxDynamic is={expr}>`. The
321
+ * module only needs type declarations; production output never imports
322
+ * it.
327
323
  */
328
324
  dynamic?: string;
325
+ /**
326
+ * Scoped-binding lowering for dynamic tags (`<{expr}>`) in production
327
+ * output; each tag binds a scoped component const referenced like an
328
+ * ordinary component. With `name`/`source`, the factory is imported as
329
+ * `_tsrx_dynamic` and wraps the tag expression in a reactive thunk:
330
+ * `const TsrxDynamic_N = _tsrx_dynamic(() => expr);` (Solid:
331
+ * `{ name: 'dynamic', source: '@solidjs/web' }`). With an empty object,
332
+ * the const is a plain import-free alias re-evaluated by the host's
333
+ * render cycle: `const TsrxDynamic_N = expr;` (React/Preact). With
334
+ * `renderBlock: true`, the alias and element move inside an
335
+ * expression-child IIFE,
336
+ * `{(() => { const TsrxDynamic_N = expr; return <TsrxDynamic_N ...>; })()}`,
337
+ * relying on the host JSX compiler's reactive render block for
338
+ * expression children (Vue Vapor, which never re-runs setup). In
339
+ * type-only output, dynamic tags always lower to
340
+ * `<TsrxDynamic is={expr}>` imported from `imports.dynamic`.
341
+ */
342
+ dynamicFactory?: { name?: string; source?: string; renderBlock?: boolean };
329
343
  /**
330
344
  * Module to import `TsrxErrorBoundary` from when an `@try { ... } @catch (...)`
331
345
  * block appears. Usually `'@tsrx/<platform>/error-boundary'`.
package/types/parse.d.ts CHANGED
@@ -1621,11 +1621,13 @@ export namespace Parse {
1621
1621
  | ReturnType<Parser['jsx_parseIdentifier']>;
1622
1622
 
1623
1623
  /**
1624
- * Parse JSX element name (identifier, member, namespaced)
1624
+ * Parse JSX element name (identifier, member, namespaced, or a dynamic
1625
+ * `{expression}` container)
1625
1626
  */
1626
1627
  jsx_parseElementName():
1627
1628
  | ESTreeJSX.JSXMemberExpression
1628
1629
  | ReturnType<Parser['jsx_parseNamespacedName']>
1630
+ | ESTreeJSX.JSXExpressionContainer
1629
1631
  | '';
1630
1632
 
1631
1633
  /**