@tsrx/core 0.0.27 → 0.1.0

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.1.0",
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
@@ -18,7 +18,7 @@ import { error } from './errors.js';
18
18
  import { DIAGNOSTIC_CODES } from './diagnostics.js';
19
19
 
20
20
  const JSX_EXPRESSION_VALUE_ERROR =
21
- 'JSX elements cannot be used as expressions. Wrap with `<>...</>` or `<tsx>...</tsx>` or use elements as statements within a component.';
21
+ 'JSX elements cannot be used as expressions. Wrap JSX with `<>...</>` or `<tsx>...</tsx>`, wrap TSRX templates with `<tsrx>...</tsrx>`, or use elements as statements within a component.';
22
22
 
23
23
  /** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
24
24
  const argument_clash_first_positions = new WeakMap();
@@ -325,7 +325,10 @@ export function TSRXPlugin(config) {
325
325
  }
326
326
 
327
327
  const parent = this.#path.at(-1);
328
- if (!parent || (parent.type !== 'Component' && parent.type !== 'Element')) {
328
+ if (
329
+ !parent ||
330
+ (parent.type !== 'Component' && parent.type !== 'Element' && parent.type !== 'Tsrx')
331
+ ) {
329
332
  return false;
330
333
  }
331
334
 
@@ -1320,7 +1323,7 @@ export function TSRXPlugin(config) {
1320
1323
  if (!nextChars) {
1321
1324
  this.raise(
1322
1325
  ref.start,
1323
- '"component" is a Ripple keyword and cannot be used as an identifier',
1326
+ '"component" is a TSRX keyword and cannot be used as an identifier',
1324
1327
  );
1325
1328
  }
1326
1329
  }
@@ -1345,6 +1348,21 @@ export function TSRXPlugin(config) {
1345
1348
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1346
1349
  this.next();
1347
1350
 
1351
+ if (this.type === tt.name && this.value === 'ref') {
1352
+ const ref_node = /** @type {AST.RefExpression} */ (this.startNode());
1353
+ this.next();
1354
+ if (this.type === tt.braceR) {
1355
+ this.raise(
1356
+ this.start,
1357
+ '"ref" is a TSRX keyword and must be used in the form {ref item}',
1358
+ );
1359
+ }
1360
+ ref_node.argument = this.parseMaybeAssign();
1361
+ node.expression = /** @type {any} */ (this.finishNode(ref_node, 'RefExpression'));
1362
+ this.expect(tt.braceR);
1363
+ return this.finishNode(node, 'JSXExpressionContainer');
1364
+ }
1365
+
1348
1366
  if (this.type === tt.name && this.value === 'html') {
1349
1367
  node.html = true;
1350
1368
  this.next();
@@ -1794,7 +1812,8 @@ export function TSRXPlugin(config) {
1794
1812
  ch === 125 &&
1795
1813
  (this.#path.length === 0 ||
1796
1814
  this.#path.at(-1)?.type === 'Component' ||
1797
- this.#path.at(-1)?.type === 'Element')
1815
+ this.#path.at(-1)?.type === 'Element' ||
1816
+ this.#path.at(-1)?.type === 'Tsrx')
1798
1817
  ) {
1799
1818
  this.#resetTokenStartToCurrentPosition();
1800
1819
  return original.readToken.call(this, ch);
@@ -1834,7 +1853,9 @@ export function TSRXPlugin(config) {
1834
1853
  * Override jsx_parseElement to intercept expression-level JSX.
1835
1854
  * This is called by acorn-jsx's parseExprAtom when it encounters <
1836
1855
  * in expression position. Bare fragments are treated as shorthand
1837
- * for <tsx>...</tsx>; other tags must still use <tsx> or <tsx:*>.
1856
+ * for <tsx>...</tsx>. <tsrx>...</tsrx> admits native TSRX
1857
+ * template syntax as an expression value. Other tags must still use
1858
+ * <tsx>, <tsrx>, or <tsx:*>.
1838
1859
  * @type {Parse.Parser['jsx_parseElement']}
1839
1860
  */
1840
1861
  jsx_parseElement() {
@@ -1844,11 +1865,12 @@ export function TSRXPlugin(config) {
1844
1865
  return super.jsx_parseElement();
1845
1866
  }
1846
1867
 
1847
- // Check if the element being parsed IS a <tsx> or <tsx:*> tag
1868
+ // Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
1848
1869
  // Current token is jsxTagStart, this.end is position after '<'
1849
1870
  const tag_name_start = this.end;
1850
1871
  const is_fragment_tag = this.input.charCodeAt(tag_name_start) === 62;
1851
1872
  const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
1873
+ const char_after_tsrx = this.input.charCodeAt(tag_name_start + 4);
1852
1874
  const is_tsx_tag =
1853
1875
  this.input.startsWith('tsx', tag_name_start) &&
1854
1876
  (tag_name_start + 3 >= this.input.length ||
@@ -1859,9 +1881,18 @@ export function TSRXPlugin(config) {
1859
1881
  char_after_tsx === 10 || // newline
1860
1882
  char_after_tsx === 13 || // carriage return
1861
1883
  char_after_tsx === 58); // : (tsx:react)
1862
-
1863
- if (is_fragment_tag || is_tsx_tag) {
1864
- // Use Ripple's parseElement to create a Tsx/TsxCompat node.
1884
+ const is_tsrx_tag =
1885
+ this.input.startsWith('tsrx', tag_name_start) &&
1886
+ (tag_name_start + 4 >= this.input.length ||
1887
+ char_after_tsrx === 62 || // >
1888
+ char_after_tsrx === 47 || // / (self-closing)
1889
+ char_after_tsrx === 32 || // space
1890
+ char_after_tsrx === 9 || // tab
1891
+ char_after_tsrx === 10 || // newline
1892
+ char_after_tsrx === 13); // carriage return
1893
+
1894
+ if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
1895
+ // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
1865
1896
  // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
1866
1897
  this.next();
1867
1898
  return /** @type {import('estree-jsx').JSXElement} */ (
@@ -1892,7 +1923,9 @@ export function TSRXPlugin(config) {
1892
1923
  const start = this.start - 1;
1893
1924
  const position = new acorn.Position(this.curLine, start - this.lineStart);
1894
1925
 
1895
- const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
1926
+ const element = /** @type {AST.Element | AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (
1927
+ this.startNode()
1928
+ );
1896
1929
  element.start = start;
1897
1930
  /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1898
1931
  element.metadata = { path: [] };
@@ -1913,6 +1946,11 @@ export function TSRXPlugin(config) {
1913
1946
  !is_tsx_compat &&
1914
1947
  open.name.type === 'JSXIdentifier' &&
1915
1948
  open.name.name === 'tsx';
1949
+ const is_tsrx =
1950
+ !is_fragment &&
1951
+ !is_tsx_compat &&
1952
+ open.name.type === 'JSXIdentifier' &&
1953
+ open.name.name === 'tsrx';
1916
1954
 
1917
1955
  if (is_tsx_compat) {
1918
1956
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
@@ -1935,6 +1973,15 @@ export function TSRXPlugin(config) {
1935
1973
  `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
1936
1974
  );
1937
1975
  }
1976
+ } else if (is_tsrx) {
1977
+ /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
1978
+
1979
+ if (open.selfClosing) {
1980
+ this.raise(
1981
+ open.start,
1982
+ `TSRX elements cannot be self-closing. '<tsrx />' must have a closing tag '</tsrx>'.`,
1983
+ );
1984
+ }
1938
1985
  } else if (is_fragment) {
1939
1986
  /** @type {AST.Tsx} */ (element).type = 'Tsx';
1940
1987
  } else {
@@ -1972,7 +2019,7 @@ export function TSRXPlugin(config) {
1972
2019
  }
1973
2020
  }
1974
2021
 
1975
- if (!is_tsx_compat && !is_tsx && !is_fragment) {
2022
+ if (!is_tsx_compat && !is_tsx && !is_tsrx && !is_fragment) {
1976
2023
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1977
2024
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1978
2025
  );
@@ -2166,6 +2213,7 @@ export function TSRXPlugin(config) {
2166
2213
  parent?.type === 'Component' ||
2167
2214
  parent?.type === 'Element' ||
2168
2215
  parent?.type === 'Tsx' ||
2216
+ parent?.type === 'Tsrx' ||
2169
2217
  parent?.type === 'TsxCompat';
2170
2218
 
2171
2219
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -2234,7 +2282,21 @@ export function TSRXPlugin(config) {
2234
2282
  this.#popTsxTokenContextBeforeTemplateExpressionChild();
2235
2283
  this.next();
2236
2284
  }
2237
- } else if (this.#path[this.#path.length - 1] === element) {
2285
+ } else if (element.type === 'Tsrx' && this.#path[this.#path.length - 1] === element) {
2286
+ this.#report_broken_markup_error(
2287
+ this.start,
2288
+ "Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
2289
+ );
2290
+ element.unclosed = true;
2291
+ /** @type {AST.SourceLocation} */ (element.loc).end = {
2292
+ .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2293
+ };
2294
+ element.end = element.openingElement.end;
2295
+ this.#path.pop();
2296
+ } else if (
2297
+ element.type === 'Element' &&
2298
+ this.#path[this.#path.length - 1] === element
2299
+ ) {
2238
2300
  // Check if this element was properly closed
2239
2301
  const tagName = this.getElementName(element.id);
2240
2302
  this.#report_broken_markup_error(
@@ -2242,7 +2304,7 @@ export function TSRXPlugin(config) {
2242
2304
  `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
2243
2305
  );
2244
2306
  element.unclosed = true;
2245
- element.loc.end = {
2307
+ /** @type {AST.SourceLocation} */ (element.loc).end = {
2246
2308
  .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2247
2309
  };
2248
2310
  element.end = element.openingElement.end;
@@ -2257,6 +2319,7 @@ export function TSRXPlugin(config) {
2257
2319
  parent?.type === 'Component' ||
2258
2320
  parent?.type === 'Element' ||
2259
2321
  parent?.type === 'Tsx' ||
2322
+ parent?.type === 'Tsrx' ||
2260
2323
  parent?.type === 'TsxCompat';
2261
2324
 
2262
2325
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -2264,7 +2327,13 @@ export function TSRXPlugin(config) {
2264
2327
  }
2265
2328
  }
2266
2329
 
2267
- if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
2330
+ if (
2331
+ element.closingElement &&
2332
+ !is_tsx_compat &&
2333
+ !is_tsx &&
2334
+ !is_tsrx &&
2335
+ element.closingElement.name
2336
+ ) {
2268
2337
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
2269
2338
  element.closingElement.name,
2270
2339
  );
@@ -2465,10 +2534,26 @@ export function TSRXPlugin(config) {
2465
2534
  this.context.pop();
2466
2535
  }
2467
2536
  return;
2468
- } else if (this.type === tstt.jsxTagStart) {
2537
+ } else if (
2538
+ this.type === tstt.jsxTagStart ||
2539
+ (this.input.charCodeAt(this.start) === 60 /* < */ &&
2540
+ this.input.charCodeAt(this.start + 1) === 47) /* / */
2541
+ ) {
2469
2542
  const startPos = this.start;
2470
2543
  const startLoc = this.startLoc;
2471
- this.next();
2544
+ if (this.type === tstt.jsxTagStart) {
2545
+ this.next();
2546
+ } else {
2547
+ // A control-flow block inside <tsrx> can leave the tokenizer
2548
+ // in normal JS mode, so `</tsrx>` may arrive as a relational
2549
+ // `<` token. Re-enter JSX closing-tag parsing manually.
2550
+ this.pos = startPos + 1;
2551
+ this.type = tstt.jsxTagStart;
2552
+ this.start = startPos;
2553
+ this.startLoc = startLoc;
2554
+ this.exprAllowed = false;
2555
+ this.next();
2556
+ }
2472
2557
  if (this.value === '/' || this.type === tt.slash) {
2473
2558
  // Consume '/'
2474
2559
  this.next();
@@ -2485,6 +2570,7 @@ export function TSRXPlugin(config) {
2485
2570
  !currentElement ||
2486
2571
  (currentElement.type !== 'Element' &&
2487
2572
  currentElement.type !== 'Tsx' &&
2573
+ currentElement.type !== 'Tsrx' &&
2488
2574
  currentElement.type !== 'TsxCompat')
2489
2575
  ) {
2490
2576
  this.raise(this.start, 'Unexpected closing tag');
@@ -2507,6 +2593,12 @@ export function TSRXPlugin(config) {
2507
2593
  closingElement.name?.type === 'JSXNamespacedName'
2508
2594
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2509
2595
  : this.getElementName(closingElement.name);
2596
+ } else if (currentElement.type === 'Tsrx') {
2597
+ openingTagName = 'tsrx';
2598
+ closingTagName =
2599
+ closingElement.name?.type === 'JSXNamespacedName'
2600
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2601
+ : this.getElementName(closingElement.name);
2510
2602
  } else {
2511
2603
  // Regular Element node (or fragment)
2512
2604
  openingTagName = currentElement.id ? this.getElementName(currentElement.id) : null;
@@ -2529,7 +2621,12 @@ export function TSRXPlugin(config) {
2529
2621
  const elem = this.#path[this.#path.length - 1];
2530
2622
 
2531
2623
  // Stop at non-Element boundaries (Component, etc.)
2532
- if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
2624
+ if (
2625
+ elem.type !== 'Element' &&
2626
+ elem.type !== 'Tsx' &&
2627
+ elem.type !== 'Tsrx' &&
2628
+ elem.type !== 'TsxCompat'
2629
+ ) {
2533
2630
  break;
2534
2631
  }
2535
2632
 
@@ -2540,9 +2637,11 @@ export function TSRXPlugin(config) {
2540
2637
  ? elem.openingElement.name
2541
2638
  ? 'tsx'
2542
2639
  : null
2543
- : elem.id
2544
- ? this.getElementName(elem.id)
2545
- : null;
2640
+ : elem.type === 'Tsrx'
2641
+ ? 'tsrx'
2642
+ : elem.id
2643
+ ? this.getElementName(elem.id)
2644
+ : null;
2546
2645
 
2547
2646
  // Found matching opening tag
2548
2647
  if (elemName === closingTagName) {
@@ -2561,12 +2660,19 @@ export function TSRXPlugin(config) {
2561
2660
  }
2562
2661
 
2563
2662
  const elementToClose = this.#path[this.#path.length - 1];
2564
- if (elementToClose && elementToClose.type === 'Element') {
2565
- const elementToCloseName = /** @type {AST.Element} */ (elementToClose).id
2566
- ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
2567
- : null;
2663
+ if (
2664
+ elementToClose &&
2665
+ (elementToClose.type === 'Element' || elementToClose.type === 'Tsrx')
2666
+ ) {
2667
+ const elementToCloseName =
2668
+ elementToClose.type === 'Tsrx'
2669
+ ? 'tsrx'
2670
+ : /** @type {AST.Element} */ (elementToClose).id
2671
+ ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
2672
+ : null;
2568
2673
  if (elementToCloseName === closingTagName) {
2569
- /** @type {AST.Element} */ (elementToClose).closingElement = closingElement;
2674
+ /** @type {AST.Element | AST.Tsrx} */ (elementToClose).closingElement =
2675
+ closingElement;
2570
2676
  }
2571
2677
  }
2572
2678
 
@@ -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
+ }
package/src/scope.js CHANGED
@@ -127,6 +127,13 @@ export function create_scopes(ast, root, parent, error_options) {
127
127
  next({ scope });
128
128
  },
129
129
 
130
+ Tsrx(node, { state, next }) {
131
+ const scope = state.scope.child();
132
+ scopes.set(node, scope);
133
+
134
+ next({ scope });
135
+ },
136
+
130
137
  TSModuleDeclaration(node, { state, next }) {
131
138
  const is_submodule = node.metadata?.module_keyword === 'module';
132
139
  if (is_submodule && node.id?.type === 'Identifier') {