@tsrx/core 0.0.18 → 0.0.19

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.
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.0.18",
6
+ "version": "0.0.19",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -28,6 +28,7 @@
28
28
  "types": "./types/acorn.d.ts"
29
29
  },
30
30
  "./runtime/merge-refs": {
31
+ "types": "./types/runtime/merge-refs.d.ts",
31
32
  "default": "./src/runtime/merge-refs.js"
32
33
  },
33
34
  "./test-harness/source-mappings": "./tests/shared/source-mappings.js",
@@ -1,9 +1,10 @@
1
1
  /** @import * as AST from 'estree' */
2
2
  /** @import * as ESTreeJSX from 'estree-jsx' */
3
- /** @import { JsxPlatform, JsxTransformOptions, JsxTransformResult } from '@tsrx/core/types' */
3
+ /** @import { JsxPlatform, JsxTransformContext, JsxTransformOptions, JsxTransformResult } from '@tsrx/core/types' */
4
4
 
5
5
  import { walk } from 'zimmerframe';
6
6
  import { print } from 'esrap';
7
+ import { error } from '../../errors.js';
7
8
  import {
8
9
  ensure_function_metadata,
9
10
  in_jsx_child_context,
@@ -14,7 +15,6 @@ import {
14
15
  clone_expression_node,
15
16
  clone_identifier,
16
17
  clone_jsx_name,
17
- create_compile_error,
18
18
  create_generated_identifier,
19
19
  create_null_literal,
20
20
  flatten_switch_consequent,
@@ -26,7 +26,11 @@ import {
26
26
  to_text_expression,
27
27
  } from './ast-builders.js';
28
28
  import { render_stylesheets as renderStylesheets } from '../stylesheet.js';
29
- import { set_location as setLocation } from '../../utils/builders.js';
29
+ import {
30
+ set_location as setLocation,
31
+ jsx_attribute as build_jsx_attribute,
32
+ jsx_id as build_jsx_id,
33
+ } from '../../utils/builders.js';
30
34
  import {
31
35
  apply_lazy_transforms,
32
36
  collect_lazy_bindings_from_component,
@@ -45,18 +49,11 @@ import {
45
49
  import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
46
50
 
47
51
  /**
48
- * @typedef {{
49
- * platform: JsxPlatform,
50
- * local_statement_component_index: number,
51
- * needs_error_boundary: boolean,
52
- * needs_suspense: boolean,
53
- * needs_merge_refs: boolean,
54
- * helper_state: { base_name: string, next_id: number, helpers: any[], statics: any[] } | null,
55
- * available_bindings: Map<string, AST.Identifier>,
56
- * lazy_next_id: number,
57
- * current_css_hash: string | null,
58
- * inside_element_child?: boolean,
59
- * }} TransformContext
52
+ * Local alias for the shared `JsxTransformContext`. Kept as a typedef so the
53
+ * rest of this file's `@param {TransformContext}` annotations don't all have
54
+ * to spell out the import.
55
+ *
56
+ * @typedef {JsxTransformContext} TransformContext
60
57
  */
61
58
 
62
59
  /**
@@ -99,7 +96,7 @@ export function createJsxTransform(platform) {
99
96
  const stylesheets = [];
100
97
 
101
98
  /** @type {TransformContext} */
102
- const transform_context = /** @type {any} */ ({
99
+ const transform_context = {
103
100
  platform,
104
101
  local_statement_component_index: 0,
105
102
  needs_error_boundary: false,
@@ -109,10 +106,14 @@ export function createJsxTransform(platform) {
109
106
  available_bindings: new Map(),
110
107
  lazy_next_id: 0,
111
108
  current_css_hash: null,
109
+ filename: filename ?? null,
110
+ loose: !!options?.loose,
111
+ errors: options?.loose ? options?.errors : undefined,
112
+ comments: options?.comments,
112
113
  // Platforms can seed their own tracking state (e.g. solid's
113
114
  // needs_show / needs_for flags) via `hooks.initialState`.
114
115
  ...(platform.hooks?.initialState?.() ?? {}),
115
- });
116
+ };
116
117
 
117
118
  preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
118
119
 
@@ -143,9 +144,12 @@ export function createJsxTransform(platform) {
143
144
  source,
144
145
  );
145
146
  } else if (!module_uses_server_directive) {
146
- throw create_compile_error(
147
- await_expression,
147
+ error(
148
148
  `${platform.name} components can only use \`await\` when the module has a top-level "use server" directive.`,
149
+ state.filename,
150
+ await_expression,
151
+ state.errors,
152
+ state.comments,
149
153
  );
150
154
  }
151
155
 
@@ -212,10 +216,10 @@ export function createJsxTransform(platform) {
212
216
  return /** @type {any} */ (tsx_node_to_jsx_expression(inner, in_jsx_child_context(path)));
213
217
  },
214
218
 
215
- TsxCompat(node, { next, path }) {
219
+ TsxCompat(node, { next, path, state }) {
216
220
  const inner = /** @type {any} */ (next() ?? node);
217
221
  return /** @type {any} */ (
218
- tsx_compat_node_to_jsx_expression(inner, platform, in_jsx_child_context(path))
222
+ tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child_context(path))
219
223
  );
220
224
  },
221
225
 
@@ -1485,7 +1489,22 @@ const TEMPLATE_FRAGMENT_ERROR =
1485
1489
  function to_jsx_element(node, transform_context, raw_children = node.children || []) {
1486
1490
  if (node.type === 'JSXElement') return node;
1487
1491
  if (!node.id) {
1488
- throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
1492
+ error(
1493
+ TEMPLATE_FRAGMENT_ERROR,
1494
+ transform_context.filename,
1495
+ node,
1496
+ transform_context.errors,
1497
+ transform_context.comments,
1498
+ );
1499
+ return set_loc(
1500
+ /** @type {any} */ ({
1501
+ type: 'JSXFragment',
1502
+ openingFragment: { type: 'JSXOpeningFragment' },
1503
+ closingFragment: { type: 'JSXClosingFragment' },
1504
+ children: [],
1505
+ }),
1506
+ node,
1507
+ );
1489
1508
  }
1490
1509
  if (is_dynamic_element_id(node.id)) {
1491
1510
  return dynamic_element_to_jsx_child(node, transform_context);
@@ -2111,7 +2130,7 @@ function to_jsx_child(node, transform_context) {
2111
2130
  // JSXExpressionContainer wrapper for bare `{expr}` children.
2112
2131
  return tsx_node_to_jsx_expression(node, true);
2113
2132
  case 'TsxCompat':
2114
- return tsx_compat_node_to_jsx_expression(node, transform_context.platform, true);
2133
+ return tsx_compat_node_to_jsx_expression(node, transform_context, true);
2115
2134
  case 'Element':
2116
2135
  return to_jsx_element(node, transform_context);
2117
2136
  case 'Text':
@@ -2282,9 +2301,12 @@ function find_key_expression_in_body(body_nodes) {
2282
2301
  */
2283
2302
  function for_of_statement_to_jsx_child(node, transform_context) {
2284
2303
  if (node.await) {
2285
- throw create_compile_error(
2286
- node,
2304
+ error(
2287
2305
  `${transform_context.platform.name} TSRX does not support \`for await...of\` in component templates.`,
2306
+ transform_context.filename,
2307
+ node,
2308
+ transform_context.errors,
2309
+ transform_context.comments,
2288
2310
  );
2289
2311
  }
2290
2312
 
@@ -2460,23 +2482,33 @@ function try_statement_to_jsx_child(node, transform_context) {
2460
2482
  const finalizer = node.finalizer;
2461
2483
 
2462
2484
  if (finalizer) {
2463
- throw create_compile_error(
2464
- finalizer,
2485
+ error(
2465
2486
  `${transform_context.platform.name} TSRX does not support JavaScript \`try/finally\` in component templates. \`finally\` is not part of TSRX control flow; move the try/finally into a function if you need cleanup logic.`,
2487
+ transform_context.filename,
2488
+ finalizer,
2489
+ transform_context.errors,
2490
+ transform_context.comments,
2466
2491
  );
2467
2492
  }
2468
2493
 
2469
2494
  if (!pending && !handler) {
2470
- throw create_compile_error(
2471
- node,
2495
+ error(
2472
2496
  'Component try statements must have a `pending` or `catch` block.',
2497
+ transform_context.filename,
2498
+ node,
2499
+ transform_context.errors,
2500
+ transform_context.comments,
2473
2501
  );
2502
+ return to_jsx_expression_container(create_null_literal());
2474
2503
  }
2475
2504
 
2476
2505
  if (pending && transform_context.platform.validation.unsupportedTryPendingMessage) {
2477
- throw create_compile_error(
2478
- pending,
2506
+ error(
2479
2507
  transform_context.platform.validation.unsupportedTryPendingMessage,
2508
+ transform_context.filename,
2509
+ pending,
2510
+ transform_context.errors,
2511
+ transform_context.comments,
2480
2512
  );
2481
2513
  }
2482
2514
 
@@ -2484,16 +2516,22 @@ function try_statement_to_jsx_child(node, transform_context) {
2484
2516
  if (pending) {
2485
2517
  const try_body = node.block.body || [];
2486
2518
  if (!try_body.some(is_jsx_child)) {
2487
- throw create_compile_error(
2488
- node.block,
2519
+ error(
2489
2520
  'Component try statements must contain a template in their main body. Move the try statement into a function if it does not render anything.',
2521
+ transform_context.filename,
2522
+ node.block,
2523
+ transform_context.errors,
2524
+ transform_context.comments,
2490
2525
  );
2491
2526
  }
2492
2527
  const pending_body = pending.body || [];
2493
2528
  if (!pending_body.some(is_jsx_child)) {
2494
- throw create_compile_error(
2495
- pending,
2529
+ error(
2496
2530
  'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
2531
+ transform_context.filename,
2532
+ pending,
2533
+ transform_context.errors,
2534
+ transform_context.comments,
2497
2535
  );
2498
2536
  }
2499
2537
  }
@@ -2963,7 +3001,7 @@ function to_jsx_expression_container(expression, source_node = expression) {
2963
3001
  * @returns {any[]}
2964
3002
  */
2965
3003
  function transform_element_attributes_dispatch(attrs, transform_context, element) {
2966
- validate_at_most_one_ref_attribute(attrs);
3004
+ validate_at_most_one_ref_attribute(attrs, transform_context);
2967
3005
  const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
2968
3006
  if (preprocess) {
2969
3007
  attrs = preprocess(attrs, transform_context, element);
@@ -2987,9 +3025,11 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
2987
3025
  * the original `JSXAttribute`/`JSXIdentifier` shape, so we accept both.
2988
3026
  *
2989
3027
  * @param {any[]} raw_attrs
3028
+ * @param {TransformContext} [transform_context]
2990
3029
  */
2991
- export function validate_at_most_one_ref_attribute(raw_attrs) {
2992
- let first = null;
3030
+ export function validate_at_most_one_ref_attribute(raw_attrs, transform_context) {
3031
+ /** @type {any[]} */
3032
+ const refs = [];
2993
3033
  for (const attr of raw_attrs) {
2994
3034
  if (!attr) continue;
2995
3035
  const is_ref_attr =
@@ -3002,14 +3042,25 @@ export function validate_at_most_one_ref_attribute(raw_attrs) {
3002
3042
  attr.name.type === 'JSXIdentifier' &&
3003
3043
  attr.name.name === 'ref');
3004
3044
  if (!is_ref_attr) continue;
3005
- if (first) {
3006
- throw create_compile_error(
3007
- attr,
3008
- 'Element has multiple `ref={...}` attributes; an element may have at most one. ' +
3009
- "Use Ripple's `{ref expr}` keyword form to combine multiple refs on one element.",
3010
- );
3045
+ refs.push(attr.name);
3046
+ }
3047
+ if (refs.length < 2) {
3048
+ return;
3049
+ }
3050
+ for (let i = 0; i < refs.length; i++) {
3051
+ const node = refs[i];
3052
+ if (!transform_context?.loose && i === 0) {
3053
+ // in the non-loose mode, only throw on the second duplicate
3054
+ continue;
3011
3055
  }
3012
- first = attr;
3056
+ error(
3057
+ 'Element has multiple `ref={...}` attributes; an element may have at most one. ' +
3058
+ "Use Ripple's `{ref expr}` keyword form to combine multiple refs on one element.",
3059
+ transform_context?.filename ?? null,
3060
+ node,
3061
+ transform_context?.errors,
3062
+ transform_context?.comments,
3063
+ );
3013
3064
  }
3014
3065
  }
3015
3066
 
@@ -3040,18 +3091,34 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
3040
3091
  if (!strategy) return jsx_attrs;
3041
3092
 
3042
3093
  let count = 0;
3094
+ let tsx_form_count = 0;
3043
3095
  for (const attr of jsx_attrs) {
3044
- if (is_jsx_ref_attribute(attr)) count += 1;
3096
+ if (!is_jsx_ref_attribute(attr)) continue;
3097
+ count += 1;
3098
+ if (!attr.metadata?.from_ref_keyword) tsx_form_count += 1;
3045
3099
  }
3046
3100
  if (count <= 1) return jsx_attrs;
3101
+ // Two or more genuine `ref={...}` (TSX-form) attributes are already a
3102
+ // validator-flagged compile error and TypeScript flags them as duplicate
3103
+ // JSX props. Leave them in place so the user gets all three signals
3104
+ // instead of silently composing them into `__mergeRefs(...)`.
3105
+ if (tsx_form_count >= 2) return jsx_attrs;
3047
3106
 
3048
3107
  /** @type {any[]} */
3049
3108
  const ref_exprs = [];
3050
3109
  /** @type {any[]} */
3051
3110
  const result = [];
3111
+ /** @type {any} */
3112
+ let source_attr = null;
3052
3113
  for (const attr of jsx_attrs) {
3053
3114
  if (is_jsx_ref_attribute(attr)) {
3054
3115
  ref_exprs.push(attr.value.expression);
3116
+ // Inherit loc from the (at most one) `ref={expr}`-form attribute so
3117
+ // the kept `ref` keyword in the generated `ref={__mergeRefs(...)}`
3118
+ // retains a source mapping back to its original `ref=` keyword.
3119
+ if (!source_attr && !attr.metadata?.from_ref_keyword) {
3120
+ source_attr = attr;
3121
+ }
3055
3122
  } else {
3056
3123
  result.push(attr);
3057
3124
  }
@@ -3080,23 +3147,23 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
3080
3147
  transform_context.needs_merge_refs = true;
3081
3148
  }
3082
3149
 
3083
- // The merged ref attribute is a synthesis of multiple input refs and
3084
- // has no single source position to map back to, so we omit `loc` for
3085
- // the same reason `to_jsx_attribute` does for `RefAttribute`-derived
3086
- // JSX attributes.
3087
- result.push(
3150
+ // Inherit start/end/loc from the (at most one) `ref={expr}`-form attribute
3151
+ // so segments.js emits a normal source-to-generated mapping for the
3152
+ // merged attribute and its name. Without this the kept `ref` keyword in
3153
+ // `ref={__mergeRefs(...)}` has no source mapping back to the user's `ref=`
3154
+ // keyword.
3155
+ const merged_name = build_jsx_id('ref', source_attr?.name);
3156
+ const merged_attr = build_jsx_attribute(
3157
+ merged_name,
3088
3158
  /** @type {any} */ ({
3089
- type: 'JSXAttribute',
3090
- name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
3091
- value: {
3092
- type: 'JSXExpressionContainer',
3093
- expression: merged_value,
3094
- metadata: { path: [] },
3095
- },
3096
- shorthand: false,
3159
+ type: 'JSXExpressionContainer',
3160
+ expression: merged_value,
3097
3161
  metadata: { path: [] },
3098
3162
  }),
3163
+ false,
3164
+ source_attr,
3099
3165
  );
3166
+ result.push(merged_attr);
3100
3167
 
3101
3168
  return result;
3102
3169
  }
@@ -3150,13 +3217,16 @@ export function to_jsx_attribute(attr, transform_context) {
3150
3217
  // so the source-to-generated mapping is imprecise — but pointing
3151
3218
  // editors at the `{ref expr}` span is still useful for hover/jump,
3152
3219
  // matching how shorthand `{name}` → `name={name}` carries loc.
3220
+ // `from_ref_keyword` lets `merge_duplicate_refs` tell this form apart
3221
+ // from genuine `ref={...}` attributes without inferring it from
3222
+ // whether `name.loc` happens to be present.
3153
3223
  return set_loc(
3154
3224
  /** @type {any} */ ({
3155
3225
  type: 'JSXAttribute',
3156
3226
  name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
3157
3227
  value: to_jsx_expression_container(attr.argument),
3158
3228
  shorthand: false,
3159
- metadata: { path: [] },
3229
+ metadata: { path: [], from_ref_keyword: true },
3160
3230
  }),
3161
3231
  attr,
3162
3232
  );
@@ -3189,13 +3259,7 @@ export function to_jsx_attribute(attr, transform_context) {
3189
3259
  }
3190
3260
  }
3191
3261
 
3192
- const jsx_attribute = /** @type {any} */ ({
3193
- type: 'JSXAttribute',
3194
- name,
3195
- value: value || null,
3196
- shorthand: false,
3197
- metadata: { path: [] },
3198
- });
3262
+ const jsx_attribute = build_jsx_attribute(name, value || null, attr.shorthand === true);
3199
3263
 
3200
3264
  if (value_has_unmappable_jsx_loc(value)) {
3201
3265
  /** @type {any} */ (jsx_attribute.metadata).has_unmappable_value = true;
@@ -3360,16 +3424,20 @@ function build_return_expression(render_nodes) {
3360
3424
 
3361
3425
  /**
3362
3426
  * @param {any} node
3363
- * @param {JsxPlatform} platform
3427
+ * @param {TransformContext} transform_context
3364
3428
  * @param {boolean} [in_jsx_child]
3365
3429
  * @returns {any}
3366
3430
  */
3367
- function tsx_compat_node_to_jsx_expression(node, platform, in_jsx_child = false) {
3431
+ function tsx_compat_node_to_jsx_expression(node, transform_context, in_jsx_child = false) {
3432
+ const platform = transform_context.platform;
3368
3433
  if (!platform.jsx.acceptedTsxKinds.includes(node.kind)) {
3369
3434
  const accepted = platform.jsx.acceptedTsxKinds.map((k) => `<tsx:${k}>`).join(', ');
3370
- throw create_compile_error(
3371
- node,
3435
+ error(
3372
3436
  `${platform.name} TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or one of: ${accepted}.`,
3437
+ transform_context.filename,
3438
+ node,
3439
+ transform_context.errors,
3440
+ transform_context.comments,
3373
3441
  );
3374
3442
  }
3375
3443
 
package/types/index.d.ts CHANGED
@@ -8,13 +8,21 @@ import type { RequireAllOrNone } from '../src/helpers.js';
8
8
  import type {
9
9
  JsxPlatform,
10
10
  JsxPlatformHooks,
11
+ JsxTransformContext,
11
12
  JsxTransformOptions,
12
13
  JsxTransformResult,
13
14
  componentToFunctionDeclaration,
14
15
  createJsxTransform,
15
16
  } from './jsx-platform';
16
17
 
17
- export type { Parse, JsxPlatform, JsxPlatformHooks, JsxTransformOptions, JsxTransformResult };
18
+ export type {
19
+ Parse,
20
+ JsxPlatform,
21
+ JsxPlatformHooks,
22
+ JsxTransformContext,
23
+ JsxTransformOptions,
24
+ JsxTransformResult,
25
+ };
18
26
  export { createJsxTransform, componentToFunctionDeclaration };
19
27
 
20
28
  /**
@@ -1,5 +1,6 @@
1
1
  import type * as AST from 'estree';
2
2
  import type { RawSourceMap } from 'source-map';
3
+ import type { CompileError } from './index';
3
4
 
4
5
  /**
5
6
  * Result returned by a JSX platform transform (React, Preact, Solid).
@@ -16,6 +17,38 @@ export interface JsxTransformResult {
16
17
  css: { code: string; hash: string } | null;
17
18
  }
18
19
 
20
+ /**
21
+ * Shared base for the per-call transform context that the JSX factory passes
22
+ * into every visitor and helper. Platform-specific transforms (e.g. Solid)
23
+ * extend this with their own `needs_*` flags via `hooks.initialState`; helpers
24
+ * defined in `@tsrx/core` only ever rely on these base fields.
25
+ */
26
+ export interface JsxTransformContext {
27
+ platform: JsxPlatform;
28
+ local_statement_component_index: number;
29
+ needs_error_boundary: boolean;
30
+ needs_suspense: boolean;
31
+ needs_merge_refs: boolean;
32
+ helper_state: {
33
+ base_name: string;
34
+ next_id: number;
35
+ helpers: any[];
36
+ statics: any[];
37
+ } | null;
38
+ available_bindings: Map<string, AST.Identifier>;
39
+ lazy_next_id: number;
40
+ current_css_hash: string | null;
41
+ inside_element_child?: boolean;
42
+ /** Source filename for diagnostics; null when the caller did not supply one. */
43
+ filename: string | null;
44
+ /** True when recoverable errors should be collected onto `errors` instead of thrown. */
45
+ loose: boolean;
46
+ /** Collected non-fatal errors. Undefined when `loose` is false. */
47
+ errors: CompileError[] | undefined;
48
+ /** Module-level comments used to honor `@tsrx-ignore` / `@tsrx-expect-error`. */
49
+ comments: AST.CommentWithLocation[] | undefined;
50
+ }
51
+
19
52
  /**
20
53
  * Optional per-call compile options passed to a created JSX transform.
21
54
  */
@@ -26,6 +59,22 @@ export interface JsxTransformOptions {
26
59
  * host pick `preact/compat` vs. another compat entry point.
27
60
  */
28
61
  suspenseSource?: string;
62
+ /**
63
+ * When true, recoverable transform errors are pushed onto `errors` instead
64
+ * of thrown so editor tooling can surface them as diagnostics. Errors that
65
+ * leave the transform in an unrecoverable state are still thrown.
66
+ */
67
+ loose?: boolean;
68
+ /**
69
+ * Collected non-fatal errors. The transform appends to this array when
70
+ * `loose` is true; callers read it after the transform returns.
71
+ */
72
+ errors?: CompileError[];
73
+ /**
74
+ * Module-level comments used to suppress diagnostics via `@tsrx-ignore` /
75
+ * `@tsrx-expect-error` line comments.
76
+ */
77
+ comments?: AST.CommentWithLocation[];
29
78
  }
30
79
 
31
80
  /**
@@ -0,0 +1,12 @@
1
+ export type MergeableRefCallback<T> = (node: T | null) => void | (() => void);
2
+ export type MergeableRefObject<T> = { current: T | null };
3
+ export type MergeableVueRef<T> = { value: T | null };
4
+
5
+ export type MergeableRef<T> =
6
+ | MergeableRefCallback<T>
7
+ | MergeableRefObject<T>
8
+ | MergeableVueRef<T>
9
+ | null
10
+ | undefined;
11
+
12
+ export function mergeRefs<T = any>(...refs: Array<MergeableRef<T>>): (node: T | null) => () => void;