@tsrx/core 0.0.27 → 0.0.28

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.27",
6
+ "version": "0.0.28",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -27,10 +27,11 @@
27
27
  "./types/acorn": {
28
28
  "types": "./types/acorn.d.ts"
29
29
  },
30
- "./runtime/merge-refs": {
31
- "types": "./types/runtime/merge-refs.d.ts",
32
- "default": "./src/runtime/merge-refs.js"
30
+ "./runtime/ref": {
31
+ "types": "./types/runtime/ref.d.ts",
32
+ "default": "./src/runtime/ref.js"
33
33
  },
34
+ "./runtime/*": "./src/runtime/*.js",
34
35
  "./test-harness/source-mappings": "./tests/shared/source-mappings.js",
35
36
  "./test-harness/compile": "./tests/shared/compile.js"
36
37
  },
package/src/index.js CHANGED
@@ -141,8 +141,14 @@ export { escape, escape_script as escapeScript } from './utils/escaping.js';
141
141
 
142
142
  // Transform
143
143
  export {
144
+ add_jsx_setup_declaration as addJsxSetupDeclaration,
144
145
  createJsxTransform,
146
+ CREATE_REF_PROP_INTERNAL_NAME,
147
+ extract_jsx_setup_declarations as extractJsxSetupDeclarations,
148
+ is_ref_prop_expression as isRefPropExpression,
149
+ MERGE_REFS_INTERNAL_NAME,
145
150
  merge_duplicate_refs as mergeDuplicateRefs,
151
+ NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
146
152
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
147
153
  to_jsx_attribute as toJsxAttribute,
148
154
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
package/src/plugin.js CHANGED
@@ -1320,7 +1320,7 @@ export function TSRXPlugin(config) {
1320
1320
  if (!nextChars) {
1321
1321
  this.raise(
1322
1322
  ref.start,
1323
- '"component" is a Ripple keyword and cannot be used as an identifier',
1323
+ '"component" is a TSRX keyword and cannot be used as an identifier',
1324
1324
  );
1325
1325
  }
1326
1326
  }
@@ -1345,6 +1345,21 @@ export function TSRXPlugin(config) {
1345
1345
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1346
1346
  this.next();
1347
1347
 
1348
+ if (this.type === tt.name && this.value === 'ref') {
1349
+ const ref_node = /** @type {AST.RefExpression} */ (this.startNode());
1350
+ this.next();
1351
+ if (this.type === tt.braceR) {
1352
+ this.raise(
1353
+ this.start,
1354
+ '"ref" is a TSRX keyword and must be used in the form {ref item}',
1355
+ );
1356
+ }
1357
+ ref_node.argument = this.parseMaybeAssign();
1358
+ node.expression = /** @type {any} */ (this.finishNode(ref_node, 'RefExpression'));
1359
+ this.expect(tt.braceR);
1360
+ return this.finishNode(node, 'JSXExpressionContainer');
1361
+ }
1362
+
1348
1363
  if (this.type === tt.name && this.value === 'html') {
1349
1364
  node.html = true;
1350
1365
  this.next();
@@ -0,0 +1,57 @@
1
+ /** @type {typeof Object.getOwnPropertyDescriptor} */
2
+ export var get_descriptor = Object.getOwnPropertyDescriptor;
3
+ /** @type {typeof Object.getOwnPropertyDescriptors} */
4
+ export var get_descriptors = Object.getOwnPropertyDescriptors;
5
+ /** @type {typeof Array.from} */
6
+ export var array_from = Array.from;
7
+ /** @type {typeof Array.isArray} */
8
+ export var is_array = Array.isArray;
9
+ /** @type {typeof Object.defineProperty} */
10
+ export var define_property = Object.defineProperty;
11
+ /** @type {typeof Object.getPrototypeOf} */
12
+ export var get_prototype_of = Object.getPrototypeOf;
13
+ /** @type {typeof Object.values} */
14
+ export var object_values = Object.values;
15
+ /** @type {typeof Object.entries} */
16
+ export var object_entries = Object.entries;
17
+ /** @type {typeof Object.keys} */
18
+ export var object_keys = Object.keys;
19
+ /** @type {typeof Object.getOwnPropertySymbols} */
20
+ export var get_own_property_symbols = Object.getOwnPropertySymbols;
21
+ /** @type {typeof structuredClone} */
22
+ export var structured_clone = structuredClone;
23
+ /** @type {typeof Object.prototype} */
24
+ export var object_prototype = Object.prototype;
25
+ /** @type {typeof Array.prototype} */
26
+ export var array_prototype = Array.prototype;
27
+ /** @type {typeof Object.prototype.hasOwnProperty} */
28
+ export var has_own_property = object_prototype.hasOwnProperty;
29
+
30
+ /**
31
+ * @param {object} value
32
+ * @param {PropertyKey} key
33
+ * @returns {boolean}
34
+ */
35
+ export function has_prototype_accessor(value, key) {
36
+ var proto = get_prototype_of(value);
37
+ while (proto != null) {
38
+ var descriptor = get_descriptor(proto, key);
39
+ if (descriptor !== undefined) {
40
+ return typeof descriptor.get === 'function' || typeof descriptor.set === 'function';
41
+ }
42
+ proto = get_prototype_of(proto);
43
+ }
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Slice helper for arrays and array-like values.
49
+ * @param {ArrayLike<any>} array_like
50
+ * @param {...number} args
51
+ * @returns {any[]}
52
+ */
53
+ export function array_slice(array_like, ...args) {
54
+ return is_array(array_like)
55
+ ? array_like.slice(...args)
56
+ : array_prototype.slice.call(array_like, ...args);
57
+ }
@@ -0,0 +1,250 @@
1
+ import {
2
+ has_own_property,
3
+ get_descriptor,
4
+ has_prototype_accessor,
5
+ } from '@tsrx/core/runtime/language-helpers';
6
+
7
+ const REF_VALUE = Symbol();
8
+
9
+ /**
10
+ * Merge multiple refs (function refs and ref objects) into a single
11
+ * callback ref. Used by React, Preact, and Vue targets when an element has
12
+ * more than one `ref` attribute.
13
+ * This is a public method and also used by the compiler to unite any refs with
14
+ * any of the supported syntaxes. It does not process spreads, that is delegated to
15
+ * `normalize_spread_props`.
16
+ *
17
+ * @param {...((node: any) => void | (() => void)) | { current: any } | { value: any } | null | undefined} refs
18
+ * @returns {(node: any) => (() => void)}
19
+ */
20
+ export function mergeRefs(...refs) {
21
+ return (node) => {
22
+ /** @type {Array<() => void>} */
23
+ const cleanups = [];
24
+ for (const ref of refs) {
25
+ if (ref == null) continue;
26
+ if (typeof ref === 'function') {
27
+ const result = ref(node);
28
+ if (typeof result === 'function') {
29
+ cleanups.push(result);
30
+ } else {
31
+ cleanups.push(() => ref(null));
32
+ }
33
+ } else if (is_ref_object(ref, 'current')) {
34
+ /** @type {{ current: any }} */ (ref).current = node;
35
+ cleanups.push(() => {
36
+ /** @type {{ current: any }} */ (ref).current = null;
37
+ });
38
+ } else if (is_ref_object(ref, 'value')) {
39
+ /** @type {{ value: any }} */ (ref).value = node;
40
+ cleanups.push(() => {
41
+ /** @type {{ value: any }} */ (ref).value = null;
42
+ });
43
+ }
44
+ }
45
+ return () => {
46
+ for (const cleanup of cleanups) cleanup();
47
+ };
48
+ };
49
+ }
50
+
51
+ export { is_ref_prop as isRefProp };
52
+
53
+ /**
54
+ * @param {unknown} value
55
+ * @returns {boolean}
56
+ */
57
+ function is_ref_prop(value) {
58
+ return typeof value === 'function' && REF_VALUE in value;
59
+ }
60
+
61
+ /**
62
+ * @param {any} ref_value
63
+ * @param {any} node
64
+ * @param {(value: any) => void} [set_ref_value]
65
+ * @returns {void | (() => void)}
66
+ */
67
+ export function apply_ref_value(ref_value, node, set_ref_value) {
68
+ if (typeof ref_value === 'function') {
69
+ return ref_value(node);
70
+ }
71
+
72
+ if (ref_value && typeof ref_value === 'object') {
73
+ if (is_ref_object(ref_value, 'current')) {
74
+ ref_value.current = node;
75
+ return () => {
76
+ ref_value.current = null;
77
+ };
78
+ }
79
+
80
+ if (is_ref_object(ref_value, 'value')) {
81
+ ref_value.value = node;
82
+ return () => {
83
+ ref_value.value = null;
84
+ };
85
+ }
86
+ }
87
+
88
+ if (set_ref_value !== undefined) {
89
+ set_ref_value(node);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {() => any} get_ref_value
95
+ * @param {(value: any) => void} [set_ref_value]
96
+ * @returns {(node: any) => void | (() => void)}
97
+ */
98
+ export function create_ref_prop(get_ref_value, set_ref_value) {
99
+ /**
100
+ * @param {any} node
101
+ * @returns {void | (() => void)}
102
+ */
103
+ function ref_prop_callback(node) {
104
+ const ref_value = get_ref_value();
105
+ const cleanup = apply_ref_value(ref_value, node, set_ref_value);
106
+ if (typeof cleanup === 'function' || node === null) {
107
+ return cleanup;
108
+ }
109
+ return () => {
110
+ apply_ref_value(ref_value, null, set_ref_value);
111
+ };
112
+ }
113
+
114
+ Object.defineProperty(ref_prop_callback, REF_VALUE, {
115
+ value: 'ref_value',
116
+ enumerable: false,
117
+ });
118
+
119
+ return ref_prop_callback;
120
+ }
121
+
122
+ /**
123
+ * @param {...any} refs
124
+ * @returns {any}
125
+ */
126
+ export function merge_ref_props(...refs) {
127
+ const filtered = refs.filter((ref) => ref != null);
128
+
129
+ if (filtered.length === 0) {
130
+ return undefined;
131
+ }
132
+
133
+ if (filtered.length === 1) {
134
+ return filtered[0];
135
+ }
136
+
137
+ /**
138
+ * @param {any} node
139
+ * @returns {void | (() => void)}
140
+ */
141
+ function merged_ref_prop(node) {
142
+ /** @type {Array<() => void>} */
143
+ const cleanups = [];
144
+
145
+ for (const ref of filtered) {
146
+ const cleanup = apply_ref_value(ref, node);
147
+ if (typeof cleanup === 'function') {
148
+ cleanups.push(cleanup);
149
+ } else if (typeof ref === 'function' && node !== null) {
150
+ cleanups.push(() => ref(null));
151
+ }
152
+ }
153
+
154
+ return () => {
155
+ for (const cleanup of cleanups) {
156
+ cleanup();
157
+ }
158
+ };
159
+ }
160
+
161
+ return merged_ref_prop;
162
+ }
163
+
164
+ /**
165
+ * @param {Record<string | symbol, any> | null | undefined} props
166
+ * @param {...any} outer_refs
167
+ * @returns {Record<string | symbol, any> | null | undefined}
168
+ */
169
+ export function normalize_spread_props(props, ...outer_refs) {
170
+ if (props == null) {
171
+ return props;
172
+ }
173
+
174
+ /** @type {any[]} */
175
+ const refs = [];
176
+ /** @type {Record<string | symbol, any>} */
177
+ let next = {};
178
+ let changed = false;
179
+ let existing_ref;
180
+
181
+ for (const key of Reflect.ownKeys(props)) {
182
+ const descriptor = get_descriptor(props, key);
183
+ if (!descriptor?.enumerable) {
184
+ continue;
185
+ }
186
+
187
+ const value = /** @type {any} */ (props)[key];
188
+
189
+ if (key === 'ref') {
190
+ if (is_ref_prop(value)) {
191
+ refs.push(value);
192
+ changed = true;
193
+ } else {
194
+ existing_ref = value;
195
+ }
196
+ continue;
197
+ }
198
+
199
+ if (is_ref_prop(value)) {
200
+ refs.push(value);
201
+ changed = true;
202
+ continue;
203
+ }
204
+
205
+ next[key] = value;
206
+ }
207
+
208
+ if (!changed && outer_refs.length === 0) {
209
+ return props;
210
+ }
211
+
212
+ const merged_ref = merge_ref_props(existing_ref, ...refs, ...outer_refs);
213
+ if (merged_ref !== undefined) {
214
+ next.ref = merged_ref;
215
+ }
216
+
217
+ return next;
218
+ }
219
+
220
+ /**
221
+ * @param {object} value
222
+ * @param {'current' | 'value'} key
223
+ * @returns {boolean}
224
+ */
225
+ function is_ref_object(value, key) {
226
+ if (is_dom_node(value)) {
227
+ return false;
228
+ }
229
+ if (key === 'value' && '__v_isRef' in value) {
230
+ return true;
231
+ }
232
+ if (has_own_property.call(value, key)) {
233
+ return true;
234
+ }
235
+ return key === 'value' && has_prototype_accessor(value, 'value');
236
+ }
237
+
238
+ /**
239
+ * @param {object} value
240
+ * @returns {boolean}
241
+ */
242
+ function is_dom_node(value) {
243
+ return (
244
+ (typeof Node !== 'undefined' && value instanceof Node) ||
245
+ ('nodeType' in value &&
246
+ typeof (/** @type {{ nodeType?: unknown }} */ (value).nodeType) === 'number' &&
247
+ 'nodeName' in value &&
248
+ typeof (/** @type {{ nodeName?: unknown }} */ (value).nodeName) === 'string')
249
+ );
250
+ }
@@ -143,7 +143,7 @@ export function identifier_to_jsx_name(id) {
143
143
  /** @type {any} */ ({
144
144
  type: 'JSXIdentifier',
145
145
  name: id.name,
146
- metadata: { path: [], is_component: /^[A-Z]/.test(id.name) },
146
+ metadata: { ...(id.metadata || {}), path: [], is_component: /^[A-Z]/.test(id.name) },
147
147
  }),
148
148
  id,
149
149
  );
@@ -376,26 +376,26 @@ export function to_text_expression(expression, source_node = expression) {
376
376
  }
377
377
 
378
378
  /**
379
- * Deep-clone an AST subtree. `loc` / `start` / `end` are shallow-shared by
380
- * reference rather than recursed into — `loc` objects can contain back-refs
381
- * to sub-objects that would blow the stack with a naive deep clone, and
382
- * every other traversal in the targets treats these positional keys as
383
- * shared.
379
+ * Deep-clone an AST subtree.
384
380
  *
385
381
  * @param {any} node
382
+ * @param {boolean} with_locations
386
383
  * @returns {any}
387
384
  */
388
- export function clone_expression_node(node) {
385
+ export function clone_expression_node(node, with_locations = true) {
389
386
  if (!node || typeof node !== 'object') return node;
390
- if (Array.isArray(node)) return node.map(clone_expression_node);
391
- const clone = { ...node };
392
- for (const key of Object.keys(clone)) {
393
- if (key === 'loc' || key === 'start' || key === 'end') continue;
387
+ if (Array.isArray(node)) return node.map((child) => clone_expression_node(child, with_locations));
388
+ const clone = /** @type {Record<string, any>} */ ({});
389
+
390
+ for (const key of Object.keys(node)) {
391
+ if (!with_locations && (key === 'loc' || key === 'start' || key === 'end')) {
392
+ continue;
393
+ }
394
394
  if (key === 'metadata') {
395
- clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
395
+ clone.metadata = node.metadata ? { ...node.metadata } : { path: [] };
396
396
  continue;
397
397
  }
398
- clone[key] = clone_expression_node(clone[key]);
398
+ clone[key] = clone_expression_node(node[key], with_locations);
399
399
  }
400
400
  return clone;
401
401
  }
@@ -169,6 +169,8 @@ export function createJsxTransform(platform) {
169
169
  needs_error_boundary: false,
170
170
  needs_suspense: false,
171
171
  needs_merge_refs: false,
172
+ needs_ref_prop: false,
173
+ needs_normalize_spread_props: false,
172
174
  needs_fragment: false,
173
175
  module_scoped_hook_components:
174
176
  options?.moduleScopedHookComponents ?? !!platform.hooks?.moduleScopedHookComponents,
@@ -382,13 +384,20 @@ export function createJsxTransform(platform) {
382
384
 
383
385
  Tsx(node, { next, path }) {
384
386
  const inner = /** @type {any} */ (next() ?? node);
385
- return /** @type {any} */ (tsx_node_to_jsx_expression(inner, in_jsx_child_context(path)));
387
+ const in_jsx_child = in_jsx_child_context(path);
388
+ return /** @type {any} */ (
389
+ wrap_jsx_setup_declarations(tsx_node_to_jsx_expression(inner, in_jsx_child), in_jsx_child)
390
+ );
386
391
  },
387
392
 
388
393
  TsxCompat(node, { next, path, state }) {
389
394
  const inner = /** @type {any} */ (next() ?? node);
395
+ const in_jsx_child = in_jsx_child_context(path);
390
396
  return /** @type {any} */ (
391
- tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child_context(path))
397
+ wrap_jsx_setup_declarations(
398
+ tsx_compat_node_to_jsx_expression(inner, state, in_jsx_child),
399
+ in_jsx_child,
400
+ )
392
401
  );
393
402
  },
394
403
 
@@ -432,6 +441,27 @@ export function createJsxTransform(platform) {
432
441
  FunctionDeclaration: ensure_function_metadata,
433
442
  FunctionExpression: ensure_function_metadata,
434
443
  ArrowFunctionExpression: ensure_function_metadata,
444
+
445
+ RefExpression(node) {
446
+ return create_ref_prop_call(node, transform_context);
447
+ },
448
+
449
+ JSXOpeningElement(node, { next }) {
450
+ const visited = next() || node;
451
+ const is_component = is_component_like_jsx_name(visited.name);
452
+ const attrs = normalize_named_ref_attributes(
453
+ visited.attributes || [],
454
+ !is_component,
455
+ transform_context,
456
+ );
457
+ return {
458
+ ...visited,
459
+ attributes: merge_duplicate_refs(
460
+ normalize_host_ref_spreads(attrs, !is_component, transform_context),
461
+ transform_context,
462
+ ),
463
+ };
464
+ },
435
465
  });
436
466
 
437
467
  const expanded = expand_component_helpers(/** @type {AST.Program} */ (transformed));
@@ -906,6 +936,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
906
936
 
907
937
  if (is_jsx_child(child)) {
908
938
  const jsx = to_jsx_child(child, transform_context);
939
+ statements.push(...extract_jsx_setup_declarations(jsx));
909
940
  if (interleaved && is_capturable_jsx_child(jsx)) {
910
941
  const { declaration, reference } = captureJsxChild(jsx, capture_index++);
911
942
  statements.push(declaration);
@@ -1576,9 +1607,7 @@ function create_component_return_statement(
1576
1607
  map_render_node_locations = true,
1577
1608
  ) {
1578
1609
  const cloned = render_nodes.map((node) =>
1579
- map_render_node_locations
1580
- ? clone_expression_node(node)
1581
- : clone_expression_node_without_locations(node),
1610
+ map_render_node_locations ? clone_expression_node(node) : clone_expression_node(node, false),
1582
1611
  );
1583
1612
 
1584
1613
  return set_loc(b.return(build_return_expression(cloned) || create_null_literal()), source_node);
@@ -1645,7 +1674,7 @@ function build_tail_helper(continuation_body, source_node, transform_context) {
1645
1674
  * @returns {any}
1646
1675
  */
1647
1676
  function clone_tail_invocation(tail_helper) {
1648
- return clone_expression_node_without_locations(tail_helper.component_element);
1677
+ return clone_expression_node(tail_helper.component_element, false);
1649
1678
  }
1650
1679
 
1651
1680
  /**
@@ -2553,7 +2582,7 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
2553
2582
  * @returns {any}
2554
2583
  */
2555
2584
  function combine_render_return_argument(render_nodes, return_argument) {
2556
- const combined = render_nodes.map((node) => clone_expression_node_without_locations(node));
2585
+ const combined = render_nodes.map((node) => clone_expression_node(node, false));
2557
2586
 
2558
2587
  if (return_argument != null && !is_null_literal(return_argument)) {
2559
2588
  combined.push(return_argument_to_render_node(return_argument));
@@ -2586,30 +2615,6 @@ function is_null_literal(node) {
2586
2615
  return node?.type === 'Literal' && node.value == null;
2587
2616
  }
2588
2617
 
2589
- /**
2590
- * @param {any} node
2591
- * @returns {any}
2592
- */
2593
- function clone_expression_node_without_locations(node) {
2594
- if (!node || typeof node !== 'object') return node;
2595
- if (Array.isArray(node)) return node.map(clone_expression_node_without_locations);
2596
-
2597
- const clone = { ...node };
2598
- delete clone.loc;
2599
- delete clone.start;
2600
- delete clone.end;
2601
-
2602
- for (const key of Object.keys(clone)) {
2603
- if (key === 'metadata') {
2604
- clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
2605
- continue;
2606
- }
2607
- clone[key] = clone_expression_node_without_locations(clone[key]);
2608
- }
2609
-
2610
- return clone;
2611
- }
2612
-
2613
2618
  const TEMPLATE_FRAGMENT_ERROR =
2614
2619
  'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
2615
2620
 
@@ -4201,30 +4206,79 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
4201
4206
  });
4202
4207
  }
4203
4208
 
4204
- if (transform_context.needs_merge_refs && platform.imports.mergeRefs) {
4205
- const merge_refs_source = platform.imports.mergeRefs;
4209
+ const merge_refs_source =
4210
+ transform_context.needs_merge_refs && platform.imports.mergeRefs
4211
+ ? platform.imports.mergeRefs
4212
+ : null;
4213
+ const ref_prop_source =
4214
+ transform_context.needs_ref_prop && platform.imports.refProp ? platform.imports.refProp : null;
4215
+ const normalize_spread_props_source =
4216
+ transform_context.needs_normalize_spread_props && platform.imports.refProp
4217
+ ? platform.imports.refProp
4218
+ : null;
4219
+
4220
+ /** @type {Map<string, any[]>} */
4221
+ const ref_imports = new Map();
4222
+
4223
+ if (merge_refs_source !== null) {
4224
+ add_ref_import_specifier(ref_imports, merge_refs_source, {
4225
+ type: 'ImportSpecifier',
4226
+ imported: {
4227
+ type: 'Identifier',
4228
+ name: 'mergeRefs',
4229
+ metadata: { path: [] },
4230
+ },
4231
+ local: {
4232
+ type: 'Identifier',
4233
+ name: MERGE_REFS_INTERNAL_NAME,
4234
+ metadata: { path: [] },
4235
+ },
4236
+ metadata: { path: [] },
4237
+ });
4238
+ }
4239
+
4240
+ if (ref_prop_source !== null) {
4241
+ add_ref_import_specifier(ref_imports, ref_prop_source, {
4242
+ type: 'ImportSpecifier',
4243
+ imported: {
4244
+ type: 'Identifier',
4245
+ name: 'create_ref_prop',
4246
+ metadata: { path: [] },
4247
+ },
4248
+ local: {
4249
+ type: 'Identifier',
4250
+ name: CREATE_REF_PROP_INTERNAL_NAME,
4251
+ metadata: { path: [] },
4252
+ },
4253
+ metadata: { path: [] },
4254
+ });
4255
+ }
4256
+
4257
+ if (normalize_spread_props_source !== null) {
4258
+ add_ref_import_specifier(ref_imports, normalize_spread_props_source, {
4259
+ type: 'ImportSpecifier',
4260
+ imported: {
4261
+ type: 'Identifier',
4262
+ name: 'normalize_spread_props',
4263
+ metadata: { path: [] },
4264
+ },
4265
+ local: {
4266
+ type: 'Identifier',
4267
+ name: NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
4268
+ metadata: { path: [] },
4269
+ },
4270
+ metadata: { path: [] },
4271
+ });
4272
+ }
4273
+
4274
+ for (const [source, ref_specifiers] of ref_imports) {
4206
4275
  imports.push({
4207
4276
  type: 'ImportDeclaration',
4208
- specifiers: [
4209
- {
4210
- type: 'ImportSpecifier',
4211
- imported: {
4212
- type: 'Identifier',
4213
- name: 'mergeRefs',
4214
- metadata: { path: [] },
4215
- },
4216
- local: {
4217
- type: 'Identifier',
4218
- name: MERGE_REFS_LOCAL_NAME,
4219
- metadata: { path: [] },
4220
- },
4221
- metadata: { path: [] },
4222
- },
4223
- ],
4277
+ specifiers: ref_specifiers,
4224
4278
  source: {
4225
4279
  type: 'Literal',
4226
- value: merge_refs_source,
4227
- raw: `'${merge_refs_source}'`,
4280
+ value: source,
4281
+ raw: `'${source}'`,
4228
4282
  },
4229
4283
  metadata: { path: [] },
4230
4284
  });
@@ -4235,6 +4289,20 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
4235
4289
  }
4236
4290
  }
4237
4291
 
4292
+ /**
4293
+ * @param {Map<string, any[]>} imports
4294
+ * @param {string} source
4295
+ * @param {any} specifier
4296
+ */
4297
+ function add_ref_import_specifier(imports, source, specifier) {
4298
+ const specifiers = imports.get(source);
4299
+ if (specifiers) {
4300
+ specifiers.push(specifier);
4301
+ } else {
4302
+ imports.set(source, [specifier]);
4303
+ }
4304
+ }
4305
+
4238
4306
  /**
4239
4307
  * @param {any} node
4240
4308
  * @param {TransformContext} transform_context
@@ -4421,6 +4489,8 @@ function to_jsx_expression_container(expression, source_node = expression) {
4421
4489
  */
4422
4490
  function transform_element_attributes_dispatch(attrs, transform_context, element) {
4423
4491
  validate_at_most_one_ref_attribute(attrs, transform_context);
4492
+ const is_component = is_component_like_element(element);
4493
+ attrs = normalize_named_ref_attributes(attrs, !is_component, transform_context);
4424
4494
  const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
4425
4495
  if (preprocess) {
4426
4496
  attrs = preprocess(attrs, transform_context, element);
@@ -4429,7 +4499,238 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
4429
4499
  const result = hook
4430
4500
  ? hook(attrs, transform_context, element)
4431
4501
  : attrs.map((/** @type {any} */ a) => to_jsx_attribute(a, transform_context));
4432
- return merge_duplicate_refs(result, transform_context);
4502
+ return merge_duplicate_refs(
4503
+ normalize_host_ref_spreads(result, !is_component, transform_context),
4504
+ transform_context,
4505
+ );
4506
+ }
4507
+
4508
+ /**
4509
+ * @param {any} element
4510
+ * @returns {boolean}
4511
+ */
4512
+ function is_component_like_element(element) {
4513
+ const id = element?.id;
4514
+ if (!id) return false;
4515
+ if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
4516
+ if (id.type === 'JSXIdentifier') return /^[A-Z]/.test(id.name);
4517
+ if (id.type === 'MemberExpression') return true;
4518
+ if (id.type === 'JSXMemberExpression') return true;
4519
+ return false;
4520
+ }
4521
+
4522
+ /**
4523
+ * @param {any} name
4524
+ * @returns {boolean}
4525
+ */
4526
+ function is_component_like_jsx_name(name) {
4527
+ if (!name) return false;
4528
+ if (name.type === 'JSXIdentifier') return /^[A-Z]/.test(name.name);
4529
+ if (name.type === 'JSXMemberExpression') return true;
4530
+ return false;
4531
+ }
4532
+
4533
+ /**
4534
+ * @param {any[]} attrs
4535
+ * @param {boolean} is_host
4536
+ * @param {TransformContext} transform_context
4537
+ * @returns {any[]}
4538
+ */
4539
+ function normalize_named_ref_attributes(attrs, is_host, transform_context) {
4540
+ if (!is_host) return attrs;
4541
+
4542
+ return attrs.map((attr) => {
4543
+ if (!is_named_ref_attribute(attr)) {
4544
+ return attr;
4545
+ }
4546
+
4547
+ if (transform_context.typeOnly) {
4548
+ return mark_type_only_named_ref_attribute(attr);
4549
+ }
4550
+
4551
+ return {
4552
+ ...attr,
4553
+ metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
4554
+ name:
4555
+ attr.name?.type === 'JSXIdentifier'
4556
+ ? { ...attr.name, name: 'ref' }
4557
+ : { type: 'Identifier', name: 'ref', metadata: { path: [] } },
4558
+ };
4559
+ });
4560
+ }
4561
+
4562
+ /**
4563
+ * @param {any} attr
4564
+ * @returns {any}
4565
+ */
4566
+ function mark_type_only_named_ref_attribute(attr) {
4567
+ return {
4568
+ ...attr,
4569
+ name: attr.name
4570
+ ? {
4571
+ ...attr.name,
4572
+ metadata: { ...(attr.name.metadata || {}), disable_verification: true },
4573
+ }
4574
+ : attr.name,
4575
+ };
4576
+ }
4577
+
4578
+ /**
4579
+ * @param {any[]} attrs
4580
+ * @param {boolean} is_host
4581
+ * @param {TransformContext} transform_context
4582
+ * @returns {any[]}
4583
+ */
4584
+ function normalize_host_ref_spreads(attrs, is_host, transform_context) {
4585
+ if (!is_host) return attrs;
4586
+
4587
+ const needs_explicit_spread_ref =
4588
+ transform_context.platform.jsx?.hostSpreadRefStrategy === 'explicit-ref-attr';
4589
+ const ref_exprs = attrs
4590
+ .filter((attr) => is_jsx_ref_attribute(attr))
4591
+ .map((attr) => attr.value.expression);
4592
+ const needs_synthetic_spread_ref = needs_explicit_spread_ref || ref_exprs.length > 0;
4593
+
4594
+ return attrs.flatMap((attr) => {
4595
+ if (!attr || attr.type !== 'JSXSpreadAttribute') {
4596
+ return [attr];
4597
+ }
4598
+
4599
+ transform_context.needs_normalize_spread_props = true;
4600
+ const normalized = b.call(NORMALIZE_SPREAD_PROPS_INTERNAL_NAME, attr.argument);
4601
+
4602
+ if (needs_synthetic_spread_ref) {
4603
+ const normalized_id = create_generated_identifier(
4604
+ create_spread_props_name(transform_context),
4605
+ );
4606
+ const spread = {
4607
+ ...attr,
4608
+ argument: clone_identifier(normalized_id),
4609
+ };
4610
+ const ref_attr = b.jsx_attribute(
4611
+ b.jsx_id('ref'),
4612
+ to_jsx_expression_container(b.member(clone_identifier(normalized_id), 'ref'), attr),
4613
+ false,
4614
+ attr,
4615
+ );
4616
+ ref_attr.metadata = { ...(ref_attr.metadata || {}) };
4617
+ /** @type {any} */ (ref_attr.metadata).from_ref_keyword = true;
4618
+ add_jsx_setup_declaration(spread, b.let(clone_identifier(normalized_id), normalized));
4619
+
4620
+ return [spread, ref_attr];
4621
+ }
4622
+
4623
+ return [
4624
+ {
4625
+ ...attr,
4626
+ argument: normalized,
4627
+ },
4628
+ ];
4629
+ });
4630
+ }
4631
+
4632
+ /**
4633
+ * @param {TransformContext} transform_context
4634
+ * @returns {string}
4635
+ */
4636
+ function create_spread_props_name(transform_context) {
4637
+ if (transform_context.helper_state) {
4638
+ return create_helper_name(transform_context.helper_state, 'spread_props');
4639
+ }
4640
+
4641
+ transform_context.local_statement_component_index += 1;
4642
+ return `_tsrx_spread_props_${transform_context.local_statement_component_index}`;
4643
+ }
4644
+
4645
+ /**
4646
+ * @param {any} node
4647
+ * @param {any} declaration
4648
+ */
4649
+ export function add_jsx_setup_declaration(node, declaration) {
4650
+ node.metadata ??= { path: [] };
4651
+ (node.metadata.generated_setup_declarations ??= []).push(declaration);
4652
+ }
4653
+
4654
+ /**
4655
+ * @param {any} node
4656
+ * @param {Set<any>} [seen]
4657
+ * @returns {any[]}
4658
+ */
4659
+ export function extract_jsx_setup_declarations(node, seen = new Set()) {
4660
+ if (node == null || typeof node !== 'object' || seen.has(node)) {
4661
+ return [];
4662
+ }
4663
+ seen.add(node);
4664
+
4665
+ const declarations = node.metadata?.generated_setup_declarations ?? [];
4666
+ if (node.metadata?.generated_setup_declarations) {
4667
+ delete node.metadata.generated_setup_declarations;
4668
+ }
4669
+
4670
+ for (const key of Object.keys(node)) {
4671
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
4672
+ continue;
4673
+ }
4674
+ declarations.push(...extract_jsx_setup_declarations(node[key], seen));
4675
+ }
4676
+
4677
+ return declarations;
4678
+ }
4679
+
4680
+ /**
4681
+ * @param {any} expression
4682
+ * @param {boolean} in_jsx_child
4683
+ * @returns {any}
4684
+ */
4685
+ function wrap_jsx_setup_declarations(expression, in_jsx_child) {
4686
+ const declarations = extract_jsx_setup_declarations(expression);
4687
+ if (declarations.length === 0) {
4688
+ return expression;
4689
+ }
4690
+
4691
+ const return_expression =
4692
+ expression?.type === 'JSXExpressionContainer' ? expression.expression : expression;
4693
+ const call = b.call(
4694
+ b.arrow(
4695
+ [],
4696
+ b.block([...declarations, b.return(return_expression)], expression),
4697
+ false,
4698
+ expression,
4699
+ ),
4700
+ );
4701
+
4702
+ return in_jsx_child ? to_jsx_expression_container(call, expression) : call;
4703
+ }
4704
+
4705
+ /**
4706
+ * @param {any} attr
4707
+ * @returns {boolean}
4708
+ */
4709
+ function is_named_ref_attribute(attr) {
4710
+ return !!(
4711
+ attr &&
4712
+ (attr.type === 'Attribute' || attr.type === 'JSXAttribute') &&
4713
+ attr.name &&
4714
+ ((attr.name.type === 'Identifier' && attr.name.name !== 'ref') ||
4715
+ (attr.name.type === 'JSXIdentifier' && attr.name.name !== 'ref')) &&
4716
+ (attr.value?.type === 'RefExpression' ||
4717
+ is_ref_prop_expression(attr.value) ||
4718
+ (attr.value?.type === 'JSXExpressionContainer' &&
4719
+ is_ref_prop_expression(attr.value.expression)))
4720
+ );
4721
+ }
4722
+
4723
+ /**
4724
+ * @param {any} expression
4725
+ * @returns {boolean}
4726
+ */
4727
+ export function is_ref_prop_expression(expression) {
4728
+ return (
4729
+ expression?.type === 'RefExpression' ||
4730
+ (expression?.type === 'CallExpression' &&
4731
+ expression.callee?.type === 'Identifier' &&
4732
+ expression.callee.name === CREATE_REF_PROP_INTERNAL_NAME)
4733
+ );
4433
4734
  }
4434
4735
 
4435
4736
  /**
@@ -4549,7 +4850,7 @@ export function merge_duplicate_refs(jsx_attrs, transform_context) {
4549
4850
  type: 'CallExpression',
4550
4851
  callee: {
4551
4852
  type: 'Identifier',
4552
- name: MERGE_REFS_LOCAL_NAME,
4853
+ name: MERGE_REFS_INTERNAL_NAME,
4553
4854
  metadata: { path: [] },
4554
4855
  },
4555
4856
  arguments: ref_exprs,
@@ -4610,7 +4911,9 @@ function is_jsx_ref_attribute(attr) {
4610
4911
  * double-underscore matches the convention for compiler-generated
4611
4912
  * identifiers and avoids shadowing user-declared `mergeRefs` symbols.
4612
4913
  */
4613
- const MERGE_REFS_LOCAL_NAME = '__mergeRefs';
4914
+ export const MERGE_REFS_INTERNAL_NAME = '__mergeRefs';
4915
+ export const CREATE_REF_PROP_INTERNAL_NAME = '__create_ref_prop';
4916
+ export const NORMALIZE_SPREAD_PROPS_INTERNAL_NAME = '__normalize_spread_props';
4614
4917
 
4615
4918
  /**
4616
4919
  * @param {any} attr
@@ -4619,7 +4922,31 @@ const MERGE_REFS_LOCAL_NAME = '__mergeRefs';
4619
4922
  */
4620
4923
  export function to_jsx_attribute(attr, transform_context) {
4621
4924
  if (!attr) return attr;
4622
- if (attr.type === 'JSXAttribute' || attr.type === 'JSXSpreadAttribute') {
4925
+ if (attr.type === 'JSXAttribute') {
4926
+ if (
4927
+ attr.value?.type === 'JSXExpressionContainer' &&
4928
+ attr.value.expression?.type === 'RefExpression'
4929
+ ) {
4930
+ return {
4931
+ ...attr,
4932
+ value: to_jsx_expression_container(
4933
+ create_ref_prop_call(attr.value.expression, transform_context),
4934
+ ),
4935
+ metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
4936
+ };
4937
+ }
4938
+ if (
4939
+ attr.value?.type === 'JSXExpressionContainer' &&
4940
+ is_ref_prop_expression(attr.value.expression)
4941
+ ) {
4942
+ return {
4943
+ ...attr,
4944
+ metadata: { ...(attr.metadata || {}), from_ref_keyword: true },
4945
+ };
4946
+ }
4947
+ return attr;
4948
+ }
4949
+ if (attr.type === 'JSXSpreadAttribute') {
4623
4950
  return attr;
4624
4951
  }
4625
4952
  if (attr.type === 'SpreadAttribute') {
@@ -4670,15 +4997,28 @@ export function to_jsx_attribute(attr, transform_context) {
4670
4997
  attr_name && attr_name.type === 'Identifier' ? identifier_to_jsx_name(attr_name) : attr_name;
4671
4998
 
4672
4999
  let value = attr.value;
5000
+ const is_ref_expression_value =
5001
+ value?.type === 'RefExpression' ||
5002
+ is_ref_prop_expression(value) ||
5003
+ (value?.type === 'JSXExpressionContainer' && is_ref_prop_expression(value.expression));
4673
5004
  if (value) {
4674
5005
  if (value.type === 'Literal' && typeof value.value === 'string') {
4675
5006
  // Keep string literal as attribute string.
5007
+ } else if (value.type === 'RefExpression') {
5008
+ value = to_jsx_expression_container(create_ref_prop_call(value, transform_context));
4676
5009
  } else if (value.type !== 'JSXExpressionContainer') {
4677
5010
  value = to_jsx_expression_container(value);
5011
+ } else if (value.expression?.type === 'RefExpression') {
5012
+ value = to_jsx_expression_container(
5013
+ create_ref_prop_call(value.expression, transform_context),
5014
+ );
4678
5015
  }
4679
5016
  }
4680
5017
 
4681
5018
  const jsx_attribute = build_jsx_attribute(name, value || null, attr.shorthand === true);
5019
+ if (is_ref_expression_value) {
5020
+ /** @type {any} */ (jsx_attribute.metadata).from_ref_keyword = true;
5021
+ }
4682
5022
 
4683
5023
  if (value_has_unmappable_jsx_loc(value)) {
4684
5024
  /** @type {any} */ (jsx_attribute.metadata).has_unmappable_value = true;
@@ -4700,6 +5040,35 @@ function value_has_unmappable_jsx_loc(value) {
4700
5040
  );
4701
5041
  }
4702
5042
 
5043
+ /**
5044
+ * @param {any} node
5045
+ * @param {TransformContext} transform_context
5046
+ * @returns {any}
5047
+ */
5048
+ function create_ref_prop_call(node, transform_context) {
5049
+ transform_context.needs_ref_prop = true;
5050
+
5051
+ const argument = node.argument;
5052
+ const args = [b.thunk(argument)];
5053
+
5054
+ if (argument.type === 'Identifier' || argument.type === 'MemberExpression') {
5055
+ args.push(
5056
+ b.arrow(
5057
+ [b.id('v')],
5058
+ /** @type {any} */ ({
5059
+ type: 'AssignmentExpression',
5060
+ operator: '=',
5061
+ left: clone_expression_node(argument, false),
5062
+ right: b.id('v'),
5063
+ metadata: { path: [] },
5064
+ }),
5065
+ ),
5066
+ );
5067
+ }
5068
+
5069
+ return b.call(CREATE_REF_PROP_INTERNAL_NAME, ...args);
5070
+ }
5071
+
4703
5072
  /**
4704
5073
  * @param {any} node
4705
5074
  * @param {TransformContext} transform_context
@@ -562,16 +562,17 @@ export function convert_source_map_to_mappings(
562
562
  } else if (node.type === 'JSXIdentifier') {
563
563
  // JSXIdentifiers can also be capitalized (for dynamic components)
564
564
  if (node.loc && node.name) {
565
- if (node.metadata?.is_capitalized) {
566
- tokens.push({
567
- source: node.metadata.source_name,
568
- generated: node.name,
569
- loc: node.loc,
570
- metadata: {},
571
- });
572
- } else {
573
- tokens.push({ source: node.name, generated: node.name, loc: node.loc, metadata: {} });
565
+ /** @type {Token} */
566
+ const token = {
567
+ source: node.metadata?.is_capitalized ? node.metadata.source_name : node.name,
568
+ generated: node.name,
569
+ loc: node.loc,
570
+ metadata: {},
571
+ };
572
+ if (node.metadata?.disable_verification) {
573
+ token.mappingData = { ...mapping_data, verification: false };
574
574
  }
575
+ tokens.push(token);
575
576
  }
576
577
  return; // Leaf node, don't traverse further
577
578
  } else if (node.type === 'Literal') {
@@ -866,6 +867,15 @@ export function convert_source_map_to_mappings(
866
867
  // but since we already map the opening - start, we just need the proper end
867
868
  // and it was causing some issues with mappings
868
869
  mapping.generatedLengths = [mapping.generatedLengths[0] + 1];
870
+ if (!closing && opening.selfClosing) {
871
+ const generated_close_length = '/>;'.length;
872
+ mapping.sourceOffsets = [/** @type {AST.NodeWithLocation} */ (opening).end - 2];
873
+ mapping.lengths = ['/>'.length];
874
+ mapping.generatedOffsets = [
875
+ mapping.generatedOffsets[0] + mapping.generatedLengths[0] - generated_close_length,
876
+ ];
877
+ mapping.generatedLengths = [generated_close_length];
878
+ }
869
879
  mappings.push(mapping);
870
880
  }
871
881
 
package/types/index.d.ts CHANGED
@@ -213,6 +213,7 @@ declare module 'estree' {
213
213
  Text: TextNode;
214
214
  Attribute: Attribute;
215
215
  RefAttribute: RefAttribute;
216
+ RefExpression: RefExpression;
216
217
  SpreadAttribute: SpreadAttribute;
217
218
  ParenthesizedExpression: ParenthesizedExpression;
218
219
  ScriptContent: ScriptContent;
@@ -220,6 +221,7 @@ declare module 'estree' {
220
221
 
221
222
  interface ExpressionMap {
222
223
  Style: Style;
224
+ RefExpression: RefExpression;
223
225
  Text: TextNode;
224
226
  JSXEmptyExpression: ESTreeJSX.JSXEmptyExpression;
225
227
  ParenthesizedExpression: ParenthesizedExpression;
@@ -437,6 +439,12 @@ declare module 'estree' {
437
439
  loc?: AST.SourceLocation;
438
440
  }
439
441
 
442
+ interface RefExpression extends AST.BaseNode {
443
+ type: 'RefExpression';
444
+ argument: AST.Expression;
445
+ loc?: AST.SourceLocation;
446
+ }
447
+
440
448
  interface SpreadAttribute extends AST.BaseNode {
441
449
  type: 'SpreadAttribute';
442
450
  argument: AST.Expression;
@@ -36,6 +36,8 @@ export interface JsxTransformContext {
36
36
  needs_error_boundary: boolean;
37
37
  needs_suspense: boolean;
38
38
  needs_merge_refs: boolean;
39
+ needs_ref_prop: boolean;
40
+ needs_normalize_spread_props: boolean;
39
41
  needs_fragment: boolean;
40
42
  module_scoped_hook_components: boolean;
41
43
  helper_state: {
@@ -290,9 +292,14 @@ export interface JsxPlatform {
290
292
  * Module to import `mergeRefs` from when an element has more than one
291
293
  * `ref` attribute and the platform uses the `'merge-refs'` strategy.
292
294
  * Required when `jsx.multiRefStrategy === 'merge-refs'`; ignored
293
- * otherwise. React: `'@tsrx/react/merge-refs'`. Preact: `'@tsrx/preact/merge-refs'`.
295
+ * otherwise. React: `'@tsrx/react/ref'`. Preact: `'@tsrx/preact/ref'`.
294
296
  */
295
297
  mergeRefs?: string;
298
+ /**
299
+ * Module to import named-ref-prop helpers from when compiling
300
+ * `prop={ref expr}` or normalizing host spreads containing named refs.
301
+ */
302
+ refProp?: string;
296
303
  };
297
304
 
298
305
  jsx: {
@@ -320,6 +327,12 @@ export interface JsxPlatform {
320
327
  * unchanged. The platform's runtime is responsible.
321
328
  */
322
329
  multiRefStrategy?: 'merge-refs' | 'array';
330
+ /**
331
+ * Some JSX runtimes do not apply a `ref` that arrives through a props
332
+ * spread. In that case, host spread normalization also emits an
333
+ * explicit `ref={normalized.ref}` attribute.
334
+ */
335
+ hostSpreadRefStrategy?: 'explicit-ref-attr';
323
336
  };
324
337
 
325
338
  validation: {
@@ -0,0 +1,32 @@
1
+ export type MergeableRefCallback<T> = {
2
+ bivarianceHack(node: T | null): void | (() => void);
3
+ }['bivarianceHack'];
4
+ export type MergeableRefObject<T> = { current: T | null };
5
+ export type MergeableVueRef<T> = { value: T | null };
6
+ export type RefProp<T = unknown> = (node: T | null) => void | (() => void);
7
+
8
+ export type MergeableRef<T> =
9
+ | MergeableRefCallback<T>
10
+ | MergeableRefObject<T>
11
+ | MergeableVueRef<T>
12
+ | null
13
+ | undefined;
14
+
15
+ export function mergeRefs<T = any>(...refs: Array<MergeableRef<T>>): (node: T | null) => () => void;
16
+ export function isRefProp(value: unknown): boolean;
17
+ export function create_ref_prop<T>(
18
+ get_ref_value: () => T,
19
+ set_ref_value?: (value: T) => void,
20
+ ): RefProp<T>;
21
+ export function apply_ref_value<T>(
22
+ ref_value: unknown,
23
+ node: T | null,
24
+ set_ref_value?: (value: T) => void,
25
+ ): void | (() => void);
26
+ export function merge_ref_props<T = any>(
27
+ ...refs: unknown[]
28
+ ): undefined | ((node: T | null) => void | (() => void));
29
+ export function normalize_spread_props<T extends Record<PropertyKey, any> | null | undefined>(
30
+ props: T,
31
+ ...outer_refs: unknown[]
32
+ ): T | Record<PropertyKey, any>;
@@ -1,61 +0,0 @@
1
- /**
2
- * Merge multiple refs (function refs and ref objects) into a single
3
- * callback ref. Used by the tsrx-react, tsrx-preact, and tsrx-vue
4
- * compilers when an element has more than one `ref` attribute, since
5
- * those runtimes treat duplicate `ref` props as a regular duplicate-prop
6
- * collision (last wins) rather than running both. Solid does not use this
7
- * helper — its native runtime accepts an array of refs and the compiler
8
- * emits an array literal directly.
9
- *
10
- * The returned callback ref handles four cases per input:
11
- * - `null` / `undefined`: skipped.
12
- * - function ref: invoked with the node. If it returns a function, that
13
- * return value is treated as a React 19 cleanup. Otherwise we record a
14
- * cleanup that calls the ref with `null` so the legacy unmount contract
15
- * still fires.
16
- * - React-style ref object (`{ current }`): assigned on mount, cleared
17
- * on unmount.
18
- * - Vue-style ref object (`{ value }`, e.g. `ref()` / `useTemplateRef()`):
19
- * assigned on mount, cleared on unmount.
20
- *
21
- * The merged ref always returns a cleanup. Under React 19 the cleanup
22
- * runs on unmount and the inner refs are not separately re-invoked with
23
- * `null`. Under older React, Preact, and Vue the cleanup return value
24
- * is ignored, so the runtime instead invokes the merged ref a second
25
- * time with `null` — which re-runs the loop body, calling each inner
26
- * with `null` and clearing each ref object. Either way every inner ref
27
- * sees a balanced mount/unmount.
28
- *
29
- * @param {...((node: any) => void | (() => void)) | { current: any } | { value: any } | null | undefined} refs
30
- * @returns {(node: any) => (() => void)}
31
- */
32
- export function mergeRefs(...refs) {
33
- return (node) => {
34
- /** @type {Array<() => void>} */
35
- const cleanups = [];
36
- for (const ref of refs) {
37
- if (ref == null) continue;
38
- if (typeof ref === 'function') {
39
- const result = ref(node);
40
- if (typeof result === 'function') {
41
- cleanups.push(result);
42
- } else {
43
- cleanups.push(() => ref(null));
44
- }
45
- } else if ('current' in ref) {
46
- ref.current = node;
47
- cleanups.push(() => {
48
- ref.current = null;
49
- });
50
- } else if ('value' in ref) {
51
- ref.value = node;
52
- cleanups.push(() => {
53
- ref.value = null;
54
- });
55
- }
56
- }
57
- return () => {
58
- for (const cleanup of cleanups) cleanup();
59
- };
60
- };
61
- }
@@ -1,12 +0,0 @@
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;