@tsrx/core 0.0.17 → 0.0.18
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 +4 -1
- package/src/index.js +3 -0
- package/src/plugin.js +14 -3
- package/src/runtime/merge-refs.js +61 -0
- package/src/transform/jsx/index.js +216 -19
- package/types/jsx-platform.d.ts +25 -2
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.
|
|
6
|
+
"version": "0.0.18",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"./types/acorn": {
|
|
28
28
|
"types": "./types/acorn.d.ts"
|
|
29
29
|
},
|
|
30
|
+
"./runtime/merge-refs": {
|
|
31
|
+
"default": "./src/runtime/merge-refs.js"
|
|
32
|
+
},
|
|
30
33
|
"./test-harness/source-mappings": "./tests/shared/source-mappings.js",
|
|
31
34
|
"./test-harness/compile": "./tests/shared/compile.js"
|
|
32
35
|
},
|
package/src/index.js
CHANGED
|
@@ -139,6 +139,9 @@ export { escape } from './utils/escaping.js';
|
|
|
139
139
|
// Transform
|
|
140
140
|
export {
|
|
141
141
|
createJsxTransform,
|
|
142
|
+
merge_duplicate_refs as mergeDuplicateRefs,
|
|
143
|
+
to_jsx_attribute as toJsxAttribute,
|
|
144
|
+
validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
|
|
142
145
|
component_to_function_declaration as componentToFunctionDeclaration,
|
|
143
146
|
} from './transform/jsx/index.js';
|
|
144
147
|
export {
|
package/src/plugin.js
CHANGED
|
@@ -176,6 +176,7 @@ export function TSRXPlugin(config) {
|
|
|
176
176
|
/** @type {AST.Node[]} */
|
|
177
177
|
#path = [];
|
|
178
178
|
#allowTagStartAfterDoubleQuotedText = false;
|
|
179
|
+
#allowDoubleQuotedTextChildAfterBrace = false;
|
|
179
180
|
#commentContextId = 0;
|
|
180
181
|
#loose = false;
|
|
181
182
|
/** @type {import('../types/index').CompileError[] | undefined} */
|
|
@@ -229,7 +230,7 @@ export function TSRXPlugin(config) {
|
|
|
229
230
|
prev === 34 || // "
|
|
230
231
|
prev === 59 || // ;
|
|
231
232
|
prev === 62 || // >
|
|
232
|
-
prev === 123 || // {
|
|
233
|
+
(prev === 123 && this.#allowDoubleQuotedTextChildAfterBrace) || // {
|
|
233
234
|
prev === 125 // }
|
|
234
235
|
);
|
|
235
236
|
}
|
|
@@ -632,8 +633,14 @@ export function TSRXPlugin(config) {
|
|
|
632
633
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
633
634
|
*/
|
|
634
635
|
getTokenFromCode(code) {
|
|
635
|
-
if (code === 34
|
|
636
|
-
|
|
636
|
+
if (code === 34) {
|
|
637
|
+
const is_double_quoted_text_child = this.#isDoubleQuotedTextChildStart();
|
|
638
|
+
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
639
|
+
if (is_double_quoted_text_child) {
|
|
640
|
+
return this.#readDoubleQuotedTextChildToken();
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
637
644
|
}
|
|
638
645
|
|
|
639
646
|
if (code !== 60) {
|
|
@@ -1058,6 +1065,9 @@ export function TSRXPlugin(config) {
|
|
|
1058
1065
|
const parent_function_body_depth = this.#functionBodyDepth;
|
|
1059
1066
|
this.#functionBodyDepth = 0;
|
|
1060
1067
|
|
|
1068
|
+
if (this.type === tt.braceL) {
|
|
1069
|
+
this.#allowDoubleQuotedTextChildAfterBrace = true;
|
|
1070
|
+
}
|
|
1061
1071
|
this.eat(tt.braceL);
|
|
1062
1072
|
node.body = [];
|
|
1063
1073
|
this.#path.push(node);
|
|
@@ -2654,6 +2664,7 @@ export function TSRXPlugin(config) {
|
|
|
2654
2664
|
if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
|
|
2655
2665
|
|
|
2656
2666
|
node.body = [];
|
|
2667
|
+
this.#allowDoubleQuotedTextChildAfterBrace = true;
|
|
2657
2668
|
this.expect(tt.braceL);
|
|
2658
2669
|
if (createNewLexicalScope) {
|
|
2659
2670
|
this.enterScope(0);
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
}
|
|
@@ -50,6 +50,7 @@ import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
|
50
50
|
* local_statement_component_index: number,
|
|
51
51
|
* needs_error_boundary: boolean,
|
|
52
52
|
* needs_suspense: boolean,
|
|
53
|
+
* needs_merge_refs: boolean,
|
|
53
54
|
* helper_state: { base_name: string, next_id: number, helpers: any[], statics: any[] } | null,
|
|
54
55
|
* available_bindings: Map<string, AST.Identifier>,
|
|
55
56
|
* lazy_next_id: number,
|
|
@@ -103,6 +104,7 @@ export function createJsxTransform(platform) {
|
|
|
103
104
|
local_statement_component_index: 0,
|
|
104
105
|
needs_error_boundary: false,
|
|
105
106
|
needs_suspense: false,
|
|
107
|
+
needs_merge_refs: false,
|
|
106
108
|
helper_state: null,
|
|
107
109
|
available_bindings: new Map(),
|
|
108
110
|
lazy_next_id: 0,
|
|
@@ -2679,10 +2681,11 @@ function create_jsx_element(tag_name, attributes, children) {
|
|
|
2679
2681
|
}
|
|
2680
2682
|
|
|
2681
2683
|
/**
|
|
2682
|
-
* Inject import declarations
|
|
2683
|
-
*
|
|
2684
|
-
*
|
|
2685
|
-
*
|
|
2684
|
+
* Inject runtime-helper import declarations the transform decided it needed
|
|
2685
|
+
* during the walk: `Suspense` for `try { ... } pending { ... }`,
|
|
2686
|
+
* `TsrxErrorBoundary` for `try { ... } catch (...)`, and `mergeRefs` for
|
|
2687
|
+
* elements with multiple `ref` attributes under the `'merge-refs'`
|
|
2688
|
+
* strategy. Import sources are platform-specific.
|
|
2686
2689
|
*
|
|
2687
2690
|
* @param {AST.Program} program
|
|
2688
2691
|
* @param {TransformContext} transform_context
|
|
@@ -2743,6 +2746,35 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
|
|
|
2743
2746
|
});
|
|
2744
2747
|
}
|
|
2745
2748
|
|
|
2749
|
+
if (transform_context.needs_merge_refs && platform.imports.mergeRefs) {
|
|
2750
|
+
const merge_refs_source = platform.imports.mergeRefs;
|
|
2751
|
+
imports.push({
|
|
2752
|
+
type: 'ImportDeclaration',
|
|
2753
|
+
specifiers: [
|
|
2754
|
+
{
|
|
2755
|
+
type: 'ImportSpecifier',
|
|
2756
|
+
imported: {
|
|
2757
|
+
type: 'Identifier',
|
|
2758
|
+
name: 'mergeRefs',
|
|
2759
|
+
metadata: { path: [] },
|
|
2760
|
+
},
|
|
2761
|
+
local: {
|
|
2762
|
+
type: 'Identifier',
|
|
2763
|
+
name: MERGE_REFS_LOCAL_NAME,
|
|
2764
|
+
metadata: { path: [] },
|
|
2765
|
+
},
|
|
2766
|
+
metadata: { path: [] },
|
|
2767
|
+
},
|
|
2768
|
+
],
|
|
2769
|
+
source: {
|
|
2770
|
+
type: 'Literal',
|
|
2771
|
+
value: merge_refs_source,
|
|
2772
|
+
raw: `'${merge_refs_source}'`,
|
|
2773
|
+
},
|
|
2774
|
+
metadata: { path: [] },
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2746
2778
|
if (imports.length > 0) {
|
|
2747
2779
|
program.body.unshift(...imports);
|
|
2748
2780
|
}
|
|
@@ -2915,9 +2947,15 @@ function to_jsx_expression_container(expression, source_node = expression) {
|
|
|
2915
2947
|
/**
|
|
2916
2948
|
* Dispatch point for element attribute transformation. Platforms can replace
|
|
2917
2949
|
* the default "map over `to_jsx_attribute`" via
|
|
2918
|
-
* `hooks.transformElementAttributes
|
|
2919
|
-
*
|
|
2920
|
-
*
|
|
2950
|
+
* `hooks.transformElementAttributes`. Whether or not the hook is used,
|
|
2951
|
+
* the result is run through `merge_duplicate_refs` so platforms with a
|
|
2952
|
+
* `multiRefStrategy` get duplicate-`ref` handling for free.
|
|
2953
|
+
*
|
|
2954
|
+
* Before lowering, the raw attribute list is validated to reject elements
|
|
2955
|
+
* with more than one TSX-style `ref={...}` attribute — that shape produces
|
|
2956
|
+
* duplicate JSX props which the JSX runtime collapses to last-wins (and
|
|
2957
|
+
* which TypeScript can't type cleanly). Multiple Ripple `{ref expr}`
|
|
2958
|
+
* keyword-form refs remain valid and merge into a single ref attribute.
|
|
2921
2959
|
*
|
|
2922
2960
|
* @param {any[]} attrs
|
|
2923
2961
|
* @param {TransformContext} transform_context
|
|
@@ -2925,21 +2963,175 @@ function to_jsx_expression_container(expression, source_node = expression) {
|
|
|
2925
2963
|
* @returns {any[]}
|
|
2926
2964
|
*/
|
|
2927
2965
|
function transform_element_attributes_dispatch(attrs, transform_context, element) {
|
|
2966
|
+
validate_at_most_one_ref_attribute(attrs);
|
|
2928
2967
|
const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
|
|
2929
2968
|
if (preprocess) {
|
|
2930
2969
|
attrs = preprocess(attrs, transform_context, element);
|
|
2931
2970
|
}
|
|
2932
2971
|
const hook = transform_context.platform.hooks?.transformElementAttributes;
|
|
2933
|
-
|
|
2934
|
-
|
|
2972
|
+
const result = hook
|
|
2973
|
+
? hook(attrs, transform_context, element)
|
|
2974
|
+
: attrs.map((/** @type {any} */ a) => to_jsx_attribute(a, transform_context));
|
|
2975
|
+
return merge_duplicate_refs(result, transform_context);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
/**
|
|
2979
|
+
* Reject elements with more than one TSX-style `ref={...}` attribute.
|
|
2980
|
+
* Ripple's `{ref expr}` keyword form is parsed as a `RefAttribute` node
|
|
2981
|
+
* and is excluded from the count — multiple keyword-form refs are a Ripple
|
|
2982
|
+
* feature that compose via the merge pass. This validator runs over the
|
|
2983
|
+
* raw, pre-lowering attribute list so each shape is still distinguishable
|
|
2984
|
+
* by `type`. Ripple `Element` attributes have type `Attribute` with an
|
|
2985
|
+
* `Identifier` name (the parser normalizes `JSXAttribute`/`JSXIdentifier`
|
|
2986
|
+
* for non-Tsx elements); inside `<tsx:react>` compat blocks they retain
|
|
2987
|
+
* the original `JSXAttribute`/`JSXIdentifier` shape, so we accept both.
|
|
2988
|
+
*
|
|
2989
|
+
* @param {any[]} raw_attrs
|
|
2990
|
+
*/
|
|
2991
|
+
export function validate_at_most_one_ref_attribute(raw_attrs) {
|
|
2992
|
+
let first = null;
|
|
2993
|
+
for (const attr of raw_attrs) {
|
|
2994
|
+
if (!attr) continue;
|
|
2995
|
+
const is_ref_attr =
|
|
2996
|
+
(attr.type === 'Attribute' &&
|
|
2997
|
+
attr.name &&
|
|
2998
|
+
attr.name.type === 'Identifier' &&
|
|
2999
|
+
attr.name.name === 'ref') ||
|
|
3000
|
+
(attr.type === 'JSXAttribute' &&
|
|
3001
|
+
attr.name &&
|
|
3002
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
3003
|
+
attr.name.name === 'ref');
|
|
3004
|
+
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
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
first = attr;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
/**
|
|
3017
|
+
* Collapse multiple `ref` JSXAttributes on a single element into one. Both
|
|
3018
|
+
* Ripple's `{ref expr}` keyword form and TSX-style `ref={expr}` are handled
|
|
3019
|
+
* because they have already been normalized to `JSXAttribute` named `ref`
|
|
3020
|
+
* by `to_jsx_attribute` (Ripple) or the parser (TSX-style). The shape of
|
|
3021
|
+
* the merged value depends on `platform.jsx.multiRefStrategy`:
|
|
3022
|
+
*
|
|
3023
|
+
* - `'merge-refs'` — emit `ref={__mergeRefs(a, b, ...)}` and flag
|
|
3024
|
+
* `needs_merge_refs` so an import is injected later. React and Preact
|
|
3025
|
+
* need this because their runtimes dedupe duplicate `ref` props.
|
|
3026
|
+
* - `'array'` — emit `ref={[a, b, ...]}`. Solid's runtime iterates
|
|
3027
|
+
* array refs natively, so no helper is required.
|
|
3028
|
+
* - `undefined` — return the list unchanged. The platform takes care
|
|
3029
|
+
* of duplicate refs at runtime (or doesn't support them).
|
|
3030
|
+
*
|
|
3031
|
+
* Single-ref elements are always left unchanged so trivial cases stay
|
|
3032
|
+
* import-free and produce no helper call.
|
|
3033
|
+
*
|
|
3034
|
+
* @param {any[]} jsx_attrs
|
|
3035
|
+
* @param {TransformContext} transform_context
|
|
3036
|
+
* @returns {any[]}
|
|
3037
|
+
*/
|
|
3038
|
+
export function merge_duplicate_refs(jsx_attrs, transform_context) {
|
|
3039
|
+
const strategy = transform_context.platform.jsx.multiRefStrategy;
|
|
3040
|
+
if (!strategy) return jsx_attrs;
|
|
3041
|
+
|
|
3042
|
+
let count = 0;
|
|
3043
|
+
for (const attr of jsx_attrs) {
|
|
3044
|
+
if (is_jsx_ref_attribute(attr)) count += 1;
|
|
3045
|
+
}
|
|
3046
|
+
if (count <= 1) return jsx_attrs;
|
|
3047
|
+
|
|
3048
|
+
/** @type {any[]} */
|
|
3049
|
+
const ref_exprs = [];
|
|
3050
|
+
/** @type {any[]} */
|
|
3051
|
+
const result = [];
|
|
3052
|
+
for (const attr of jsx_attrs) {
|
|
3053
|
+
if (is_jsx_ref_attribute(attr)) {
|
|
3054
|
+
ref_exprs.push(attr.value.expression);
|
|
3055
|
+
} else {
|
|
3056
|
+
result.push(attr);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
const merged_value =
|
|
3061
|
+
strategy === 'merge-refs'
|
|
3062
|
+
? /** @type {any} */ ({
|
|
3063
|
+
type: 'CallExpression',
|
|
3064
|
+
callee: {
|
|
3065
|
+
type: 'Identifier',
|
|
3066
|
+
name: MERGE_REFS_LOCAL_NAME,
|
|
3067
|
+
metadata: { path: [] },
|
|
3068
|
+
},
|
|
3069
|
+
arguments: ref_exprs,
|
|
3070
|
+
optional: false,
|
|
3071
|
+
metadata: { path: [] },
|
|
3072
|
+
})
|
|
3073
|
+
: /** @type {any} */ ({
|
|
3074
|
+
type: 'ArrayExpression',
|
|
3075
|
+
elements: ref_exprs,
|
|
3076
|
+
metadata: { path: [] },
|
|
3077
|
+
});
|
|
3078
|
+
|
|
3079
|
+
if (strategy === 'merge-refs') {
|
|
3080
|
+
transform_context.needs_merge_refs = true;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
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(
|
|
3088
|
+
/** @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,
|
|
3097
|
+
metadata: { path: [] },
|
|
3098
|
+
}),
|
|
3099
|
+
);
|
|
3100
|
+
|
|
3101
|
+
return result;
|
|
2935
3102
|
}
|
|
2936
3103
|
|
|
3104
|
+
/**
|
|
3105
|
+
* @param {any} attr
|
|
3106
|
+
* @returns {boolean}
|
|
3107
|
+
*/
|
|
3108
|
+
function is_jsx_ref_attribute(attr) {
|
|
3109
|
+
return (
|
|
3110
|
+
!!attr &&
|
|
3111
|
+
attr.type === 'JSXAttribute' &&
|
|
3112
|
+
!!attr.name &&
|
|
3113
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
3114
|
+
attr.name.name === 'ref' &&
|
|
3115
|
+
!!attr.value &&
|
|
3116
|
+
attr.value.type === 'JSXExpressionContainer' &&
|
|
3117
|
+
!!attr.value.expression &&
|
|
3118
|
+
attr.value.expression.type !== 'JSXEmptyExpression'
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
/**
|
|
3123
|
+
* Local alias used for the injected `mergeRefs` import. The leading
|
|
3124
|
+
* double-underscore matches the convention for compiler-generated
|
|
3125
|
+
* identifiers and avoids shadowing user-declared `mergeRefs` symbols.
|
|
3126
|
+
*/
|
|
3127
|
+
const MERGE_REFS_LOCAL_NAME = '__mergeRefs';
|
|
3128
|
+
|
|
2937
3129
|
/**
|
|
2938
3130
|
* @param {any} attr
|
|
2939
3131
|
* @param {TransformContext} transform_context
|
|
2940
3132
|
* @returns {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute}
|
|
2941
3133
|
*/
|
|
2942
|
-
function to_jsx_attribute(attr, transform_context) {
|
|
3134
|
+
export function to_jsx_attribute(attr, transform_context) {
|
|
2943
3135
|
if (!attr) return attr;
|
|
2944
3136
|
if (attr.type === 'JSXAttribute' || attr.type === 'JSXSpreadAttribute') {
|
|
2945
3137
|
return attr;
|
|
@@ -2954,15 +3146,20 @@ function to_jsx_attribute(attr, transform_context) {
|
|
|
2954
3146
|
);
|
|
2955
3147
|
}
|
|
2956
3148
|
if (attr.type === 'RefAttribute') {
|
|
2957
|
-
//
|
|
2958
|
-
//
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3149
|
+
// `{ref expr}` and the generated `ref={expr}` have different shapes,
|
|
3150
|
+
// so the source-to-generated mapping is imprecise — but pointing
|
|
3151
|
+
// editors at the `{ref expr}` span is still useful for hover/jump,
|
|
3152
|
+
// matching how shorthand `{name}` → `name={name}` carries loc.
|
|
3153
|
+
return set_loc(
|
|
3154
|
+
/** @type {any} */ ({
|
|
3155
|
+
type: 'JSXAttribute',
|
|
3156
|
+
name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
|
|
3157
|
+
value: to_jsx_expression_container(attr.argument),
|
|
3158
|
+
shorthand: false,
|
|
3159
|
+
metadata: { path: [] },
|
|
3160
|
+
}),
|
|
3161
|
+
attr,
|
|
3162
|
+
);
|
|
2966
3163
|
}
|
|
2967
3164
|
|
|
2968
3165
|
// Platforms that expect React-style DOM attrs (React) rewrite `class` to
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -91,8 +91,10 @@ export interface JsxPlatformHooks {
|
|
|
91
91
|
injectImports?: (program: AST.Program, ctx: any, suspenseSource: string) => void;
|
|
92
92
|
/**
|
|
93
93
|
* Transform a Ripple element's attributes to JSX attributes. Default
|
|
94
|
-
* is "map over `to_jsx_attribute`"
|
|
95
|
-
*
|
|
94
|
+
* is "map over `to_jsx_attribute`" plus the shared multi-`ref` merge
|
|
95
|
+
* pass. Platforms that own a `transformElement` hook (e.g. Solid) bypass
|
|
96
|
+
* this entirely — they never reach the dispatch path that would call
|
|
97
|
+
* it — and run their own attribute pass inside their `transformElement`.
|
|
96
98
|
*/
|
|
97
99
|
transformElementAttributes?: (attrs: any[], ctx: any, element: any) => any[];
|
|
98
100
|
/**
|
|
@@ -194,6 +196,13 @@ export interface JsxPlatform {
|
|
|
194
196
|
* block appears. Usually `'@tsrx/<platform>/error-boundary'`.
|
|
195
197
|
*/
|
|
196
198
|
errorBoundary: string;
|
|
199
|
+
/**
|
|
200
|
+
* Module to import `mergeRefs` from when an element has more than one
|
|
201
|
+
* `ref` attribute and the platform uses the `'merge-refs'` strategy.
|
|
202
|
+
* Required when `jsx.multiRefStrategy === 'merge-refs'`; ignored
|
|
203
|
+
* otherwise. React: `'@tsrx/react/merge-refs'`. Preact: `'@tsrx/preact/merge-refs'`.
|
|
204
|
+
*/
|
|
205
|
+
mergeRefs?: string;
|
|
197
206
|
};
|
|
198
207
|
|
|
199
208
|
jsx: {
|
|
@@ -207,6 +216,20 @@ export interface JsxPlatform {
|
|
|
207
216
|
* only `'react'`. Preact accepts both `'preact'` and `'react'`.
|
|
208
217
|
*/
|
|
209
218
|
acceptedTsxKinds: readonly string[];
|
|
219
|
+
/**
|
|
220
|
+
* How to collapse multiple `ref` attributes on the same element into
|
|
221
|
+
* one. React's and Preact's runtimes treat duplicate `ref` props as
|
|
222
|
+
* a normal duplicate-prop collision (last wins), so they need a
|
|
223
|
+
* compile-time merge. Solid's runtime accepts an array of refs
|
|
224
|
+
* natively, so it can use the cheaper array form.
|
|
225
|
+
*
|
|
226
|
+
* - `'merge-refs'`: emit `ref={mergeRefs(a, b, ...)}` and inject an
|
|
227
|
+
* import from `imports.mergeRefs`.
|
|
228
|
+
* - `'array'`: emit `ref={[a, b, ...]}`. No runtime helper needed.
|
|
229
|
+
* - `undefined`: no merging — duplicate `ref` attributes pass through
|
|
230
|
+
* unchanged. The platform's runtime is responsible.
|
|
231
|
+
*/
|
|
232
|
+
multiRefStrategy?: 'merge-refs' | 'array';
|
|
210
233
|
};
|
|
211
234
|
|
|
212
235
|
validation: {
|