@tsrx/core 0.0.23 → 0.0.25
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 +1 -1
- package/src/analyze/validation.js +79 -4
- package/src/index.js +15 -1
- package/src/plugin.js +83 -2
- package/src/transform/jsx/index.js +592 -73
- package/src/utils/builders.js +17 -4
- package/src/utils/escaping.js +11 -0
- package/src/utils/events.js +1 -1
- package/types/index.d.ts +13 -6
- package/types/jsx-platform.d.ts +7 -1
package/package.json
CHANGED
|
@@ -7,6 +7,18 @@ import { error } from '../errors.js';
|
|
|
7
7
|
|
|
8
8
|
export const COMPONENT_RETURN_VALUE_ERROR =
|
|
9
9
|
'Return statements inside components cannot have a return value.';
|
|
10
|
+
export const COMPONENT_LOOP_RETURN_ERROR =
|
|
11
|
+
'Return statements are not allowed inside component for...of loops. Use continue instead.';
|
|
12
|
+
export const COMPONENT_LOOP_BREAK_ERROR =
|
|
13
|
+
'Break statements are not allowed inside component for...of loops.';
|
|
14
|
+
export const COMPONENT_FOR_STATEMENT_ERROR =
|
|
15
|
+
'For loops are not supported in components. Use for...of instead.';
|
|
16
|
+
export const COMPONENT_FOR_IN_STATEMENT_ERROR =
|
|
17
|
+
'For...in loops are not supported in components. Use for...of instead.';
|
|
18
|
+
export const COMPONENT_WHILE_STATEMENT_ERROR =
|
|
19
|
+
'While loops are not supported in components. Move the while loop into a function.';
|
|
20
|
+
export const COMPONENT_DO_WHILE_STATEMENT_ERROR =
|
|
21
|
+
'Do...while loops are not supported in components. Move the do...while loop into a function.';
|
|
10
22
|
|
|
11
23
|
const invalid_nestings = {
|
|
12
24
|
// <p> cannot contain block-level elements
|
|
@@ -133,19 +145,29 @@ function get_element_tag(element) {
|
|
|
133
145
|
* @returns {AST.ReturnStatement}
|
|
134
146
|
*/
|
|
135
147
|
export function get_return_keyword_node(node) {
|
|
136
|
-
|
|
148
|
+
return get_statement_keyword_node(node, 'return');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @template {AST.Node} T
|
|
153
|
+
* @param {T} node
|
|
154
|
+
* @param {string} keyword
|
|
155
|
+
* @returns {T}
|
|
156
|
+
*/
|
|
157
|
+
export function get_statement_keyword_node(node, keyword) {
|
|
158
|
+
const keyword_length = keyword.length;
|
|
137
159
|
const start = /** @type {AST.NodeWithLocation} */ (node).start ?? 0;
|
|
138
160
|
const loc = /** @type {AST.NodeWithLocation} */ (node).loc;
|
|
139
161
|
|
|
140
|
-
return /** @type {
|
|
162
|
+
return /** @type {T} */ ({
|
|
141
163
|
...node,
|
|
142
|
-
end: start +
|
|
164
|
+
end: start + keyword_length,
|
|
143
165
|
loc: loc
|
|
144
166
|
? {
|
|
145
167
|
start: loc.start,
|
|
146
168
|
end: {
|
|
147
169
|
line: loc.start.line,
|
|
148
|
-
column: loc.start.column +
|
|
170
|
+
column: loc.start.column + keyword_length,
|
|
149
171
|
},
|
|
150
172
|
}
|
|
151
173
|
: undefined,
|
|
@@ -172,6 +194,59 @@ export function validate_component_return_statement(node, filename, errors, comm
|
|
|
172
194
|
);
|
|
173
195
|
}
|
|
174
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @param {AST.ReturnStatement} node
|
|
199
|
+
* @param {string | null | undefined} filename
|
|
200
|
+
* @param {CompileError[]} [errors]
|
|
201
|
+
* @param {AST.CommentWithLocation[]} [comments]
|
|
202
|
+
*/
|
|
203
|
+
export function validate_component_loop_return_statement(node, filename, errors, comments) {
|
|
204
|
+
error(
|
|
205
|
+
COMPONENT_LOOP_RETURN_ERROR,
|
|
206
|
+
filename ?? null,
|
|
207
|
+
get_return_keyword_node(node),
|
|
208
|
+
errors,
|
|
209
|
+
comments,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {AST.BreakStatement} node
|
|
215
|
+
* @param {string | null | undefined} filename
|
|
216
|
+
* @param {CompileError[]} [errors]
|
|
217
|
+
* @param {AST.CommentWithLocation[]} [comments]
|
|
218
|
+
*/
|
|
219
|
+
export function validate_component_loop_break_statement(node, filename, errors, comments) {
|
|
220
|
+
error(
|
|
221
|
+
COMPONENT_LOOP_BREAK_ERROR,
|
|
222
|
+
filename ?? null,
|
|
223
|
+
get_statement_keyword_node(node, 'break'),
|
|
224
|
+
errors,
|
|
225
|
+
comments,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {AST.ForStatement | AST.ForInStatement | AST.WhileStatement | AST.DoWhileStatement} node
|
|
231
|
+
* @param {string | null | undefined} filename
|
|
232
|
+
* @param {CompileError[]} [errors]
|
|
233
|
+
* @param {AST.CommentWithLocation[]} [comments]
|
|
234
|
+
*/
|
|
235
|
+
export function validate_component_unsupported_loop_statement(node, filename, errors, comments) {
|
|
236
|
+
let message;
|
|
237
|
+
if (node.type === 'ForStatement') {
|
|
238
|
+
message = COMPONENT_FOR_STATEMENT_ERROR;
|
|
239
|
+
} else if (node.type === 'ForInStatement') {
|
|
240
|
+
message = COMPONENT_FOR_IN_STATEMENT_ERROR;
|
|
241
|
+
} else if (node.type === 'WhileStatement') {
|
|
242
|
+
message = COMPONENT_WHILE_STATEMENT_ERROR;
|
|
243
|
+
} else {
|
|
244
|
+
message = COMPONENT_DO_WHILE_STATEMENT_ERROR;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
error(message, filename ?? null, node, errors, comments);
|
|
248
|
+
}
|
|
249
|
+
|
|
175
250
|
/**
|
|
176
251
|
* @param {AST.Element} element
|
|
177
252
|
* @param {AnalysisContext} context
|
package/src/index.js
CHANGED
|
@@ -133,13 +133,17 @@ export {
|
|
|
133
133
|
// Sanitize
|
|
134
134
|
export { sanitize_template_string as sanitizeTemplateString } from './utils/sanitize_template_string.js';
|
|
135
135
|
|
|
136
|
+
// CSS Property Name
|
|
137
|
+
export { normalize_css_property_name as normalizeCssPropertyName } from './utils/normalize_css_property_name.js';
|
|
138
|
+
|
|
136
139
|
// Escaping
|
|
137
|
-
export { escape } from './utils/escaping.js';
|
|
140
|
+
export { escape, escape_script as escapeScript } from './utils/escaping.js';
|
|
138
141
|
|
|
139
142
|
// Transform
|
|
140
143
|
export {
|
|
141
144
|
createJsxTransform,
|
|
142
145
|
merge_duplicate_refs as mergeDuplicateRefs,
|
|
146
|
+
rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
|
|
143
147
|
to_jsx_attribute as toJsxAttribute,
|
|
144
148
|
validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
|
|
145
149
|
component_to_function_declaration as componentToFunctionDeclaration,
|
|
@@ -209,8 +213,18 @@ export {
|
|
|
209
213
|
// Analyze
|
|
210
214
|
export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
|
|
211
215
|
export {
|
|
216
|
+
COMPONENT_DO_WHILE_STATEMENT_ERROR,
|
|
217
|
+
COMPONENT_FOR_IN_STATEMENT_ERROR,
|
|
218
|
+
COMPONENT_FOR_STATEMENT_ERROR,
|
|
219
|
+
COMPONENT_LOOP_BREAK_ERROR,
|
|
220
|
+
COMPONENT_LOOP_RETURN_ERROR,
|
|
212
221
|
COMPONENT_RETURN_VALUE_ERROR,
|
|
222
|
+
COMPONENT_WHILE_STATEMENT_ERROR,
|
|
213
223
|
get_return_keyword_node as getReturnKeywordNode,
|
|
224
|
+
get_statement_keyword_node as getStatementKeywordNode,
|
|
225
|
+
validate_component_loop_break_statement as validateComponentLoopBreakStatement,
|
|
226
|
+
validate_component_loop_return_statement as validateComponentLoopReturnStatement,
|
|
214
227
|
validate_component_return_statement as validateComponentReturnStatement,
|
|
228
|
+
validate_component_unsupported_loop_statement as validateComponentUnsupportedLoopStatement,
|
|
215
229
|
validate_nesting as validateNesting,
|
|
216
230
|
} from './analyze/validation.js';
|
package/src/plugin.js
CHANGED
|
@@ -269,6 +269,56 @@ export function TSRXPlugin(config) {
|
|
|
269
269
|
return null;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
#popTsxTokenContextBeforeTemplateExpressionChild() {
|
|
273
|
+
let index = this.pos;
|
|
274
|
+
let has_newline = false;
|
|
275
|
+
|
|
276
|
+
// Text-only Tsx nodes can leave the tokenizer in JSX text mode.
|
|
277
|
+
// Only unwind it for ASI before a following TSRX `{expr}` child;
|
|
278
|
+
// fragment props like `content={<></>}` still need the JSX context.
|
|
279
|
+
while (index < this.input.length) {
|
|
280
|
+
const ch = this.input.charCodeAt(index);
|
|
281
|
+
if (ch === 32 || ch === 9) {
|
|
282
|
+
index++;
|
|
283
|
+
} else if (ch === 10 || ch === 13) {
|
|
284
|
+
has_newline = true;
|
|
285
|
+
index++;
|
|
286
|
+
} else if (ch === 47 && this.input.charCodeAt(index + 1) === 42) {
|
|
287
|
+
const end = this.input.indexOf('*/', index + 2);
|
|
288
|
+
const comment_end = end === -1 ? this.input.length : end + 2;
|
|
289
|
+
if (this.input.slice(index, comment_end).match(regex_newline_characters)) {
|
|
290
|
+
has_newline = true;
|
|
291
|
+
}
|
|
292
|
+
index = comment_end;
|
|
293
|
+
} else if (ch === 47 && this.input.charCodeAt(index + 1) === 47) {
|
|
294
|
+
has_newline = true;
|
|
295
|
+
index += 2;
|
|
296
|
+
while (index < this.input.length) {
|
|
297
|
+
const comment_ch = this.input.charCodeAt(index);
|
|
298
|
+
if (comment_ch === 10 || comment_ch === 13) break;
|
|
299
|
+
index++;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!has_newline || this.input.charCodeAt(index) !== 123) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const context_index = this.context.lastIndexOf(tstc.tc_expr);
|
|
311
|
+
if (context_index !== -1) {
|
|
312
|
+
this.context.length = context_index;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#popTemplateLiteralTokenContext() {
|
|
317
|
+
while (this.curContext()?.token === '`') {
|
|
318
|
+
this.context.pop();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
272
322
|
#isDoubleQuotedTextChildStart() {
|
|
273
323
|
if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
|
|
274
324
|
return false;
|
|
@@ -1053,6 +1103,19 @@ export function TSRXPlugin(config) {
|
|
|
1053
1103
|
this.parseFunctionParams(node);
|
|
1054
1104
|
this.checkComponentParams(node.params);
|
|
1055
1105
|
|
|
1106
|
+
const is_arrow_component = this.type === tt.arrow;
|
|
1107
|
+
if (is_arrow_component) {
|
|
1108
|
+
if (node.id || requireName || skipName) {
|
|
1109
|
+
this.raise(
|
|
1110
|
+
this.start,
|
|
1111
|
+
'Arrow component syntax is only supported for anonymous component expressions.',
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
node.metadata ??= { path: [] };
|
|
1115
|
+
node.metadata.arrow = true;
|
|
1116
|
+
this.next();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1056
1119
|
// Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
|
|
1057
1120
|
// the component body's first token as if we'd entered fresh — no
|
|
1058
1121
|
// surrounding function body should affect our parseStatement/parseBlock
|
|
@@ -2009,6 +2072,7 @@ export function TSRXPlugin(config) {
|
|
|
2009
2072
|
if (this.type !== tstt.jsxTagEnd) {
|
|
2010
2073
|
raise_error();
|
|
2011
2074
|
}
|
|
2075
|
+
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2012
2076
|
this.next();
|
|
2013
2077
|
}
|
|
2014
2078
|
}
|
|
@@ -2194,6 +2258,7 @@ export function TSRXPlugin(config) {
|
|
|
2194
2258
|
if (this.type !== tstt.jsxTagEnd) {
|
|
2195
2259
|
raise_error();
|
|
2196
2260
|
}
|
|
2261
|
+
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2197
2262
|
this.next();
|
|
2198
2263
|
}
|
|
2199
2264
|
} else if (element.type === 'TsxCompat') {
|
|
@@ -2225,6 +2290,7 @@ export function TSRXPlugin(config) {
|
|
|
2225
2290
|
if (this.type !== tstt.jsxTagEnd) {
|
|
2226
2291
|
raise_error();
|
|
2227
2292
|
}
|
|
2293
|
+
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2228
2294
|
this.next();
|
|
2229
2295
|
}
|
|
2230
2296
|
} else if (this.#path[this.#path.length - 1] === element) {
|
|
@@ -2323,7 +2389,7 @@ export function TSRXPlugin(config) {
|
|
|
2323
2389
|
body.push(node);
|
|
2324
2390
|
} else if (this.type === tstt.jsxTagStart) {
|
|
2325
2391
|
// Parse JSX element
|
|
2326
|
-
const node = super.
|
|
2392
|
+
const node = super.jsx_parseElement();
|
|
2327
2393
|
body.push(node);
|
|
2328
2394
|
} else {
|
|
2329
2395
|
const start = this.start;
|
|
@@ -2354,6 +2420,7 @@ export function TSRXPlugin(config) {
|
|
|
2354
2420
|
body.push(node);
|
|
2355
2421
|
}
|
|
2356
2422
|
|
|
2423
|
+
this.#popTemplateLiteralTokenContext();
|
|
2357
2424
|
// Always call next() to ensure parser makes progress
|
|
2358
2425
|
this.next();
|
|
2359
2426
|
}
|
|
@@ -2386,7 +2453,7 @@ export function TSRXPlugin(config) {
|
|
|
2386
2453
|
body.push(node);
|
|
2387
2454
|
} else if (this.type === tstt.jsxTagStart) {
|
|
2388
2455
|
// Parse JSX element
|
|
2389
|
-
const node = super.
|
|
2456
|
+
const node = super.jsx_parseElement();
|
|
2390
2457
|
body.push(node);
|
|
2391
2458
|
} else {
|
|
2392
2459
|
const start = this.start;
|
|
@@ -2417,6 +2484,7 @@ export function TSRXPlugin(config) {
|
|
|
2417
2484
|
body.push(node);
|
|
2418
2485
|
}
|
|
2419
2486
|
|
|
2487
|
+
this.#popTemplateLiteralTokenContext();
|
|
2420
2488
|
this.next();
|
|
2421
2489
|
}
|
|
2422
2490
|
}
|
|
@@ -2701,6 +2769,19 @@ export function TSRXPlugin(config) {
|
|
|
2701
2769
|
return node;
|
|
2702
2770
|
}
|
|
2703
2771
|
|
|
2772
|
+
if (
|
|
2773
|
+
this.#functionBodyDepth === 0 &&
|
|
2774
|
+
this.type === tt.string &&
|
|
2775
|
+
this.input.charCodeAt(this.start) === 34 &&
|
|
2776
|
+
(this.#path.at(-1)?.type === 'Component' || this.#path.at(-1)?.type === 'Element')
|
|
2777
|
+
) {
|
|
2778
|
+
this.pos = this.start;
|
|
2779
|
+
this.#readDoubleQuotedTextChildToken();
|
|
2780
|
+
const node = this.parseDoubleQuotedTextChild();
|
|
2781
|
+
this.semicolon();
|
|
2782
|
+
return node;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2704
2785
|
// &[ or &{ at statement level — lazy destructuring assignment
|
|
2705
2786
|
// e.g., &[data] = track(0); or &{x, y} = obj;
|
|
2706
2787
|
if (this.type === tt.bitwiseAND) {
|
|
@@ -40,7 +40,12 @@ import {
|
|
|
40
40
|
} from '../lazy.js';
|
|
41
41
|
import { find_first_top_level_await_in_component_body } from '../await.js';
|
|
42
42
|
import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
validate_component_loop_break_statement,
|
|
45
|
+
validate_component_loop_return_statement,
|
|
46
|
+
validate_component_return_statement,
|
|
47
|
+
validate_component_unsupported_loop_statement,
|
|
48
|
+
} from '../../analyze/validation.js';
|
|
44
49
|
import { get_component_from_path } from '../../utils/ast.js';
|
|
45
50
|
import {
|
|
46
51
|
is_interleaved_body as is_interleaved_body_core,
|
|
@@ -61,6 +66,63 @@ import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
|
61
66
|
* @typedef {{ source_name: string, read: () => any }} LazyBinding
|
|
62
67
|
*/
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @param {any} node
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function is_function_or_class_boundary(node) {
|
|
74
|
+
return (
|
|
75
|
+
node?.type === 'FunctionDeclaration' ||
|
|
76
|
+
node?.type === 'FunctionExpression' ||
|
|
77
|
+
node?.type === 'ArrowFunctionExpression' ||
|
|
78
|
+
node?.type === 'ClassDeclaration' ||
|
|
79
|
+
node?.type === 'ClassExpression'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {any[]} path
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function is_inside_component_for_of(path) {
|
|
88
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
89
|
+
const node = path[i];
|
|
90
|
+
if (is_function_or_class_boundary(node) || node?.type === 'Component') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (node?.type === 'ForOfStatement') {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {any[]} path
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function break_targets_component_loop(path) {
|
|
105
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
106
|
+
const node = path[i];
|
|
107
|
+
if (is_function_or_class_boundary(node) || node?.type === 'Component') {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (node?.type === 'SwitchStatement') {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
node?.type === 'ForOfStatement' ||
|
|
115
|
+
node?.type === 'ForStatement' ||
|
|
116
|
+
node?.type === 'ForInStatement' ||
|
|
117
|
+
node?.type === 'WhileStatement' ||
|
|
118
|
+
node?.type === 'DoWhileStatement'
|
|
119
|
+
) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
64
126
|
/**
|
|
65
127
|
* Build a `transform()` function for a specific JSX platform (React, Preact,
|
|
66
128
|
* Solid). Given a `JsxPlatform` descriptor, returns a transform that parses
|
|
@@ -104,6 +166,7 @@ export function createJsxTransform(platform) {
|
|
|
104
166
|
needs_error_boundary: false,
|
|
105
167
|
needs_suspense: false,
|
|
106
168
|
needs_merge_refs: false,
|
|
169
|
+
needs_fragment: false,
|
|
107
170
|
helper_state: null,
|
|
108
171
|
available_bindings: new Map(),
|
|
109
172
|
lazy_next_id: 0,
|
|
@@ -122,7 +185,81 @@ export function createJsxTransform(platform) {
|
|
|
122
185
|
walk(/** @type {any} */ (ast), transform_context, {
|
|
123
186
|
ReturnStatement(node, { next, path }) {
|
|
124
187
|
if (get_component_from_path(path)) {
|
|
125
|
-
|
|
188
|
+
if (is_inside_component_for_of(path)) {
|
|
189
|
+
validate_component_loop_return_statement(
|
|
190
|
+
node,
|
|
191
|
+
filename,
|
|
192
|
+
transform_context.errors,
|
|
193
|
+
transform_context.comments,
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
validate_component_return_statement(
|
|
197
|
+
node,
|
|
198
|
+
filename,
|
|
199
|
+
transform_context.errors,
|
|
200
|
+
transform_context.comments,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return next();
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
BreakStatement(node, { next, path }) {
|
|
209
|
+
if (get_component_from_path(path) && break_targets_component_loop(path)) {
|
|
210
|
+
validate_component_loop_break_statement(
|
|
211
|
+
node,
|
|
212
|
+
filename,
|
|
213
|
+
transform_context.errors,
|
|
214
|
+
transform_context.comments,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return next();
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
ForStatement(node, { next, path }) {
|
|
222
|
+
if (get_component_from_path(path)) {
|
|
223
|
+
validate_component_unsupported_loop_statement(
|
|
224
|
+
node,
|
|
225
|
+
filename,
|
|
226
|
+
transform_context.errors,
|
|
227
|
+
transform_context.comments,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return next();
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
ForInStatement(node, { next, path }) {
|
|
235
|
+
if (get_component_from_path(path)) {
|
|
236
|
+
validate_component_unsupported_loop_statement(
|
|
237
|
+
node,
|
|
238
|
+
filename,
|
|
239
|
+
transform_context.errors,
|
|
240
|
+
transform_context.comments,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return next();
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
WhileStatement(node, { next, path }) {
|
|
248
|
+
if (get_component_from_path(path)) {
|
|
249
|
+
validate_component_unsupported_loop_statement(
|
|
250
|
+
node,
|
|
251
|
+
filename,
|
|
252
|
+
transform_context.errors,
|
|
253
|
+
transform_context.comments,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return next();
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
DoWhileStatement(node, { next, path }) {
|
|
261
|
+
if (get_component_from_path(path)) {
|
|
262
|
+
validate_component_unsupported_loop_statement(
|
|
126
263
|
node,
|
|
127
264
|
filename,
|
|
128
265
|
transform_context.errors,
|
|
@@ -350,7 +487,7 @@ function has_use_server_directive(program) {
|
|
|
350
487
|
* @param {any} component
|
|
351
488
|
* @param {TransformContext} transform_context
|
|
352
489
|
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
|
|
353
|
-
* @returns {AST.FunctionDeclaration}
|
|
490
|
+
* @returns {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression}
|
|
354
491
|
*/
|
|
355
492
|
export function component_to_function_declaration(component, transform_context, walk_helper_state) {
|
|
356
493
|
const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
|
|
@@ -390,28 +527,62 @@ export function component_to_function_declaration(component, transform_context,
|
|
|
390
527
|
const final_body =
|
|
391
528
|
lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
|
|
392
529
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
530
|
+
/** @type {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression} */
|
|
531
|
+
let fn;
|
|
532
|
+
|
|
533
|
+
if (component.id) {
|
|
534
|
+
fn = /** @type {any} */ ({
|
|
535
|
+
type: 'FunctionDeclaration',
|
|
536
|
+
id: component.id,
|
|
537
|
+
typeParameters: component.typeParameters,
|
|
538
|
+
params: final_params,
|
|
539
|
+
body: final_body,
|
|
540
|
+
async: is_async_component,
|
|
541
|
+
generator: false,
|
|
542
|
+
metadata: {
|
|
543
|
+
path: [],
|
|
544
|
+
is_component: true,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
} else if (component.metadata?.arrow) {
|
|
548
|
+
fn = /** @type {any} */ ({
|
|
549
|
+
type: 'ArrowFunctionExpression',
|
|
550
|
+
typeParameters: component.typeParameters,
|
|
551
|
+
params: final_params,
|
|
552
|
+
body: final_body,
|
|
553
|
+
async: is_async_component,
|
|
554
|
+
generator: false,
|
|
555
|
+
expression: false,
|
|
556
|
+
metadata: {
|
|
557
|
+
path: [],
|
|
558
|
+
is_component: true,
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
} else {
|
|
562
|
+
fn = /** @type {any} */ ({
|
|
563
|
+
type: 'FunctionExpression',
|
|
564
|
+
id: null,
|
|
565
|
+
typeParameters: component.typeParameters,
|
|
566
|
+
params: final_params,
|
|
567
|
+
body: final_body,
|
|
568
|
+
async: is_async_component,
|
|
569
|
+
generator: false,
|
|
570
|
+
metadata: {
|
|
571
|
+
path: [],
|
|
572
|
+
is_component: true,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
}
|
|
406
576
|
|
|
407
577
|
// Restore context
|
|
408
578
|
transform_context.helper_state = saved_helper_state;
|
|
409
579
|
transform_context.available_bindings = saved_bindings;
|
|
410
580
|
|
|
411
|
-
|
|
412
|
-
|
|
581
|
+
const fn_metadata = /** @type {any} */ (fn.metadata);
|
|
582
|
+
fn_metadata.generated_helpers = helper_state.helpers;
|
|
583
|
+
fn_metadata.generated_statics = helper_state.statics;
|
|
413
584
|
|
|
414
|
-
if (fn.id) {
|
|
585
|
+
if (fn.type === 'FunctionDeclaration' && fn.id) {
|
|
415
586
|
fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
|
|
416
587
|
...fn.id.metadata,
|
|
417
588
|
is_component: true,
|
|
@@ -440,6 +611,7 @@ function build_component_statements(body_nodes, transform_context) {
|
|
|
440
611
|
function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
|
|
441
612
|
const statements = [];
|
|
442
613
|
const render_nodes = [];
|
|
614
|
+
let has_bare_return = false;
|
|
443
615
|
|
|
444
616
|
// Create a new bindings map so inner-scope bindings from
|
|
445
617
|
// collect_statement_bindings don't leak to the caller's scope.
|
|
@@ -460,6 +632,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
460
632
|
if (is_bare_return_statement(child)) {
|
|
461
633
|
statements.push(create_component_return_statement(render_nodes, child));
|
|
462
634
|
render_nodes.length = 0;
|
|
635
|
+
has_bare_return = true;
|
|
463
636
|
continue;
|
|
464
637
|
}
|
|
465
638
|
|
|
@@ -708,7 +881,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
708
881
|
}
|
|
709
882
|
|
|
710
883
|
const return_arg = build_return_expression(render_nodes);
|
|
711
|
-
if (return_arg || return_null_when_empty) {
|
|
884
|
+
if (return_arg || (return_null_when_empty && !has_bare_return)) {
|
|
712
885
|
statements.push({
|
|
713
886
|
type: 'ReturnStatement',
|
|
714
887
|
argument: return_arg || { type: 'Literal', value: null, raw: 'null' },
|
|
@@ -1158,9 +1331,9 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
|
1158
1331
|
*/
|
|
1159
1332
|
function expand_component_helpers(program) {
|
|
1160
1333
|
program.body = program.body.flatMap((statement) => {
|
|
1161
|
-
const
|
|
1162
|
-
const statics = meta
|
|
1163
|
-
const helpers = meta
|
|
1334
|
+
const metas = get_generated_component_metadata_list(statement);
|
|
1335
|
+
const statics = metas.flatMap((meta) => meta.generated_statics || []);
|
|
1336
|
+
const helpers = metas.flatMap((meta) => meta.generated_helpers || []);
|
|
1164
1337
|
if (statics.length || helpers.length) {
|
|
1165
1338
|
return [...statics, ...helpers, statement];
|
|
1166
1339
|
}
|
|
@@ -1173,30 +1346,63 @@ function expand_component_helpers(program) {
|
|
|
1173
1346
|
|
|
1174
1347
|
/**
|
|
1175
1348
|
* Component hooks may replace a `Component` node with a function declaration,
|
|
1176
|
-
* variable declaration, or export-safe expression.
|
|
1177
|
-
* metadata is carried on whichever replacement node
|
|
1178
|
-
* helper expansion must read metadata from that broader
|
|
1349
|
+
* variable declaration, object literal member, or export-safe expression.
|
|
1350
|
+
* Generated helper/statics metadata is carried on whichever replacement node
|
|
1351
|
+
* the hook returns, so helper expansion must read metadata from that broader
|
|
1352
|
+
* set.
|
|
1179
1353
|
*
|
|
1180
1354
|
* @param {any} node
|
|
1181
|
-
* @returns {{ generated_helpers?: any[], generated_statics?: any[] }
|
|
1355
|
+
* @returns {{ generated_helpers?: any[], generated_statics?: any[] }[]}
|
|
1182
1356
|
*/
|
|
1183
|
-
function
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1357
|
+
function get_generated_component_metadata_list(node) {
|
|
1358
|
+
/** @type {{ generated_helpers?: any[], generated_statics?: any[] }[]} */
|
|
1359
|
+
const metas = [];
|
|
1360
|
+
const seen_nodes = new Set();
|
|
1361
|
+
const seen_metas = new Set();
|
|
1362
|
+
|
|
1363
|
+
/** @param {any} current */
|
|
1364
|
+
const visit = (current) => {
|
|
1365
|
+
if (!current || typeof current !== 'object' || seen_nodes.has(current)) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1187
1368
|
|
|
1188
|
-
|
|
1189
|
-
return node.metadata;
|
|
1190
|
-
}
|
|
1369
|
+
seen_nodes.add(current);
|
|
1191
1370
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1371
|
+
if (current.metadata?.generated_helpers || current.metadata?.generated_statics) {
|
|
1372
|
+
if (!seen_metas.has(current.metadata)) {
|
|
1373
|
+
seen_metas.add(current.metadata);
|
|
1374
|
+
metas.push(current.metadata);
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (
|
|
1380
|
+
current.type === 'FunctionDeclaration' ||
|
|
1381
|
+
current.type === 'FunctionExpression' ||
|
|
1382
|
+
current.type === 'ArrowFunctionExpression'
|
|
1383
|
+
) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
for (const key of Object.keys(current)) {
|
|
1388
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const value = current[key];
|
|
1393
|
+
if (Array.isArray(value)) {
|
|
1394
|
+
for (const child of value) {
|
|
1395
|
+
visit(child);
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
visit(value);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
visit(node);
|
|
1198
1404
|
|
|
1199
|
-
return
|
|
1405
|
+
return metas;
|
|
1200
1406
|
}
|
|
1201
1407
|
|
|
1202
1408
|
/**
|
|
@@ -1339,6 +1545,126 @@ function append_tail_invocation(body, tail_helper) {
|
|
|
1339
1545
|
return [...body, clone_tail_invocation(tail_helper)];
|
|
1340
1546
|
}
|
|
1341
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* @param {AST.Identifier} tail_synthetic_id
|
|
1550
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1551
|
+
* @returns {any}
|
|
1552
|
+
*/
|
|
1553
|
+
function create_loop_tail_expression(tail_synthetic_id, tail_helper) {
|
|
1554
|
+
return b.logical('&&', clone_identifier(tail_synthetic_id), clone_tail_invocation(tail_helper));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* @param {AST.Identifier} tail_synthetic_id
|
|
1559
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1560
|
+
* @returns {any}
|
|
1561
|
+
*/
|
|
1562
|
+
function create_loop_tail_conditional(tail_synthetic_id, tail_helper) {
|
|
1563
|
+
return b.conditional(
|
|
1564
|
+
clone_identifier(tail_synthetic_id),
|
|
1565
|
+
clone_tail_invocation(tail_helper),
|
|
1566
|
+
create_null_literal(),
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* @param {any[]} statements
|
|
1572
|
+
* @param {AST.Identifier} tail_synthetic_id
|
|
1573
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1574
|
+
* @returns {void}
|
|
1575
|
+
*/
|
|
1576
|
+
function append_loop_tail_to_return_statements(statements, tail_synthetic_id, tail_helper) {
|
|
1577
|
+
for (const statement of statements) {
|
|
1578
|
+
append_loop_tail_to_return_statement(statement, tail_synthetic_id, tail_helper, false);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* @param {any} node
|
|
1584
|
+
* @param {AST.Identifier} tail_synthetic_id
|
|
1585
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1586
|
+
* @param {boolean} inside_nested_function
|
|
1587
|
+
* @returns {void}
|
|
1588
|
+
*/
|
|
1589
|
+
function append_loop_tail_to_return_statement(
|
|
1590
|
+
node,
|
|
1591
|
+
tail_synthetic_id,
|
|
1592
|
+
tail_helper,
|
|
1593
|
+
inside_nested_function,
|
|
1594
|
+
) {
|
|
1595
|
+
if (!node || typeof node !== 'object') {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (
|
|
1600
|
+
node.type === 'FunctionDeclaration' ||
|
|
1601
|
+
node.type === 'FunctionExpression' ||
|
|
1602
|
+
node.type === 'ArrowFunctionExpression'
|
|
1603
|
+
) {
|
|
1604
|
+
inside_nested_function = true;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (!inside_nested_function && node.type === 'ReturnStatement') {
|
|
1608
|
+
if (
|
|
1609
|
+
references_scope_bindings(
|
|
1610
|
+
node.argument,
|
|
1611
|
+
new Map([[tail_synthetic_id.name, tail_synthetic_id]]),
|
|
1612
|
+
)
|
|
1613
|
+
) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
node.argument = append_loop_tail_to_return_argument(
|
|
1617
|
+
node.argument,
|
|
1618
|
+
tail_synthetic_id,
|
|
1619
|
+
tail_helper,
|
|
1620
|
+
);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if (Array.isArray(node)) {
|
|
1625
|
+
for (const child of node) {
|
|
1626
|
+
append_loop_tail_to_return_statement(
|
|
1627
|
+
child,
|
|
1628
|
+
tail_synthetic_id,
|
|
1629
|
+
tail_helper,
|
|
1630
|
+
inside_nested_function,
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
for (const key of Object.keys(node)) {
|
|
1637
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
append_loop_tail_to_return_statement(
|
|
1641
|
+
node[key],
|
|
1642
|
+
tail_synthetic_id,
|
|
1643
|
+
tail_helper,
|
|
1644
|
+
inside_nested_function,
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* @param {any} return_argument
|
|
1651
|
+
* @param {AST.Identifier} tail_synthetic_id
|
|
1652
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1653
|
+
* @returns {any}
|
|
1654
|
+
*/
|
|
1655
|
+
function append_loop_tail_to_return_argument(return_argument, tail_synthetic_id, tail_helper) {
|
|
1656
|
+
if (return_argument == null || is_null_literal(return_argument)) {
|
|
1657
|
+
return create_loop_tail_conditional(tail_synthetic_id, tail_helper);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
return (
|
|
1661
|
+
build_return_expression([
|
|
1662
|
+
return_argument_to_render_node(return_argument),
|
|
1663
|
+
to_jsx_expression_container(create_loop_tail_expression(tail_synthetic_id, tail_helper)),
|
|
1664
|
+
]) || create_null_literal()
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1342
1668
|
/**
|
|
1343
1669
|
* Build a `return <combined-render-fragment>;` statement, prepending any
|
|
1344
1670
|
* `render_nodes` collected before the control-flow construct so they don't
|
|
@@ -1703,7 +2029,11 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
|
|
|
1703
2029
|
}
|
|
1704
2030
|
|
|
1705
2031
|
const has_tail = continuation_body.length > 0;
|
|
1706
|
-
const original_loop_body =
|
|
2032
|
+
const original_loop_body = /** @type {any[]} */ (
|
|
2033
|
+
rewrite_loop_continues_to_bare_returns(
|
|
2034
|
+
node.body.type === 'BlockStatement' ? node.body.body : [node.body],
|
|
2035
|
+
)
|
|
2036
|
+
);
|
|
1707
2037
|
|
|
1708
2038
|
// When there's a tail, build TailHelper first so its component_element can
|
|
1709
2039
|
// be embedded inside the loop helper's body (gated on isLast). The
|
|
@@ -1720,18 +2050,13 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
|
|
|
1720
2050
|
} else {
|
|
1721
2051
|
tail_synthetic_id = /** @type {any} */ (null);
|
|
1722
2052
|
}
|
|
1723
|
-
const
|
|
1724
|
-
?
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
clone_tail_invocation(/** @type {any} */ (tail_helper)),
|
|
1731
|
-
),
|
|
1732
|
-
),
|
|
1733
|
-
]
|
|
1734
|
-
: original_loop_body;
|
|
2053
|
+
const loop_tail_expression = has_tail
|
|
2054
|
+
? create_loop_tail_expression(tail_synthetic_id, /** @type {any} */ (tail_helper))
|
|
2055
|
+
: null;
|
|
2056
|
+
const loop_body =
|
|
2057
|
+
has_tail && loop_tail_expression
|
|
2058
|
+
? [...original_loop_body, b.jsx_expression_container(loop_tail_expression)]
|
|
2059
|
+
: original_loop_body;
|
|
1735
2060
|
|
|
1736
2061
|
const source_id = create_generated_identifier(
|
|
1737
2062
|
`_tsrx_iteration_items_${transform_context.local_statement_component_index + 1}`,
|
|
@@ -1767,10 +2092,8 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
|
|
|
1767
2092
|
);
|
|
1768
2093
|
|
|
1769
2094
|
// Synthetic `isLast` prop on the loop helper when there's a tail. It's
|
|
1770
|
-
// passed from the .map callback as `i === source.length - 1` so
|
|
1771
|
-
// helper
|
|
1772
|
-
// gate on this prop's value here — the JSXLogicalExpression appended to
|
|
1773
|
-
// `loop_body` does the gating at render time.
|
|
2095
|
+
// passed from the .map callback as `i === source.length - 1` so every
|
|
2096
|
+
// loop-helper return can append the tail helper on the last iteration.
|
|
1774
2097
|
const tail_isLast_alias = has_tail
|
|
1775
2098
|
? {
|
|
1776
2099
|
id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
|
|
@@ -1808,6 +2131,13 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
|
|
|
1808
2131
|
transform_context.available_bindings.set(tail_synthetic_id.name, tail_synthetic_id);
|
|
1809
2132
|
}
|
|
1810
2133
|
const fn_body_statements = build_render_statements(loop_body, true, transform_context);
|
|
2134
|
+
if (has_tail) {
|
|
2135
|
+
append_loop_tail_to_return_statements(
|
|
2136
|
+
fn_body_statements,
|
|
2137
|
+
tail_synthetic_id,
|
|
2138
|
+
/** @type {any} */ (tail_helper),
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
1811
2141
|
transform_context.available_bindings = fn_saved_bindings;
|
|
1812
2142
|
|
|
1813
2143
|
const helper_fn = /** @type {any} */ (
|
|
@@ -1851,7 +2181,7 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
|
|
|
1851
2181
|
index_identifier = null;
|
|
1852
2182
|
}
|
|
1853
2183
|
|
|
1854
|
-
const body_key_expression = find_key_expression_in_body(
|
|
2184
|
+
const body_key_expression = find_key_expression_in_body(original_loop_body);
|
|
1855
2185
|
const explicit_key_expression =
|
|
1856
2186
|
body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
|
|
1857
2187
|
const key_expression =
|
|
@@ -2087,7 +2417,7 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
|
|
|
2087
2417
|
function combine_render_return_argument(render_nodes, return_argument) {
|
|
2088
2418
|
const combined = render_nodes.map((node) => clone_expression_node_without_locations(node));
|
|
2089
2419
|
|
|
2090
|
-
if (!is_null_literal(return_argument)) {
|
|
2420
|
+
if (return_argument != null && !is_null_literal(return_argument)) {
|
|
2091
2421
|
combined.push(return_argument_to_render_node(return_argument));
|
|
2092
2422
|
}
|
|
2093
2423
|
|
|
@@ -3049,6 +3379,71 @@ function find_key_expression_in_body(body_nodes) {
|
|
|
3049
3379
|
return undefined;
|
|
3050
3380
|
}
|
|
3051
3381
|
|
|
3382
|
+
/**
|
|
3383
|
+
* @param {any} source_node
|
|
3384
|
+
* @returns {any}
|
|
3385
|
+
*/
|
|
3386
|
+
function continue_to_bare_return(source_node) {
|
|
3387
|
+
return set_loc(
|
|
3388
|
+
/** @type {any} */ ({
|
|
3389
|
+
type: 'ReturnStatement',
|
|
3390
|
+
argument: null,
|
|
3391
|
+
metadata: { path: [] },
|
|
3392
|
+
}),
|
|
3393
|
+
source_node,
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
/**
|
|
3398
|
+
* `continue` in a component `for...of` body means "skip this item". JSX targets
|
|
3399
|
+
* lower `for...of` to callbacks, so a raw ContinueStatement would be invalid JS;
|
|
3400
|
+
* a bare `return` from the callback preserves the item-skip behavior.
|
|
3401
|
+
*
|
|
3402
|
+
* @param {any[] | any} node
|
|
3403
|
+
* @param {boolean} [is_root]
|
|
3404
|
+
* @returns {any[] | any}
|
|
3405
|
+
*/
|
|
3406
|
+
export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
|
|
3407
|
+
if (Array.isArray(node)) {
|
|
3408
|
+
return node.map((child) => rewrite_loop_continues_to_bare_returns(child, false));
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
if (!node || typeof node !== 'object') {
|
|
3412
|
+
return node;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
if (node.type === 'ContinueStatement') {
|
|
3416
|
+
return continue_to_bare_return(node);
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
if (is_function_or_class_boundary(node) || (!is_root && is_loop_statement(node))) {
|
|
3420
|
+
return node;
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
for (const key of Object.keys(node)) {
|
|
3424
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
node[key] = rewrite_loop_continues_to_bare_returns(node[key], false);
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
return node;
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
/**
|
|
3434
|
+
* @param {any} node
|
|
3435
|
+
* @returns {boolean}
|
|
3436
|
+
*/
|
|
3437
|
+
function is_loop_statement(node) {
|
|
3438
|
+
return (
|
|
3439
|
+
node?.type === 'ForOfStatement' ||
|
|
3440
|
+
node?.type === 'ForStatement' ||
|
|
3441
|
+
node?.type === 'ForInStatement' ||
|
|
3442
|
+
node?.type === 'WhileStatement' ||
|
|
3443
|
+
node?.type === 'DoWhileStatement'
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3052
3447
|
/**
|
|
3053
3448
|
* @param {any} node
|
|
3054
3449
|
* @param {TransformContext} transform_context
|
|
@@ -3066,7 +3461,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
3066
3461
|
}
|
|
3067
3462
|
|
|
3068
3463
|
const loop_params = get_for_of_iteration_params(node.left, node.index);
|
|
3069
|
-
const loop_body =
|
|
3464
|
+
const loop_body = /** @type {any[]} */ (
|
|
3465
|
+
rewrite_loop_continues_to_bare_returns(
|
|
3466
|
+
node.body.type === 'BlockStatement' ? node.body.body : [node.body],
|
|
3467
|
+
)
|
|
3468
|
+
);
|
|
3070
3469
|
const has_hooks = body_contains_top_level_hook_call(loop_body, transform_context, true);
|
|
3071
3470
|
const body_key_expression = find_key_expression_in_body(loop_body);
|
|
3072
3471
|
const explicit_key_expression =
|
|
@@ -3091,14 +3490,14 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
3091
3490
|
collect_pattern_bindings(param, transform_context.available_bindings);
|
|
3092
3491
|
}
|
|
3093
3492
|
|
|
3493
|
+
if (implicit_non_hook_key_expression && should_apply_key_to_loop_body(loop_body)) {
|
|
3494
|
+
apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3094
3497
|
const body_statements = has_hooks
|
|
3095
3498
|
? hook_safe_render_statements(loop_body, key_expression, transform_context)
|
|
3096
3499
|
: build_render_statements(loop_body, true, transform_context);
|
|
3097
3500
|
|
|
3098
|
-
if (implicit_non_hook_key_expression) {
|
|
3099
|
-
apply_key_to_render_statements(body_statements, implicit_non_hook_key_expression);
|
|
3100
|
-
}
|
|
3101
|
-
|
|
3102
3501
|
const platform_for_of = transform_context.platform.hooks?.renderForOf?.(
|
|
3103
3502
|
node,
|
|
3104
3503
|
loop_params,
|
|
@@ -3110,6 +3509,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
3110
3509
|
return platform_for_of;
|
|
3111
3510
|
}
|
|
3112
3511
|
|
|
3512
|
+
const non_hook_key_expression = key_expression ?? implicit_non_hook_key_expression;
|
|
3513
|
+
if (!has_hooks && non_hook_key_expression) {
|
|
3514
|
+
apply_key_to_render_statements(body_statements, non_hook_key_expression, transform_context);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3113
3517
|
// Restore bindings
|
|
3114
3518
|
transform_context.available_bindings = saved_bindings;
|
|
3115
3519
|
|
|
@@ -3147,19 +3551,33 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
3147
3551
|
}
|
|
3148
3552
|
|
|
3149
3553
|
/**
|
|
3150
|
-
* @param {any[]}
|
|
3554
|
+
* @param {any[]} body_nodes
|
|
3151
3555
|
* @param {any} key_expression
|
|
3152
3556
|
* @returns {void}
|
|
3153
3557
|
*/
|
|
3154
|
-
function
|
|
3155
|
-
for (
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3558
|
+
function apply_key_to_loop_body(body_nodes, key_expression) {
|
|
3559
|
+
for (const node of body_nodes) {
|
|
3560
|
+
if (node.type === 'Element') {
|
|
3561
|
+
const attributes = node.attributes || (node.attributes = []);
|
|
3562
|
+
const has_key = attributes.some((/** @type {any} */ attr) => {
|
|
3563
|
+
const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
|
|
3564
|
+
return attr_name === 'key';
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
if (!has_key) {
|
|
3568
|
+
attributes.push({
|
|
3569
|
+
type: 'Attribute',
|
|
3570
|
+
name: { type: 'Identifier', name: 'key', metadata: { path: [] } },
|
|
3571
|
+
value: clone_expression_node(key_expression),
|
|
3572
|
+
shorthand: false,
|
|
3573
|
+
metadata: { path: [] },
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
return;
|
|
3159
3577
|
}
|
|
3160
3578
|
|
|
3161
|
-
if (
|
|
3162
|
-
const attributes =
|
|
3579
|
+
if (node.type === 'JSXElement') {
|
|
3580
|
+
const attributes = node.openingElement?.attributes || [];
|
|
3163
3581
|
const has_key = attributes.some(
|
|
3164
3582
|
(/** @type {any} */ attr) =>
|
|
3165
3583
|
attr.type === 'JSXAttribute' &&
|
|
@@ -3180,12 +3598,92 @@ function apply_key_to_render_statements(statements, key_expression) {
|
|
|
3180
3598
|
}),
|
|
3181
3599
|
);
|
|
3182
3600
|
}
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
/**
|
|
3607
|
+
* @param {any[]} body_nodes
|
|
3608
|
+
* @returns {boolean}
|
|
3609
|
+
*/
|
|
3610
|
+
function should_apply_key_to_loop_body(body_nodes) {
|
|
3611
|
+
let keyable_children = 0;
|
|
3612
|
+
for (const node of body_nodes) {
|
|
3613
|
+
if (node.type === 'Element' || node.type === 'JSXElement') {
|
|
3614
|
+
keyable_children += 1;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
return keyable_children === 1;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
/**
|
|
3621
|
+
* @param {any[]} statements
|
|
3622
|
+
* @param {any} key_expression
|
|
3623
|
+
* @param {TransformContext} transform_context
|
|
3624
|
+
* @returns {void}
|
|
3625
|
+
*/
|
|
3626
|
+
function apply_key_to_render_statements(statements, key_expression, transform_context) {
|
|
3627
|
+
for (let i = statements.length - 1; i >= 0; i -= 1) {
|
|
3628
|
+
const statement = statements[i];
|
|
3629
|
+
if (statement?.type !== 'ReturnStatement' || !statement.argument) {
|
|
3630
|
+
continue;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
if (statement.argument.type === 'JSXElement') {
|
|
3634
|
+
apply_key_to_jsx_element(statement.argument, key_expression);
|
|
3635
|
+
} else if (statement.argument.type === 'JSXFragment') {
|
|
3636
|
+
transform_context.needs_fragment = true;
|
|
3637
|
+
statement.argument = keyed_fragment_to_jsx_element(statement.argument, key_expression);
|
|
3183
3638
|
}
|
|
3184
3639
|
|
|
3185
3640
|
return;
|
|
3186
3641
|
}
|
|
3187
3642
|
}
|
|
3188
3643
|
|
|
3644
|
+
/**
|
|
3645
|
+
* @param {any} element
|
|
3646
|
+
* @param {any} key_expression
|
|
3647
|
+
* @returns {void}
|
|
3648
|
+
*/
|
|
3649
|
+
function apply_key_to_jsx_element(element, key_expression) {
|
|
3650
|
+
const attributes = element.openingElement?.attributes || [];
|
|
3651
|
+
const has_key = attributes.some(
|
|
3652
|
+
(/** @type {any} */ attr) =>
|
|
3653
|
+
attr.type === 'JSXAttribute' &&
|
|
3654
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
3655
|
+
attr.name.name === 'key',
|
|
3656
|
+
);
|
|
3657
|
+
|
|
3658
|
+
if (!has_key) {
|
|
3659
|
+
attributes.push(
|
|
3660
|
+
b.jsx_attribute(
|
|
3661
|
+
b.jsx_id('key'),
|
|
3662
|
+
to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
|
|
3663
|
+
),
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
/**
|
|
3669
|
+
* @param {any} fragment
|
|
3670
|
+
* @param {any} key_expression
|
|
3671
|
+
* @returns {any}
|
|
3672
|
+
*/
|
|
3673
|
+
function keyed_fragment_to_jsx_element(fragment, key_expression) {
|
|
3674
|
+
const name = b.jsx_id('Fragment');
|
|
3675
|
+
const key_attribute = b.jsx_attribute(
|
|
3676
|
+
b.jsx_id('key'),
|
|
3677
|
+
to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
|
|
3678
|
+
);
|
|
3679
|
+
|
|
3680
|
+
return b.jsx_element_fresh(
|
|
3681
|
+
b.jsx_opening_element(name, [key_attribute]),
|
|
3682
|
+
b.jsx_closing_element(clone_jsx_name(name)),
|
|
3683
|
+
fragment.children,
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3189
3687
|
/**
|
|
3190
3688
|
* @param {any} node
|
|
3191
3689
|
* @param {TransformContext} transform_context
|
|
@@ -3473,6 +3971,27 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
|
|
|
3473
3971
|
/** @type {any[]} */
|
|
3474
3972
|
const imports = [];
|
|
3475
3973
|
|
|
3974
|
+
if (transform_context.needs_fragment && platform.imports.fragment) {
|
|
3975
|
+
const fragment_source = platform.imports.fragment;
|
|
3976
|
+
imports.push({
|
|
3977
|
+
type: 'ImportDeclaration',
|
|
3978
|
+
specifiers: [
|
|
3979
|
+
{
|
|
3980
|
+
type: 'ImportSpecifier',
|
|
3981
|
+
imported: { type: 'Identifier', name: 'Fragment', metadata: { path: [] } },
|
|
3982
|
+
local: { type: 'Identifier', name: 'Fragment', metadata: { path: [] } },
|
|
3983
|
+
metadata: { path: [] },
|
|
3984
|
+
},
|
|
3985
|
+
],
|
|
3986
|
+
source: {
|
|
3987
|
+
type: 'Literal',
|
|
3988
|
+
value: fragment_source,
|
|
3989
|
+
raw: `'${fragment_source}'`,
|
|
3990
|
+
},
|
|
3991
|
+
metadata: { path: [] },
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3476
3995
|
if (transform_context.needs_suspense) {
|
|
3477
3996
|
imports.push({
|
|
3478
3997
|
type: 'ImportDeclaration',
|
package/src/utils/builders.js
CHANGED
|
@@ -57,18 +57,21 @@ export function assignment_pattern(left, right) {
|
|
|
57
57
|
/**
|
|
58
58
|
* @param {Array<AST.Pattern>} params
|
|
59
59
|
* @param {AST.BlockStatement | AST.Expression} body
|
|
60
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
60
61
|
* @returns {AST.ArrowFunctionExpression}
|
|
61
62
|
*/
|
|
62
|
-
export function arrow(params, body, async = false) {
|
|
63
|
-
|
|
63
|
+
export function arrow(params, body, async = false, loc_info) {
|
|
64
|
+
const node = /** @type {AST.ArrowFunctionExpression} */ ({
|
|
64
65
|
type: 'ArrowFunctionExpression',
|
|
65
66
|
params,
|
|
66
67
|
body,
|
|
67
68
|
expression: body.type !== 'BlockStatement',
|
|
68
69
|
generator: false,
|
|
69
70
|
async,
|
|
70
|
-
metadata:
|
|
71
|
-
};
|
|
71
|
+
metadata: { path: [] },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return set_location(node, loc_info);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
@@ -1343,6 +1346,15 @@ export const break_statement = {
|
|
|
1343
1346
|
metadata: { path: [] },
|
|
1344
1347
|
};
|
|
1345
1348
|
|
|
1349
|
+
/**
|
|
1350
|
+
* @type {AST.ContinueStatement}
|
|
1351
|
+
*/
|
|
1352
|
+
export const continue_statement = {
|
|
1353
|
+
type: 'ContinueStatement',
|
|
1354
|
+
label: null,
|
|
1355
|
+
metadata: { path: [] },
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1346
1358
|
export {
|
|
1347
1359
|
await_builder as await,
|
|
1348
1360
|
let_builder as let,
|
|
@@ -1352,6 +1364,7 @@ export {
|
|
|
1352
1364
|
true_instance as true,
|
|
1353
1365
|
false_instance as false,
|
|
1354
1366
|
break_statement as break,
|
|
1367
|
+
continue_statement as continue,
|
|
1355
1368
|
for_builder as for,
|
|
1356
1369
|
switch_builder as switch,
|
|
1357
1370
|
function_builder as function,
|
package/src/utils/escaping.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const ATTR_REGEX = /[&"<]/g;
|
|
2
2
|
const CONTENT_REGEX = /[&<]/g;
|
|
3
|
+
const OPEN_TAG_REGEX = /</g;
|
|
4
|
+
const CLOSE_TAG_REGEX = />/g;
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* @template V
|
|
@@ -24,3 +26,12 @@ export function escape(value, is_attr) {
|
|
|
24
26
|
|
|
25
27
|
return escaped + str.substring(last);
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Escapes characters that can prematurely terminate inline script tags.
|
|
32
|
+
* @param {string} str
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function escape_script(str) {
|
|
36
|
+
return str.replace(OPEN_TAG_REGEX, '\\u003c').replace(CLOSE_TAG_REGEX, '\\u003e');
|
|
37
|
+
}
|
package/src/utils/events.js
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ interface BaseNodeMetaData {
|
|
|
74
74
|
returns?: AST.ReturnStatement[];
|
|
75
75
|
has_return?: boolean;
|
|
76
76
|
has_throw?: boolean;
|
|
77
|
+
has_continue?: boolean;
|
|
77
78
|
is_reactive?: boolean;
|
|
78
79
|
lone_return?: boolean;
|
|
79
80
|
forceMapping?: boolean;
|
|
@@ -319,12 +320,13 @@ declare module 'estree' {
|
|
|
319
320
|
*/
|
|
320
321
|
interface Component extends AST.BaseNode {
|
|
321
322
|
type: 'Component';
|
|
322
|
-
// null is for anonymous components
|
|
323
|
+
// null is for anonymous components, e.g. `component(props) => {}`
|
|
323
324
|
id: AST.Identifier | null;
|
|
324
325
|
params: AST.Pattern[];
|
|
325
326
|
body: AST.Node[];
|
|
326
327
|
css: CSS.StyleSheet | null;
|
|
327
328
|
metadata: BaseNodeMetaData & {
|
|
329
|
+
arrow?: boolean;
|
|
328
330
|
topScopedClasses?: TopScopedClasses;
|
|
329
331
|
styleClasses?: StyleClasses;
|
|
330
332
|
};
|
|
@@ -1498,15 +1500,20 @@ export type StyleClasses = Map<string, AST.MemberExpression['property']>;
|
|
|
1498
1500
|
/**
|
|
1499
1501
|
* Event handling types
|
|
1500
1502
|
*/
|
|
1501
|
-
export interface
|
|
1503
|
+
export interface AddEventOptions extends ExtendedEventOptions {
|
|
1502
1504
|
customName?: string;
|
|
1503
|
-
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
export interface AddEventObject extends AddEventOptions {
|
|
1508
|
+
handleEvent(object: Event): void;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
export interface ExtendedEventOptions {
|
|
1512
|
+
capture?: boolean;
|
|
1504
1513
|
once?: boolean;
|
|
1505
1514
|
passive?: boolean;
|
|
1506
1515
|
signal?: AbortSignal;
|
|
1507
|
-
|
|
1508
|
-
// from EventListenerObject
|
|
1509
|
-
handleEvent?(object: Event): void;
|
|
1516
|
+
delegated?: boolean;
|
|
1510
1517
|
}
|
|
1511
1518
|
|
|
1512
1519
|
/**
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface JsxTransformContext {
|
|
|
29
29
|
needs_error_boundary: boolean;
|
|
30
30
|
needs_suspense: boolean;
|
|
31
31
|
needs_merge_refs: boolean;
|
|
32
|
+
needs_fragment: boolean;
|
|
32
33
|
helper_state: {
|
|
33
34
|
base_name: string;
|
|
34
35
|
next_id: number;
|
|
@@ -239,6 +240,11 @@ export interface JsxPlatform {
|
|
|
239
240
|
name: string;
|
|
240
241
|
|
|
241
242
|
imports: {
|
|
243
|
+
/**
|
|
244
|
+
* Module to import `Fragment` from when a keyed fragment is required
|
|
245
|
+
* for a multi-child loop body. React: `'react'`. Preact: `'preact'`.
|
|
246
|
+
*/
|
|
247
|
+
fragment?: string;
|
|
242
248
|
/**
|
|
243
249
|
* Module to import `Suspense` from when a `try { ... } pending { ... }`
|
|
244
250
|
* block appears. React: `'react'`. Preact: `'preact/compat'`.
|
|
@@ -345,4 +351,4 @@ export function componentToFunctionDeclaration(
|
|
|
345
351
|
component: any,
|
|
346
352
|
ctx: any,
|
|
347
353
|
helperState?: any,
|
|
348
|
-
): AST.FunctionDeclaration;
|
|
354
|
+
): AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression;
|