@tsrx/core 0.0.14 → 0.0.16
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/README.md +13 -8
- package/package.json +1 -1
- package/src/index.js +4 -1
- package/src/plugin.js +39 -1
- package/src/transform/jsx/helpers.js +64 -0
- package/src/transform/jsx/index.js +514 -61
- package/src/transform/segments.js +4 -6
- package/src/utils/builders.js +6 -4
- package/types/index.d.ts +2 -1
- package/types/jsx-platform.d.ts +83 -4
- package/types/parse.d.ts +2 -0
package/README.md
CHANGED
|
@@ -61,15 +61,20 @@ component Button(props: Props) {
|
|
|
61
61
|
### 2. JSX-as-statements
|
|
62
62
|
|
|
63
63
|
Inside a `component` body, JSX elements are valid _statement_ forms. They describe
|
|
64
|
-
rendered output and are not expressions — they have no value.
|
|
64
|
+
rendered output and are not expressions — they have no value. Static text may be
|
|
65
|
+
written as a direct double-quoted child; dynamic values and other JavaScript
|
|
66
|
+
expressions stay inside `{}`.
|
|
65
67
|
|
|
66
68
|
```tsx
|
|
67
69
|
component Greeting() {
|
|
68
|
-
<h1>
|
|
69
|
-
<p>
|
|
70
|
+
<h1>"Hello"</h1>
|
|
71
|
+
<p>"Welcome"</p>
|
|
70
72
|
}
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
Only double quotes have direct-child text meaning. Single-quoted strings and
|
|
76
|
+
template literals remain JavaScript expressions and must be written inside `{}`.
|
|
77
|
+
|
|
73
78
|
Elsewhere (outside a `component` body), JSX remains an expression, as in standard
|
|
74
79
|
JSX.
|
|
75
80
|
|
|
@@ -84,9 +89,9 @@ introduced — but framework compilers treat them as _reactive_ boundaries.
|
|
|
84
89
|
```tsx
|
|
85
90
|
component List(props: { items: Item[]; showHeader: boolean }) {
|
|
86
91
|
if (props.showHeader) {
|
|
87
|
-
<h1>
|
|
92
|
+
<h1>"Items"</h1>
|
|
88
93
|
} else {
|
|
89
|
-
<h2>
|
|
94
|
+
<h2>"(no header)"</h2>
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
for (const item of props.items) {
|
|
@@ -95,10 +100,10 @@ component List(props: { items: Item[]; showHeader: boolean }) {
|
|
|
95
100
|
|
|
96
101
|
switch (props.items.length) {
|
|
97
102
|
case 0:
|
|
98
|
-
<p>
|
|
103
|
+
<p>"empty"</p>
|
|
99
104
|
break;
|
|
100
105
|
default:
|
|
101
|
-
<p>
|
|
106
|
+
<p>"has items"</p>
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
try {
|
|
@@ -169,7 +174,7 @@ children).
|
|
|
169
174
|
|
|
170
175
|
```tsx
|
|
171
176
|
component Page() {
|
|
172
|
-
const header = <tsx><h1>
|
|
177
|
+
const header = <tsx><h1>Hello</h1></tsx>;
|
|
173
178
|
renderSomewhereElse(header);
|
|
174
179
|
}
|
|
175
180
|
```
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -137,7 +137,10 @@ export { sanitize_template_string as sanitizeTemplateString } from './utils/sani
|
|
|
137
137
|
export { escape } from './utils/escaping.js';
|
|
138
138
|
|
|
139
139
|
// Transform
|
|
140
|
-
export {
|
|
140
|
+
export {
|
|
141
|
+
createJsxTransform,
|
|
142
|
+
component_to_function_declaration as componentToFunctionDeclaration,
|
|
143
|
+
} from './transform/jsx/index.js';
|
|
141
144
|
export {
|
|
142
145
|
ensure_function_metadata as ensureFunctionMetadata,
|
|
143
146
|
in_jsx_child_context as inJsxChildContext,
|
package/src/plugin.js
CHANGED
|
@@ -175,6 +175,7 @@ export function TSRXPlugin(config) {
|
|
|
175
175
|
class TSRXParser extends Parser {
|
|
176
176
|
/** @type {AST.Node[]} */
|
|
177
177
|
#path = [];
|
|
178
|
+
#allowTagStartAfterDoubleQuotedText = false;
|
|
178
179
|
#commentContextId = 0;
|
|
179
180
|
#loose = false;
|
|
180
181
|
/** @type {import('../types/index').CompileError[] | undefined} */
|
|
@@ -558,6 +559,10 @@ export function TSRXPlugin(config) {
|
|
|
558
559
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
559
560
|
*/
|
|
560
561
|
getTokenFromCode(code) {
|
|
562
|
+
if (code !== 60) {
|
|
563
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
564
|
+
}
|
|
565
|
+
|
|
561
566
|
if (code === 60) {
|
|
562
567
|
// < character
|
|
563
568
|
const inComponent = this.#path.findLast((n) => n.type === 'Component');
|
|
@@ -634,8 +639,14 @@ export function TSRXPlugin(config) {
|
|
|
634
639
|
// Inside component template bodies, allow adjacent tags without requiring
|
|
635
640
|
// a newline/indentation before the next '<'. This is important for inputs
|
|
636
641
|
// like `<div />` and `</div><style>...</style>` which Prettier formats.
|
|
637
|
-
if (
|
|
642
|
+
if (
|
|
643
|
+
(prevNonWhitespaceChar === 34 /* '"' */ &&
|
|
644
|
+
this.#allowTagStartAfterDoubleQuotedText) ||
|
|
645
|
+
prevNonWhitespaceChar === 123 /* '{' */ ||
|
|
646
|
+
prevNonWhitespaceChar === 62 /* '>' */
|
|
647
|
+
) {
|
|
638
648
|
if (!isWhitespaceAfterLt) {
|
|
649
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
639
650
|
++this.pos;
|
|
640
651
|
return this.finishToken(tstt.jsxTagStart);
|
|
641
652
|
}
|
|
@@ -712,6 +723,7 @@ export function TSRXPlugin(config) {
|
|
|
712
723
|
}
|
|
713
724
|
}
|
|
714
725
|
}
|
|
726
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
715
727
|
return super.getTokenFromCode(code);
|
|
716
728
|
}
|
|
717
729
|
|
|
@@ -1300,6 +1312,30 @@ export function TSRXPlugin(config) {
|
|
|
1300
1312
|
);
|
|
1301
1313
|
}
|
|
1302
1314
|
|
|
1315
|
+
/**
|
|
1316
|
+
* @returns {AST.TextNode}
|
|
1317
|
+
*/
|
|
1318
|
+
parseDoubleQuotedTextChild() {
|
|
1319
|
+
const node = /** @type {AST.TextNode} */ (this.startNode());
|
|
1320
|
+
const expression = /** @type {AST.Literal} */ (this.startNode());
|
|
1321
|
+
const raw = this.input.slice(this.start, this.end);
|
|
1322
|
+
const end = this.end;
|
|
1323
|
+
const endLoc = this.endLoc;
|
|
1324
|
+
|
|
1325
|
+
expression.value = this.value;
|
|
1326
|
+
expression.raw = raw;
|
|
1327
|
+
node.expression = this.finishNodeAt(expression, 'Literal', end, endLoc);
|
|
1328
|
+
|
|
1329
|
+
this.#allowTagStartAfterDoubleQuotedText = true;
|
|
1330
|
+
try {
|
|
1331
|
+
this.next();
|
|
1332
|
+
} finally {
|
|
1333
|
+
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return this.finishNodeAt(node, 'Text', end, endLoc);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1303
1339
|
/**
|
|
1304
1340
|
* @type {Parse.Parser['jsx_parseAttribute']}
|
|
1305
1341
|
*/
|
|
@@ -2293,6 +2329,8 @@ export function TSRXPlugin(config) {
|
|
|
2293
2329
|
delete node.text;
|
|
2294
2330
|
}
|
|
2295
2331
|
body.push(node);
|
|
2332
|
+
} else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
|
|
2333
|
+
body.push(this.parseDoubleQuotedTextChild());
|
|
2296
2334
|
} else if (this.type === tt.braceR) {
|
|
2297
2335
|
// Leaving a component/template body. We may still be in TSX/JSX tokenization
|
|
2298
2336
|
// context (e.g. after parsing markup), but the closing `}` is a JS token.
|
|
@@ -123,6 +123,69 @@ export function tsx_with_ts_locations() {
|
|
|
123
123
|
context.write(': ');
|
|
124
124
|
context.visit(node.elementType);
|
|
125
125
|
},
|
|
126
|
+
// esrap's Property printer for method shorthand (`{ foo<T>() {} }`)
|
|
127
|
+
// does not visit `value.typeParameters`, so the `<T>` is dropped from
|
|
128
|
+
// the output and segments.js can't resolve the TSTypeParameterDeclaration's
|
|
129
|
+
// source position. Override only the actual method-shorthand branch —
|
|
130
|
+
// `{ foo: function() {} }` (`node.method === false`) and getters/setters
|
|
131
|
+
// must fall through to base.Property to preserve their printed form.
|
|
132
|
+
Property: (node, context) => {
|
|
133
|
+
if (!node.method || node.value.type !== 'FunctionExpression') {
|
|
134
|
+
base.Property(node, context);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const value = node.value;
|
|
138
|
+
if (value.async) context.write('async ');
|
|
139
|
+
if (value.generator) context.write('*');
|
|
140
|
+
if (node.computed) context.write('[');
|
|
141
|
+
context.visit(node.key);
|
|
142
|
+
if (node.computed) context.write(']');
|
|
143
|
+
if (value.typeParameters) {
|
|
144
|
+
context.visit(value.typeParameters);
|
|
145
|
+
}
|
|
146
|
+
context.write('(');
|
|
147
|
+
for (let i = 0; i < value.params.length; i++) {
|
|
148
|
+
if (i > 0) context.write(', ');
|
|
149
|
+
context.visit(value.params[i]);
|
|
150
|
+
}
|
|
151
|
+
context.write(')');
|
|
152
|
+
if (value.returnType) context.visit(value.returnType);
|
|
153
|
+
context.write(' ');
|
|
154
|
+
context.visit(value.body);
|
|
155
|
+
},
|
|
156
|
+
// esrap's ArrowFunctionExpression printer ignores `typeParameters` and
|
|
157
|
+
// `returnType`, so an annotated arrow like `(): Record<...> => ...`
|
|
158
|
+
// prints as `() => ...` and segments.js can't resolve the return-type
|
|
159
|
+
// nodes' positions in the generated output.
|
|
160
|
+
ArrowFunctionExpression: (node, context) => {
|
|
161
|
+
if (node.async) context.write('async ');
|
|
162
|
+
if (node.typeParameters) {
|
|
163
|
+
context.visit(node.typeParameters);
|
|
164
|
+
}
|
|
165
|
+
context.write('(');
|
|
166
|
+
for (let i = 0; i < node.params.length; i++) {
|
|
167
|
+
if (i > 0) context.write(', ');
|
|
168
|
+
context.visit(node.params[i]);
|
|
169
|
+
}
|
|
170
|
+
context.write(')');
|
|
171
|
+
if (node.returnType) {
|
|
172
|
+
context.visit(node.returnType);
|
|
173
|
+
}
|
|
174
|
+
context.write(' => ');
|
|
175
|
+
const body = node.body;
|
|
176
|
+
const wrap_body =
|
|
177
|
+
body.type === 'ObjectExpression' ||
|
|
178
|
+
(body.type === 'AssignmentExpression' && body.left.type === 'ObjectPattern') ||
|
|
179
|
+
(body.type === 'LogicalExpression' && body.left.type === 'ObjectExpression') ||
|
|
180
|
+
(body.type === 'ConditionalExpression' && body.test.type === 'ObjectExpression');
|
|
181
|
+
if (wrap_body) {
|
|
182
|
+
context.write('(');
|
|
183
|
+
context.visit(body);
|
|
184
|
+
context.write(')');
|
|
185
|
+
} else {
|
|
186
|
+
context.visit(body);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
126
189
|
};
|
|
127
190
|
for (const type of [
|
|
128
191
|
// JS nodes whose esrap printer emits no location marker, causing
|
|
@@ -138,6 +201,7 @@ export function tsx_with_ts_locations() {
|
|
|
138
201
|
'ForOfStatement',
|
|
139
202
|
'TemplateLiteral',
|
|
140
203
|
'AwaitExpression',
|
|
204
|
+
'SwitchStatement',
|
|
141
205
|
'TaggedTemplateExpression',
|
|
142
206
|
// JSX wrapper nodes: esrap writes `<`, `>`, `</`, `{`, `}` without
|
|
143
207
|
// locations, so the opening/closing element's and expression
|
|
@@ -54,6 +54,7 @@ import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
|
54
54
|
* available_bindings: Map<string, AST.Identifier>,
|
|
55
55
|
* lazy_next_id: number,
|
|
56
56
|
* current_css_hash: string | null,
|
|
57
|
+
* inside_element_child?: boolean,
|
|
57
58
|
* }} TransformContext
|
|
58
59
|
*/
|
|
59
60
|
|
|
@@ -186,7 +187,9 @@ export function createJsxTransform(platform) {
|
|
|
186
187
|
// the names their body actually references before generating props.
|
|
187
188
|
const body_bindings = collect_param_bindings(as_any.params || []);
|
|
188
189
|
const body = as_any.body || [];
|
|
189
|
-
|
|
190
|
+
const split_index = find_hook_safe_split_index(body, state);
|
|
191
|
+
const collect_end = split_index === -1 ? body.length : split_index;
|
|
192
|
+
for (let i = 0; i < collect_end; i += 1) {
|
|
190
193
|
collect_statement_bindings(body[i], body_bindings);
|
|
191
194
|
}
|
|
192
195
|
state.available_bindings = body_bindings;
|
|
@@ -223,7 +226,7 @@ export function createJsxTransform(platform) {
|
|
|
223
226
|
const inner = /** @type {any} */ (next() ?? node);
|
|
224
227
|
const hook = platform.hooks?.transformElement;
|
|
225
228
|
if (hook) return /** @type {any} */ (hook(inner, state, raw_children));
|
|
226
|
-
return /** @type {any} */ (to_jsx_element(inner, state));
|
|
229
|
+
return /** @type {any} */ (to_jsx_element(inner, state, raw_children));
|
|
227
230
|
},
|
|
228
231
|
|
|
229
232
|
Text(node, { next }) {
|
|
@@ -326,12 +329,18 @@ function has_use_server_directive(program) {
|
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
/**
|
|
332
|
+
* Lower a TSRX `Component` node into the shared function-declaration form used
|
|
333
|
+
* by the default JSX targets. Platform hooks can reuse this helper and wrap the
|
|
334
|
+
* resulting function in another declaration shape without reimplementing
|
|
335
|
+
* component body lowering, lazy destructuring, helper generation, or top-level
|
|
336
|
+
* await handling.
|
|
337
|
+
*
|
|
329
338
|
* @param {any} component
|
|
330
339
|
* @param {TransformContext} transform_context
|
|
331
340
|
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
|
|
332
341
|
* @returns {AST.FunctionDeclaration}
|
|
333
342
|
*/
|
|
334
|
-
function component_to_function_declaration(component, transform_context, walk_helper_state) {
|
|
343
|
+
export function component_to_function_declaration(component, transform_context, walk_helper_state) {
|
|
335
344
|
const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
|
|
336
345
|
const params = component.params || [];
|
|
337
346
|
const body = /** @type {any[]} */ (component.body || []);
|
|
@@ -443,10 +452,31 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
443
452
|
}
|
|
444
453
|
|
|
445
454
|
if (is_returning_if_statement(child)) {
|
|
446
|
-
const branch_has_hooks = body_contains_top_level_hook_call(
|
|
447
|
-
|
|
455
|
+
const branch_has_hooks = body_contains_top_level_hook_call(
|
|
456
|
+
get_if_consequent_body(child),
|
|
457
|
+
transform_context,
|
|
458
|
+
true,
|
|
459
|
+
);
|
|
460
|
+
const continuation_has_hooks = body_contains_top_level_hook_call(
|
|
461
|
+
body_nodes.slice(i + 1),
|
|
462
|
+
transform_context,
|
|
463
|
+
true,
|
|
464
|
+
);
|
|
448
465
|
|
|
449
466
|
if (branch_has_hooks || continuation_has_hooks) {
|
|
467
|
+
if (transform_context.platform.hooks?.isTopLevelSetupCall) {
|
|
468
|
+
statements.push(
|
|
469
|
+
...create_setup_once_helper_split_returning_if_statements(
|
|
470
|
+
child,
|
|
471
|
+
body_nodes.slice(i + 1),
|
|
472
|
+
render_nodes,
|
|
473
|
+
transform_context,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
transform_context.available_bindings = saved_bindings;
|
|
477
|
+
return statements;
|
|
478
|
+
}
|
|
479
|
+
|
|
450
480
|
statements.push(
|
|
451
481
|
...create_component_helper_split_returning_if_statements(
|
|
452
482
|
child,
|
|
@@ -460,6 +490,83 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
460
490
|
}
|
|
461
491
|
|
|
462
492
|
if (is_lone_return_if_statement(child)) {
|
|
493
|
+
// On platforms where setup runs once (Vue Vapor), an early
|
|
494
|
+
// `if (cond) return;` placed at setup level is non-reactive:
|
|
495
|
+
// `cond` is evaluated only when setup runs and never again.
|
|
496
|
+
// Inline the rest of the body as a render-time ternary so the
|
|
497
|
+
// conditional re-evaluates when `cond` changes after mount.
|
|
498
|
+
// React/Preact/Solid re-run the component body on every render,
|
|
499
|
+
// so the old setup-time early return is already reactive there
|
|
500
|
+
// and we keep it to avoid gratuitous output changes.
|
|
501
|
+
if (transform_context.platform.hooks?.isTopLevelSetupCall) {
|
|
502
|
+
const continuation_body = body_nodes.slice(i + 1);
|
|
503
|
+
|
|
504
|
+
// Render-time inlining unconditionally lifts continuation
|
|
505
|
+
// statements (provide/watch/declarations/etc.) into the
|
|
506
|
+
// parent setup, which would run them regardless of the
|
|
507
|
+
// early-return condition — wrong when the user wrote them
|
|
508
|
+
// after `if (cond) return;`. Fall back to helper-split if
|
|
509
|
+
// the continuation has any non-render statements so they
|
|
510
|
+
// stay scoped to the helper's lifecycle.
|
|
511
|
+
const continuation_has_setup_statements = continuation_body.some(
|
|
512
|
+
(node) =>
|
|
513
|
+
!is_bare_return_statement(node) &&
|
|
514
|
+
!is_returning_if_statement(node) &&
|
|
515
|
+
!is_jsx_child(node),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (continuation_has_setup_statements) {
|
|
519
|
+
statements.push(
|
|
520
|
+
...create_setup_once_helper_split_returning_if_statements(
|
|
521
|
+
child,
|
|
522
|
+
continuation_body,
|
|
523
|
+
render_nodes,
|
|
524
|
+
transform_context,
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
transform_context.available_bindings = saved_bindings;
|
|
528
|
+
return statements;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const continuation_statements = build_render_statements(
|
|
532
|
+
continuation_body,
|
|
533
|
+
false,
|
|
534
|
+
transform_context,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
for (const stmt of continuation_statements) {
|
|
538
|
+
if (stmt.type === 'ReturnStatement') {
|
|
539
|
+
if (stmt.argument) {
|
|
540
|
+
render_nodes.push(
|
|
541
|
+
/** @type {any} */ ({
|
|
542
|
+
type: 'JSXExpressionContainer',
|
|
543
|
+
expression: set_loc(
|
|
544
|
+
/** @type {any} */ ({
|
|
545
|
+
type: 'ConditionalExpression',
|
|
546
|
+
test: clone_expression_node(child.test),
|
|
547
|
+
consequent: {
|
|
548
|
+
type: 'Literal',
|
|
549
|
+
value: null,
|
|
550
|
+
raw: 'null',
|
|
551
|
+
metadata: { path: [] },
|
|
552
|
+
},
|
|
553
|
+
alternate: stmt.argument,
|
|
554
|
+
metadata: { path: [] },
|
|
555
|
+
}),
|
|
556
|
+
child,
|
|
557
|
+
),
|
|
558
|
+
metadata: { path: [] },
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
statements.push(stmt);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
|
|
463
570
|
statements.push(create_component_lone_return_if_statement(child, render_nodes));
|
|
464
571
|
continue;
|
|
465
572
|
}
|
|
@@ -519,26 +626,62 @@ function is_interleaved_body(body_nodes) {
|
|
|
519
626
|
|
|
520
627
|
/**
|
|
521
628
|
* @param {any[]} body_nodes
|
|
629
|
+
* @param {TransformContext} transform_context
|
|
630
|
+
* @returns {number}
|
|
631
|
+
*/
|
|
632
|
+
function find_hook_safe_split_index(body_nodes, transform_context) {
|
|
633
|
+
for (let i = 0; i < body_nodes.length; i += 1) {
|
|
634
|
+
if (!is_lone_return_if_statement(body_nodes[i])) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (body_contains_top_level_hook_call(body_nodes.slice(i + 1), transform_context, true)) {
|
|
639
|
+
return i;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return -1;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* @param {any[]} body_nodes
|
|
648
|
+
* @param {TransformContext} transform_context
|
|
649
|
+
* @param {boolean} include_platform_setup
|
|
522
650
|
* @returns {boolean}
|
|
523
651
|
*/
|
|
524
|
-
function body_contains_top_level_hook_call(
|
|
525
|
-
|
|
652
|
+
function body_contains_top_level_hook_call(
|
|
653
|
+
body_nodes,
|
|
654
|
+
transform_context,
|
|
655
|
+
include_platform_setup = false,
|
|
656
|
+
) {
|
|
657
|
+
return body_nodes.some((node) =>
|
|
658
|
+
statement_contains_top_level_hook_call(node, transform_context, include_platform_setup),
|
|
659
|
+
);
|
|
526
660
|
}
|
|
527
661
|
|
|
528
662
|
/**
|
|
529
663
|
* @param {any} node
|
|
664
|
+
* @param {TransformContext} transform_context
|
|
665
|
+
* @param {boolean} include_platform_setup
|
|
530
666
|
* @returns {boolean}
|
|
531
667
|
*/
|
|
532
|
-
function statement_contains_top_level_hook_call(node) {
|
|
533
|
-
return node_contains_top_level_hook_call(node, false);
|
|
668
|
+
function statement_contains_top_level_hook_call(node, transform_context, include_platform_setup) {
|
|
669
|
+
return node_contains_top_level_hook_call(node, false, transform_context, include_platform_setup);
|
|
534
670
|
}
|
|
535
671
|
|
|
536
672
|
/**
|
|
537
673
|
* @param {any} node
|
|
538
674
|
* @param {boolean} inside_nested_function
|
|
675
|
+
* @param {TransformContext} transform_context
|
|
676
|
+
* @param {boolean} include_platform_setup
|
|
539
677
|
* @returns {boolean}
|
|
540
678
|
*/
|
|
541
|
-
function node_contains_top_level_hook_call(
|
|
679
|
+
function node_contains_top_level_hook_call(
|
|
680
|
+
node,
|
|
681
|
+
inside_nested_function,
|
|
682
|
+
transform_context,
|
|
683
|
+
include_platform_setup,
|
|
684
|
+
) {
|
|
542
685
|
if (!node || typeof node !== 'object') {
|
|
543
686
|
return false;
|
|
544
687
|
}
|
|
@@ -562,26 +705,53 @@ function node_contains_top_level_hook_call(node, inside_nested_function) {
|
|
|
562
705
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
563
706
|
continue;
|
|
564
707
|
}
|
|
565
|
-
if (
|
|
708
|
+
if (
|
|
709
|
+
node_contains_top_level_hook_call(
|
|
710
|
+
node[key],
|
|
711
|
+
next_inside_nested_function,
|
|
712
|
+
transform_context,
|
|
713
|
+
include_platform_setup,
|
|
714
|
+
)
|
|
715
|
+
) {
|
|
566
716
|
return true;
|
|
567
717
|
}
|
|
568
718
|
}
|
|
569
719
|
return false;
|
|
570
720
|
}
|
|
571
721
|
|
|
572
|
-
if (
|
|
722
|
+
if (
|
|
723
|
+
!inside_nested_function &&
|
|
724
|
+
node.type === 'CallExpression' &&
|
|
725
|
+
(is_hook_callee(node.callee) ||
|
|
726
|
+
(include_platform_setup &&
|
|
727
|
+
transform_context.platform.hooks?.isTopLevelSetupCall?.(node, transform_context) === true))
|
|
728
|
+
) {
|
|
573
729
|
return true;
|
|
574
730
|
}
|
|
575
731
|
|
|
576
732
|
if (Array.isArray(node)) {
|
|
577
|
-
return node.some((child) =>
|
|
733
|
+
return node.some((child) =>
|
|
734
|
+
node_contains_top_level_hook_call(
|
|
735
|
+
child,
|
|
736
|
+
inside_nested_function,
|
|
737
|
+
transform_context,
|
|
738
|
+
include_platform_setup,
|
|
739
|
+
),
|
|
740
|
+
);
|
|
578
741
|
}
|
|
579
742
|
|
|
580
743
|
for (const key of Object.keys(node)) {
|
|
581
744
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
582
745
|
continue;
|
|
583
746
|
}
|
|
584
|
-
if (
|
|
747
|
+
if (
|
|
748
|
+
node_contains_top_level_hook_call(
|
|
749
|
+
node[key],
|
|
750
|
+
inside_nested_function,
|
|
751
|
+
transform_context,
|
|
752
|
+
include_platform_setup,
|
|
753
|
+
)
|
|
754
|
+
) {
|
|
585
755
|
return true;
|
|
586
756
|
}
|
|
587
757
|
}
|
|
@@ -863,6 +1033,12 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
|
863
1033
|
const node = render_nodes[i];
|
|
864
1034
|
if (node.type !== 'JSXElement') continue;
|
|
865
1035
|
if (!is_hoist_safe_jsx_node(node)) continue;
|
|
1036
|
+
if (
|
|
1037
|
+
transform_context.platform.hooks?.canHoistStaticNode &&
|
|
1038
|
+
!transform_context.platform.hooks.canHoistStaticNode(node, transform_context)
|
|
1039
|
+
) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
866
1042
|
if (references_scope_bindings(node, transform_context.available_bindings)) continue;
|
|
867
1043
|
|
|
868
1044
|
const name = create_helper_name(transform_context.helper_state, 'static');
|
|
@@ -894,26 +1070,11 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
|
894
1070
|
*/
|
|
895
1071
|
function expand_component_helpers(program) {
|
|
896
1072
|
program.body = program.body.flatMap((statement) => {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
return [...statics, ...helpers, statement];
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
(statement.type === 'ExportNamedDeclaration' ||
|
|
908
|
-
statement.type === 'ExportDefaultDeclaration') &&
|
|
909
|
-
statement.declaration?.type === 'FunctionDeclaration'
|
|
910
|
-
) {
|
|
911
|
-
const meta = /** @type {any} */ (statement.declaration.metadata);
|
|
912
|
-
const statics = meta?.generated_statics || [];
|
|
913
|
-
const helpers = meta?.generated_helpers || [];
|
|
914
|
-
if (statics.length || helpers.length) {
|
|
915
|
-
return [...statics, ...helpers, statement];
|
|
916
|
-
}
|
|
1073
|
+
const meta = get_generated_component_metadata(statement);
|
|
1074
|
+
const statics = meta?.generated_statics || [];
|
|
1075
|
+
const helpers = meta?.generated_helpers || [];
|
|
1076
|
+
if (statics.length || helpers.length) {
|
|
1077
|
+
return [...statics, ...helpers, statement];
|
|
917
1078
|
}
|
|
918
1079
|
|
|
919
1080
|
return [statement];
|
|
@@ -922,6 +1083,34 @@ function expand_component_helpers(program) {
|
|
|
922
1083
|
return program;
|
|
923
1084
|
}
|
|
924
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Component hooks may replace a `Component` node with a function declaration,
|
|
1088
|
+
* variable declaration, or export-safe expression. Generated helper/statics
|
|
1089
|
+
* metadata is carried on whichever replacement node the hook returns, so
|
|
1090
|
+
* helper expansion must read metadata from that broader set.
|
|
1091
|
+
*
|
|
1092
|
+
* @param {any} node
|
|
1093
|
+
* @returns {{ generated_helpers?: any[], generated_statics?: any[] } | null}
|
|
1094
|
+
*/
|
|
1095
|
+
function get_generated_component_metadata(node) {
|
|
1096
|
+
if (!node || typeof node !== 'object') {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (node.metadata?.generated_helpers || node.metadata?.generated_statics) {
|
|
1101
|
+
return node.metadata;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (
|
|
1105
|
+
(node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') &&
|
|
1106
|
+
node.declaration?.metadata
|
|
1107
|
+
) {
|
|
1108
|
+
return node.declaration.metadata;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
925
1114
|
/**
|
|
926
1115
|
* @param {any} node
|
|
927
1116
|
* @returns {boolean}
|
|
@@ -1119,6 +1308,53 @@ function create_component_helper_split_returning_if_statements(
|
|
|
1119
1308
|
];
|
|
1120
1309
|
}
|
|
1121
1310
|
|
|
1311
|
+
/**
|
|
1312
|
+
* @param {any} node
|
|
1313
|
+
* @param {any[]} continuation_body
|
|
1314
|
+
* @param {any[]} render_nodes
|
|
1315
|
+
* @param {TransformContext} transform_context
|
|
1316
|
+
* @returns {any[]}
|
|
1317
|
+
*/
|
|
1318
|
+
function create_setup_once_helper_split_returning_if_statements(
|
|
1319
|
+
node,
|
|
1320
|
+
continuation_body,
|
|
1321
|
+
render_nodes,
|
|
1322
|
+
transform_context,
|
|
1323
|
+
) {
|
|
1324
|
+
const consequent_body = get_if_consequent_body(node);
|
|
1325
|
+
const return_index = consequent_body.findIndex(is_bare_return_statement);
|
|
1326
|
+
const branch_body =
|
|
1327
|
+
return_index === -1 ? consequent_body : consequent_body.slice(0, return_index);
|
|
1328
|
+
const branch_helper = branch_body.length
|
|
1329
|
+
? create_hook_safe_helper(branch_body, undefined, node.consequent, transform_context)
|
|
1330
|
+
: { setup_statements: [], component_element: create_null_literal() };
|
|
1331
|
+
const continuation_helper = continuation_body.length
|
|
1332
|
+
? create_hook_safe_helper(continuation_body, undefined, node, transform_context)
|
|
1333
|
+
: { setup_statements: [], component_element: create_null_literal() };
|
|
1334
|
+
|
|
1335
|
+
return [
|
|
1336
|
+
...branch_helper.setup_statements,
|
|
1337
|
+
...continuation_helper.setup_statements,
|
|
1338
|
+
{
|
|
1339
|
+
type: 'ReturnStatement',
|
|
1340
|
+
argument: combine_render_return_argument(
|
|
1341
|
+
render_nodes,
|
|
1342
|
+
set_loc(
|
|
1343
|
+
/** @type {any} */ ({
|
|
1344
|
+
type: 'ConditionalExpression',
|
|
1345
|
+
test: node.test,
|
|
1346
|
+
consequent: branch_helper.component_element,
|
|
1347
|
+
alternate: continuation_helper.component_element,
|
|
1348
|
+
metadata: { path: [] },
|
|
1349
|
+
}),
|
|
1350
|
+
node,
|
|
1351
|
+
),
|
|
1352
|
+
),
|
|
1353
|
+
metadata: { path: [] },
|
|
1354
|
+
},
|
|
1355
|
+
];
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1122
1358
|
/**
|
|
1123
1359
|
* @param {any[]} statements
|
|
1124
1360
|
* @param {any[]} render_nodes
|
|
@@ -1244,13 +1480,8 @@ const TEMPLATE_FRAGMENT_ERROR =
|
|
|
1244
1480
|
* @param {TransformContext} transform_context
|
|
1245
1481
|
* @returns {any}
|
|
1246
1482
|
*/
|
|
1247
|
-
function to_jsx_element(node, transform_context) {
|
|
1483
|
+
function to_jsx_element(node, transform_context, raw_children = node.children || []) {
|
|
1248
1484
|
if (node.type === 'JSXElement') return node;
|
|
1249
|
-
if ((node.children || []).some((/** @type {any} */ c) => c && c.type === 'Html')) {
|
|
1250
|
-
throw new Error(
|
|
1251
|
-
`\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
1485
|
if (!node.id) {
|
|
1255
1486
|
throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
|
|
1256
1487
|
}
|
|
@@ -1264,8 +1495,30 @@ function to_jsx_element(node, transform_context) {
|
|
|
1264
1495
|
transform_context,
|
|
1265
1496
|
node,
|
|
1266
1497
|
);
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1498
|
+
const walked_children = node.children || [];
|
|
1499
|
+
let selfClosing = !!node.selfClosing;
|
|
1500
|
+
let children;
|
|
1501
|
+
const child_transform = transform_context.platform.hooks?.transformElementChildren?.(
|
|
1502
|
+
node,
|
|
1503
|
+
walked_children,
|
|
1504
|
+
raw_children,
|
|
1505
|
+
attributes,
|
|
1506
|
+
transform_context,
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
if (child_transform) {
|
|
1510
|
+
children = child_transform.children;
|
|
1511
|
+
if (typeof child_transform.selfClosing === 'boolean') {
|
|
1512
|
+
selfClosing = child_transform.selfClosing;
|
|
1513
|
+
}
|
|
1514
|
+
} else {
|
|
1515
|
+
if (walked_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
`\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
children = create_element_children(walked_children, transform_context);
|
|
1521
|
+
}
|
|
1269
1522
|
const has_unmappable_attribute = attributes.some(
|
|
1270
1523
|
(/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
|
|
1271
1524
|
);
|
|
@@ -1325,10 +1578,22 @@ function create_element_children(children, transform_context) {
|
|
|
1325
1578
|
}
|
|
1326
1579
|
|
|
1327
1580
|
if (children.every(is_inline_element_child) && !children_contain_return_semantics(children)) {
|
|
1328
|
-
|
|
1581
|
+
const saved_inside_element_child = transform_context.inside_element_child;
|
|
1582
|
+
transform_context.inside_element_child = true;
|
|
1583
|
+
try {
|
|
1584
|
+
return children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context));
|
|
1585
|
+
} finally {
|
|
1586
|
+
transform_context.inside_element_child = saved_inside_element_child;
|
|
1587
|
+
}
|
|
1329
1588
|
}
|
|
1330
1589
|
|
|
1331
|
-
|
|
1590
|
+
const saved_inside_element_child = transform_context.inside_element_child;
|
|
1591
|
+
transform_context.inside_element_child = true;
|
|
1592
|
+
try {
|
|
1593
|
+
return [statement_body_to_jsx_child(children, transform_context)];
|
|
1594
|
+
} finally {
|
|
1595
|
+
transform_context.inside_element_child = saved_inside_element_child;
|
|
1596
|
+
}
|
|
1332
1597
|
}
|
|
1333
1598
|
|
|
1334
1599
|
/**
|
|
@@ -1391,7 +1656,7 @@ function is_inline_element_child(node) {
|
|
|
1391
1656
|
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1392
1657
|
*/
|
|
1393
1658
|
function statement_body_to_jsx_child(body_nodes, transform_context) {
|
|
1394
|
-
if (body_contains_top_level_hook_call(body_nodes)) {
|
|
1659
|
+
if (body_contains_top_level_hook_call(body_nodes, transform_context, true)) {
|
|
1395
1660
|
return hook_safe_statement_body_to_jsx_child(body_nodes, transform_context);
|
|
1396
1661
|
}
|
|
1397
1662
|
|
|
@@ -1570,7 +1835,7 @@ function create_hook_safe_helper(body_nodes, key_expression, source_node, transf
|
|
|
1570
1835
|
return {
|
|
1571
1836
|
setup_statements: [
|
|
1572
1837
|
...aliases.map((alias) => alias.declaration),
|
|
1573
|
-
|
|
1838
|
+
create_helper_declaration(helper_id, helper_fn, source_node, transform_context),
|
|
1574
1839
|
],
|
|
1575
1840
|
component_element,
|
|
1576
1841
|
};
|
|
@@ -1584,12 +1849,54 @@ function create_hook_safe_helper(body_nodes, key_expression, source_node, transf
|
|
|
1584
1849
|
return {
|
|
1585
1850
|
setup_statements: [
|
|
1586
1851
|
...aliases.map((alias) => alias.declaration),
|
|
1587
|
-
create_cached_helper_declaration(
|
|
1852
|
+
create_cached_helper_declaration(
|
|
1853
|
+
helper_id,
|
|
1854
|
+
cache_id,
|
|
1855
|
+
create_helper_init_expression(helper_id, helper_fn, source_node, transform_context),
|
|
1856
|
+
),
|
|
1588
1857
|
],
|
|
1589
1858
|
component_element,
|
|
1590
1859
|
};
|
|
1591
1860
|
}
|
|
1592
1861
|
|
|
1862
|
+
/**
|
|
1863
|
+
* @param {AST.Identifier} helper_id
|
|
1864
|
+
* @param {any} helper_fn
|
|
1865
|
+
* @param {any} source_node
|
|
1866
|
+
* @param {TransformContext} transform_context
|
|
1867
|
+
* @returns {any}
|
|
1868
|
+
*/
|
|
1869
|
+
function create_helper_declaration(helper_id, helper_fn, source_node, transform_context) {
|
|
1870
|
+
const declaration = create_helper_function_declaration_from_expression(helper_id, helper_fn);
|
|
1871
|
+
const hook = transform_context.platform.hooks?.wrapHelperComponent;
|
|
1872
|
+
return hook ? hook(declaration, helper_id, transform_context, source_node) : declaration;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* @param {AST.Identifier} helper_id
|
|
1877
|
+
* @param {any} helper_fn
|
|
1878
|
+
* @param {any} source_node
|
|
1879
|
+
* @param {TransformContext} transform_context
|
|
1880
|
+
* @returns {any}
|
|
1881
|
+
*/
|
|
1882
|
+
function create_helper_init_expression(helper_id, helper_fn, source_node, transform_context) {
|
|
1883
|
+
const hook = transform_context.platform.hooks?.wrapHelperComponent;
|
|
1884
|
+
if (!hook) return helper_fn;
|
|
1885
|
+
|
|
1886
|
+
const declaration = hook(
|
|
1887
|
+
create_helper_function_declaration_from_expression(helper_id, helper_fn),
|
|
1888
|
+
helper_id,
|
|
1889
|
+
transform_context,
|
|
1890
|
+
source_node,
|
|
1891
|
+
);
|
|
1892
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
1893
|
+
const init = declaration.declarations?.[0]?.init;
|
|
1894
|
+
if (init) return init;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
return helper_fn;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1593
1900
|
/**
|
|
1594
1901
|
* @param {any[]} setup_statements
|
|
1595
1902
|
* @param {ESTreeJSX.JSXElement} component_element
|
|
@@ -1723,10 +2030,10 @@ function create_helper_cache_declaration(cache_id) {
|
|
|
1723
2030
|
/**
|
|
1724
2031
|
* @param {AST.Identifier} helper_id
|
|
1725
2032
|
* @param {AST.Identifier} cache_id
|
|
1726
|
-
* @param {any}
|
|
2033
|
+
* @param {any} helper_init
|
|
1727
2034
|
* @returns {any}
|
|
1728
2035
|
*/
|
|
1729
|
-
function create_cached_helper_declaration(helper_id, cache_id,
|
|
2036
|
+
function create_cached_helper_declaration(helper_id, cache_id, helper_init) {
|
|
1730
2037
|
return /** @type {any} */ ({
|
|
1731
2038
|
type: 'VariableDeclaration',
|
|
1732
2039
|
kind: 'const',
|
|
@@ -1742,7 +2049,7 @@ function create_cached_helper_declaration(helper_id, cache_id, helper_fn) {
|
|
|
1742
2049
|
type: 'AssignmentExpression',
|
|
1743
2050
|
operator: '=',
|
|
1744
2051
|
left: clone_identifier(cache_id),
|
|
1745
|
-
right:
|
|
2052
|
+
right: helper_init,
|
|
1746
2053
|
metadata: { path: [] },
|
|
1747
2054
|
},
|
|
1748
2055
|
metadata: { path: [] },
|
|
@@ -1841,6 +2148,12 @@ function to_jsx_child(node, transform_context) {
|
|
|
1841
2148
|
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1842
2149
|
*/
|
|
1843
2150
|
function if_statement_to_jsx_child(node, transform_context) {
|
|
2151
|
+
const render_if_statement = create_render_if_statement(node, transform_context);
|
|
2152
|
+
const conditional_expression = render_if_statement_to_conditional_expression(render_if_statement);
|
|
2153
|
+
if (conditional_expression) {
|
|
2154
|
+
return to_jsx_expression_container(conditional_expression, node);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
1844
2157
|
return to_jsx_expression_container(
|
|
1845
2158
|
/** @type {any} */ ({
|
|
1846
2159
|
type: 'CallExpression',
|
|
@@ -1849,10 +2162,7 @@ function if_statement_to_jsx_child(node, transform_context) {
|
|
|
1849
2162
|
params: [],
|
|
1850
2163
|
body: /** @type {any} */ ({
|
|
1851
2164
|
type: 'BlockStatement',
|
|
1852
|
-
body: [
|
|
1853
|
-
create_render_if_statement(node, transform_context),
|
|
1854
|
-
create_null_return_statement(),
|
|
1855
|
-
],
|
|
2165
|
+
body: [render_if_statement, create_null_return_statement()],
|
|
1856
2166
|
metadata: { path: [] },
|
|
1857
2167
|
}),
|
|
1858
2168
|
async: false,
|
|
@@ -1867,6 +2177,61 @@ function if_statement_to_jsx_child(node, transform_context) {
|
|
|
1867
2177
|
);
|
|
1868
2178
|
}
|
|
1869
2179
|
|
|
2180
|
+
/**
|
|
2181
|
+
* @param {any} node
|
|
2182
|
+
* @returns {any | null}
|
|
2183
|
+
*/
|
|
2184
|
+
function render_if_statement_to_conditional_expression(node) {
|
|
2185
|
+
if (!node || node.type !== 'IfStatement') return null;
|
|
2186
|
+
|
|
2187
|
+
const consequent = block_statement_to_return_expression(node.consequent);
|
|
2188
|
+
if (!consequent) return null;
|
|
2189
|
+
|
|
2190
|
+
let alternate = create_null_literal();
|
|
2191
|
+
if (node.alternate) {
|
|
2192
|
+
if (node.alternate.type === 'IfStatement') {
|
|
2193
|
+
alternate = render_if_statement_to_conditional_expression(node.alternate);
|
|
2194
|
+
if (!alternate) return null;
|
|
2195
|
+
} else {
|
|
2196
|
+
alternate = block_statement_to_return_expression(node.alternate);
|
|
2197
|
+
if (!alternate) return null;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
return set_loc(
|
|
2202
|
+
/** @type {any} */ ({
|
|
2203
|
+
type: 'ConditionalExpression',
|
|
2204
|
+
test: node.test,
|
|
2205
|
+
consequent,
|
|
2206
|
+
alternate,
|
|
2207
|
+
metadata: { path: [] },
|
|
2208
|
+
}),
|
|
2209
|
+
node,
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
/**
|
|
2214
|
+
* @param {any} block
|
|
2215
|
+
* @returns {any | null}
|
|
2216
|
+
*/
|
|
2217
|
+
function block_statement_to_return_expression(block) {
|
|
2218
|
+
if (!block || block.type !== 'BlockStatement' || block.body.length === 0) {
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
const statement = block.body[block.body.length - 1];
|
|
2223
|
+
if (!statement || statement.type !== 'ReturnStatement') {
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const argument = statement.argument || create_null_literal();
|
|
2228
|
+
if (block.body.length === 1) {
|
|
2229
|
+
return argument;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
return create_hook_safe_helper_iife(block.body.slice(0, -1), argument);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1870
2235
|
/**
|
|
1871
2236
|
* Find the first `key` attribute expression in the top-level elements of a body.
|
|
1872
2237
|
* Used to propagate keys from loop body elements to wrapper components.
|
|
@@ -1923,7 +2288,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
1923
2288
|
|
|
1924
2289
|
const loop_params = get_for_of_iteration_params(node.left, node.index);
|
|
1925
2290
|
const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
|
|
1926
|
-
const has_hooks = body_contains_top_level_hook_call(loop_body);
|
|
2291
|
+
const has_hooks = body_contains_top_level_hook_call(loop_body, transform_context, true);
|
|
1927
2292
|
const body_key_expression = find_key_expression_in_body(loop_body);
|
|
1928
2293
|
const explicit_key_expression =
|
|
1929
2294
|
body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
|
|
@@ -1955,6 +2320,17 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
1955
2320
|
apply_key_to_render_statements(body_statements, implicit_non_hook_key_expression);
|
|
1956
2321
|
}
|
|
1957
2322
|
|
|
2323
|
+
const platform_for_of = transform_context.platform.hooks?.renderForOf?.(
|
|
2324
|
+
node,
|
|
2325
|
+
loop_params,
|
|
2326
|
+
body_statements,
|
|
2327
|
+
transform_context,
|
|
2328
|
+
);
|
|
2329
|
+
if (platform_for_of) {
|
|
2330
|
+
transform_context.available_bindings = saved_bindings;
|
|
2331
|
+
return platform_for_of;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
1958
2334
|
// Restore bindings
|
|
1959
2335
|
transform_context.available_bindings = saved_bindings;
|
|
1960
2336
|
|
|
@@ -2070,7 +2446,7 @@ function switch_statement_to_jsx_child(node, transform_context) {
|
|
|
2070
2446
|
* - `pending` → `<Suspense fallback={...}>`
|
|
2071
2447
|
* - `catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
|
|
2072
2448
|
* - both → ErrorBoundary wraps Suspense
|
|
2073
|
-
* - `finally`
|
|
2449
|
+
* - JavaScript `try/finally` is not part of component template control flow
|
|
2074
2450
|
*
|
|
2075
2451
|
* @param {any} node
|
|
2076
2452
|
* @param {TransformContext} transform_context
|
|
@@ -2084,7 +2460,7 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2084
2460
|
if (finalizer) {
|
|
2085
2461
|
throw create_compile_error(
|
|
2086
2462
|
finalizer,
|
|
2087
|
-
`${transform_context.platform.name} TSRX does not support \`finally\`
|
|
2463
|
+
`${transform_context.platform.name} TSRX does not support JavaScript \`try/finally\` in component templates. \`finally\` is not part of TSRX control flow; move the try/finally into a function if you need cleanup logic.`,
|
|
2088
2464
|
);
|
|
2089
2465
|
}
|
|
2090
2466
|
|
|
@@ -2095,6 +2471,13 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2095
2471
|
);
|
|
2096
2472
|
}
|
|
2097
2473
|
|
|
2474
|
+
if (pending && transform_context.platform.validation.unsupportedTryPendingMessage) {
|
|
2475
|
+
throw create_compile_error(
|
|
2476
|
+
pending,
|
|
2477
|
+
transform_context.platform.validation.unsupportedTryPendingMessage,
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2098
2481
|
// Validate that try body contains JSX if pending block is present
|
|
2099
2482
|
if (pending) {
|
|
2100
2483
|
const try_body = node.block.body || [];
|
|
@@ -2182,6 +2565,54 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2182
2565
|
|
|
2183
2566
|
transform_context.available_bindings = saved_catch_bindings;
|
|
2184
2567
|
|
|
2568
|
+
const boundary_content =
|
|
2569
|
+
transform_context.platform.hooks?.createErrorBoundaryContent?.(
|
|
2570
|
+
try_content,
|
|
2571
|
+
transform_context,
|
|
2572
|
+
node,
|
|
2573
|
+
) ?? null;
|
|
2574
|
+
|
|
2575
|
+
if (boundary_content && transform_context.inside_element_child) {
|
|
2576
|
+
result = to_jsx_expression_container(
|
|
2577
|
+
/** @type {any} */ ({
|
|
2578
|
+
type: 'CallExpression',
|
|
2579
|
+
callee: { type: 'Identifier', name: 'TsrxErrorBoundary', metadata: { path: [] } },
|
|
2580
|
+
arguments: [
|
|
2581
|
+
{
|
|
2582
|
+
type: 'ObjectExpression',
|
|
2583
|
+
properties: [
|
|
2584
|
+
{
|
|
2585
|
+
type: 'Property',
|
|
2586
|
+
key: { type: 'Identifier', name: 'fallback', metadata: { path: [] } },
|
|
2587
|
+
value: fallback_fn,
|
|
2588
|
+
kind: 'init',
|
|
2589
|
+
method: false,
|
|
2590
|
+
shorthand: false,
|
|
2591
|
+
computed: false,
|
|
2592
|
+
metadata: { path: [] },
|
|
2593
|
+
},
|
|
2594
|
+
{
|
|
2595
|
+
type: 'Property',
|
|
2596
|
+
key: { type: 'Identifier', name: 'content', metadata: { path: [] } },
|
|
2597
|
+
value: boundary_content,
|
|
2598
|
+
kind: 'init',
|
|
2599
|
+
method: false,
|
|
2600
|
+
shorthand: false,
|
|
2601
|
+
computed: false,
|
|
2602
|
+
metadata: { path: [] },
|
|
2603
|
+
},
|
|
2604
|
+
],
|
|
2605
|
+
metadata: { path: [] },
|
|
2606
|
+
},
|
|
2607
|
+
],
|
|
2608
|
+
optional: false,
|
|
2609
|
+
metadata: { path: [] },
|
|
2610
|
+
}),
|
|
2611
|
+
);
|
|
2612
|
+
|
|
2613
|
+
return result;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2185
2616
|
result = create_jsx_element(
|
|
2186
2617
|
'TsrxErrorBoundary',
|
|
2187
2618
|
[
|
|
@@ -2191,8 +2622,18 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2191
2622
|
value: to_jsx_expression_container(/** @type {any} */ (fallback_fn)),
|
|
2192
2623
|
metadata: { path: [] },
|
|
2193
2624
|
},
|
|
2625
|
+
...(boundary_content
|
|
2626
|
+
? [
|
|
2627
|
+
{
|
|
2628
|
+
type: 'JSXAttribute',
|
|
2629
|
+
name: { type: 'JSXIdentifier', name: 'content', metadata: { path: [] } },
|
|
2630
|
+
value: to_jsx_expression_container(boundary_content),
|
|
2631
|
+
metadata: { path: [] },
|
|
2632
|
+
},
|
|
2633
|
+
]
|
|
2634
|
+
: []),
|
|
2194
2635
|
],
|
|
2195
|
-
[result],
|
|
2636
|
+
boundary_content ? [] : [result],
|
|
2196
2637
|
);
|
|
2197
2638
|
}
|
|
2198
2639
|
|
|
@@ -2315,7 +2756,11 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
|
|
|
2315
2756
|
function create_render_if_statement(node, transform_context) {
|
|
2316
2757
|
const consequent_body =
|
|
2317
2758
|
node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
|
|
2318
|
-
const consequent_has_hooks = body_contains_top_level_hook_call(
|
|
2759
|
+
const consequent_has_hooks = body_contains_top_level_hook_call(
|
|
2760
|
+
consequent_body,
|
|
2761
|
+
transform_context,
|
|
2762
|
+
true,
|
|
2763
|
+
);
|
|
2319
2764
|
|
|
2320
2765
|
let alternate = null;
|
|
2321
2766
|
if (node.alternate) {
|
|
@@ -2323,7 +2768,11 @@ function create_render_if_statement(node, transform_context) {
|
|
|
2323
2768
|
alternate = create_render_if_statement(node.alternate, transform_context);
|
|
2324
2769
|
} else {
|
|
2325
2770
|
const alternate_body = node.alternate.body || [node.alternate];
|
|
2326
|
-
const alternate_has_hooks = body_contains_top_level_hook_call(
|
|
2771
|
+
const alternate_has_hooks = body_contains_top_level_hook_call(
|
|
2772
|
+
alternate_body,
|
|
2773
|
+
transform_context,
|
|
2774
|
+
true,
|
|
2775
|
+
);
|
|
2327
2776
|
alternate = set_loc(
|
|
2328
2777
|
/** @type {any} */ ({
|
|
2329
2778
|
type: 'BlockStatement',
|
|
@@ -2388,7 +2837,7 @@ function create_render_switch_case(switch_case, transform_context) {
|
|
|
2388
2837
|
body_without_break.push(child);
|
|
2389
2838
|
}
|
|
2390
2839
|
|
|
2391
|
-
if (body_contains_top_level_hook_call(body_without_break)) {
|
|
2840
|
+
if (body_contains_top_level_hook_call(body_without_break, transform_context, true)) {
|
|
2392
2841
|
return /** @type {any} */ ({
|
|
2393
2842
|
type: 'SwitchCase',
|
|
2394
2843
|
test: switch_case.test,
|
|
@@ -2476,6 +2925,10 @@ function to_jsx_expression_container(expression, source_node = expression) {
|
|
|
2476
2925
|
* @returns {any[]}
|
|
2477
2926
|
*/
|
|
2478
2927
|
function transform_element_attributes_dispatch(attrs, transform_context, element) {
|
|
2928
|
+
const preprocess = transform_context.platform.hooks?.preprocessElementAttributes;
|
|
2929
|
+
if (preprocess) {
|
|
2930
|
+
attrs = preprocess(attrs, transform_context, element);
|
|
2931
|
+
}
|
|
2479
2932
|
const hook = transform_context.platform.hooks?.transformElementAttributes;
|
|
2480
2933
|
if (hook) return hook(attrs, transform_context, element);
|
|
2481
2934
|
return attrs.map((/** @type {any} */ a) => to_jsx_attribute(a, transform_context));
|
|
@@ -500,6 +500,9 @@ export function convert_source_map_to_mappings(
|
|
|
500
500
|
node.metadata?.lazy_param_is_component ? replace_label_to_component : undefined,
|
|
501
501
|
);
|
|
502
502
|
}
|
|
503
|
+
if ('hover' in (node.metadata || {})) {
|
|
504
|
+
token.metadata.hover = /** @type {any} */ (node.metadata).hover;
|
|
505
|
+
}
|
|
503
506
|
if (node.metadata?.disable_verification) {
|
|
504
507
|
token.mappingData = { ...mapping_data, verification: false };
|
|
505
508
|
}
|
|
@@ -1702,18 +1705,13 @@ export function convert_source_map_to_mappings(
|
|
|
1702
1705
|
node.type === 'TSTypeParameterDeclaration'
|
|
1703
1706
|
) {
|
|
1704
1707
|
if (node.loc) {
|
|
1705
|
-
// Generic spans can be emitted by downstream transforms with sparse source-map
|
|
1706
|
-
// coverage around the angle-bracket delimiters. Skip missing whole-node mappings
|
|
1707
|
-
// instead of crashing Volar, and rely on child type-node mappings instead.
|
|
1708
1708
|
const mapping = get_mapping_from_node(
|
|
1709
1709
|
node,
|
|
1710
1710
|
src_to_gen_map,
|
|
1711
1711
|
gen_line_offsets,
|
|
1712
1712
|
mapping_data_verify_only,
|
|
1713
1713
|
);
|
|
1714
|
-
|
|
1715
|
-
mappings.push(mapping);
|
|
1716
|
-
}
|
|
1714
|
+
mappings.push(mapping);
|
|
1717
1715
|
}
|
|
1718
1716
|
// Generic type parameters - visit to collect type variable names
|
|
1719
1717
|
if (node.params) {
|
package/src/utils/builders.js
CHANGED
|
@@ -681,14 +681,15 @@ export function prop(kind, key, value, computed = false, shorthand = false) {
|
|
|
681
681
|
* @returns {AST.PropertyDefinition}
|
|
682
682
|
*/
|
|
683
683
|
export function prop_def(key, value, computed = false, is_static = false) {
|
|
684
|
-
return {
|
|
684
|
+
return /** @type {AST.PropertyDefinition} */ ({
|
|
685
685
|
type: 'PropertyDefinition',
|
|
686
686
|
key,
|
|
687
687
|
value,
|
|
688
|
+
decorators: [],
|
|
688
689
|
computed,
|
|
689
690
|
static: is_static,
|
|
690
691
|
metadata: { path: [] },
|
|
691
|
-
};
|
|
692
|
+
});
|
|
692
693
|
}
|
|
693
694
|
|
|
694
695
|
/**
|
|
@@ -918,15 +919,16 @@ export function for_of(left, right, body, await_flag = false, loc_info) {
|
|
|
918
919
|
* @returns {AST.MethodDefinition}
|
|
919
920
|
*/
|
|
920
921
|
export function method(kind, key, params, body, computed = false, is_static = false) {
|
|
921
|
-
return {
|
|
922
|
+
return /** @type {AST.MethodDefinition} */ ({
|
|
922
923
|
type: 'MethodDefinition',
|
|
923
924
|
key,
|
|
924
925
|
kind,
|
|
925
926
|
value: function_builder(null, params, block(body)),
|
|
927
|
+
decorators: [],
|
|
926
928
|
computed,
|
|
927
929
|
static: is_static,
|
|
928
930
|
metadata: { path: [] },
|
|
929
|
-
};
|
|
931
|
+
});
|
|
930
932
|
}
|
|
931
933
|
|
|
932
934
|
/**
|
package/types/index.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ import type {
|
|
|
10
10
|
JsxPlatformHooks,
|
|
11
11
|
JsxTransformOptions,
|
|
12
12
|
JsxTransformResult,
|
|
13
|
+
componentToFunctionDeclaration,
|
|
13
14
|
createJsxTransform,
|
|
14
15
|
} from './jsx-platform';
|
|
15
16
|
|
|
16
17
|
export type { Parse, JsxPlatform, JsxPlatformHooks, JsxTransformOptions, JsxTransformResult };
|
|
17
|
-
export { createJsxTransform };
|
|
18
|
+
export { createJsxTransform, componentToFunctionDeclaration };
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Compile error interface
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -56,12 +56,31 @@ export interface JsxPlatformHooks {
|
|
|
56
56
|
tryStatement?: (node: any, ctx: any) => any;
|
|
57
57
|
};
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* Mark a top-level call expression inside a control-flow branch as requiring
|
|
60
|
+
* helper-component isolation so setup/state is created once per mounted
|
|
61
|
+
* branch instead of once per parent rerender. Vue uses this for branch-local
|
|
62
|
+
* Composition API state like `ref()`.
|
|
63
|
+
*/
|
|
64
|
+
isTopLevelSetupCall?: (callExpression: any, ctx: any) => boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Lower a `component` declaration to the replacement node for its current
|
|
67
|
+
* position. React / Preact use the default helper and return a
|
|
68
|
+
* `FunctionDeclaration`. Other targets may return a variable declaration or
|
|
69
|
+
* an expression that wraps the shared lowered function body (for example,
|
|
70
|
+
* `defineVaporComponent(...)`).
|
|
71
|
+
*
|
|
72
|
+
* The default lowering is exported as `componentToFunctionDeclaration()` so
|
|
73
|
+
* platform hooks can build on it instead of reimplementing component body
|
|
74
|
+
* handling.
|
|
63
75
|
*/
|
|
64
76
|
componentToFunction?: (component: any, ctx: any, helperState?: any) => any;
|
|
77
|
+
/**
|
|
78
|
+
* Wrap a hoisted helper component declaration emitted by the shared control-
|
|
79
|
+
* flow splitter. The default is the plain function declaration; Vue uses
|
|
80
|
+
* this to wrap helpers in `defineVaporComponent(...)` so branch-local setup
|
|
81
|
+
* state behaves like normal component state.
|
|
82
|
+
*/
|
|
83
|
+
wrapHelperComponent?: (helperFn: any, helperId: any, ctx: any, sourceNode: any) => any;
|
|
65
84
|
/**
|
|
66
85
|
* Inject module-level imports after the main walk. Default: import
|
|
67
86
|
* `Suspense` from `platform.imports.suspense` and `TsrxErrorBoundary`
|
|
@@ -76,6 +95,27 @@ export interface JsxPlatformHooks {
|
|
|
76
95
|
* attributes through its composite-element handling.
|
|
77
96
|
*/
|
|
78
97
|
transformElementAttributes?: (attrs: any[], ctx: any, element: any) => any[];
|
|
98
|
+
/**
|
|
99
|
+
* Rewrite or normalize raw Ripple attributes before the shared
|
|
100
|
+
* `to_jsx_attribute()` mapping runs. Targets can use this to merge multiple
|
|
101
|
+
* keyword attributes, such as collapsing repeated `{ref ...}` entries into a
|
|
102
|
+
* single `RefAttribute` backed by an array expression.
|
|
103
|
+
*/
|
|
104
|
+
preprocessElementAttributes?: (attrs: any[], ctx: any, element: any) => any[];
|
|
105
|
+
/**
|
|
106
|
+
* Optionally replace the default React-style `.map(...)` lowering for a
|
|
107
|
+
* `for...of` body after the shared transform has already produced its render
|
|
108
|
+
* statements and applied any explicit or implicit keys. Vue uses this to hand
|
|
109
|
+
* the loop to the downstream Vapor JSX compiler as a native `v-for` template.
|
|
110
|
+
*/
|
|
111
|
+
renderForOf?: (node: any, loopParams: any[], bodyStatements: any[], ctx: any) => any | null;
|
|
112
|
+
/**
|
|
113
|
+
* Optionally move the primary `try { ... }` render content into an explicit
|
|
114
|
+
* error-boundary prop instead of rendering it as the boundary's JSX children.
|
|
115
|
+
* Vue Vapor uses this because boundary content must execute lazily from a
|
|
116
|
+
* zero-argument function.
|
|
117
|
+
*/
|
|
118
|
+
createErrorBoundaryContent?: (tryContent: any, ctx: any, node: any) => any | null;
|
|
79
119
|
/**
|
|
80
120
|
* Lower a Ripple `Element` node to a JSXElement. Default is the
|
|
81
121
|
* factory's `to_jsx_element`. The hook receives the walker-transformed
|
|
@@ -85,6 +125,30 @@ export interface JsxPlatformHooks {
|
|
|
85
125
|
* generic text→JSXExpressionContainer transform runs.
|
|
86
126
|
*/
|
|
87
127
|
transformElement?: (inner: any, ctx: any, rawChildren: any[]) => any;
|
|
128
|
+
/**
|
|
129
|
+
* Optionally rewrite a host element's children into attributes or another
|
|
130
|
+
* specialized child shape after generic attribute lowering but before the
|
|
131
|
+
* default child-to-JSX conversion runs.
|
|
132
|
+
*
|
|
133
|
+
* This lets a target support target-native DOM content props such as
|
|
134
|
+
* `textContent` / `innerHTML` without forking the whole element lowering.
|
|
135
|
+
* The hook may mutate `attrs` directly and either return a replacement
|
|
136
|
+
* `children` array (plus optional `selfClosing` override) or `null` to fall
|
|
137
|
+
* back to the default child handling.
|
|
138
|
+
*/
|
|
139
|
+
transformElementChildren?: (
|
|
140
|
+
element: any,
|
|
141
|
+
walkedChildren: any[],
|
|
142
|
+
rawChildren: any[],
|
|
143
|
+
attrs: any[],
|
|
144
|
+
ctx: any,
|
|
145
|
+
) => { children: any[]; selfClosing?: boolean } | null;
|
|
146
|
+
/**
|
|
147
|
+
* Decide whether a JSX subtree may be hoisted to module scope when it is
|
|
148
|
+
* otherwise statically safe. Targets can use this to keep runtime-sensitive
|
|
149
|
+
* JSX, such as component invocations, inside render/setup execution.
|
|
150
|
+
*/
|
|
151
|
+
canHoistStaticNode?: (node: any, ctx: any) => boolean;
|
|
88
152
|
/**
|
|
89
153
|
* Custom validation for a component body that uses top-level `await`.
|
|
90
154
|
* Default: enforce `validation.requireUseServerForAwait`. Solid rejects
|
|
@@ -167,6 +231,15 @@ export interface JsxPlatform {
|
|
|
167
231
|
* Default: `true`.
|
|
168
232
|
*/
|
|
169
233
|
scanUseServerDirectiveForAwaitWithCustomValidator?: boolean;
|
|
234
|
+
/**
|
|
235
|
+
* Optional branded compiler error for targets that cannot lower
|
|
236
|
+
* `try { ... } pending { ... }` in component template context.
|
|
237
|
+
*
|
|
238
|
+
* When provided, the shared try-block lowering rejects any `pending`
|
|
239
|
+
* block with this message instead of emitting a React-style
|
|
240
|
+
* `<Suspense fallback={...}>` wrapper.
|
|
241
|
+
*/
|
|
242
|
+
unsupportedTryPendingMessage?: string;
|
|
170
243
|
};
|
|
171
244
|
|
|
172
245
|
/**
|
|
@@ -191,3 +264,9 @@ export function createJsxTransform(
|
|
|
191
264
|
filename?: string,
|
|
192
265
|
options?: JsxTransformOptions,
|
|
193
266
|
) => JsxTransformResult;
|
|
267
|
+
|
|
268
|
+
export function componentToFunctionDeclaration(
|
|
269
|
+
component: any,
|
|
270
|
+
ctx: any,
|
|
271
|
+
helperState?: any,
|
|
272
|
+
): AST.FunctionDeclaration;
|
package/types/parse.d.ts
CHANGED
|
@@ -1174,6 +1174,8 @@ export namespace Parse {
|
|
|
1174
1174
|
|
|
1175
1175
|
parseElement(): AST.Element | AST.Tsx | AST.TsxCompat;
|
|
1176
1176
|
|
|
1177
|
+
parseDoubleQuotedTextChild(): AST.TextNode;
|
|
1178
|
+
|
|
1177
1179
|
parseTemplateBody(
|
|
1178
1180
|
body: (AST.Statement | AST.Node | ESTreeJSX.JSXText | ESTreeJSX.JSXElement['children'])[],
|
|
1179
1181
|
): void;
|