@tsrx/core 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/index.js +22 -0
- package/src/plugin.js +39 -8
- package/src/source-map-utils.js +4 -2
- package/src/transform/jsx/ast-builders.js +321 -0
- package/src/transform/jsx/helpers.js +131 -0
- package/src/transform/jsx/index.js +2486 -0
- package/src/transform/scoping.js +120 -8
- package/src/transform/segments.js +37 -46
- package/types/index.d.ts +24 -10
- package/types/jsx-platform.d.ts +193 -0
- package/types/parse.d.ts +5 -6
|
@@ -0,0 +1,2486 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import * as ESTreeJSX from 'estree-jsx' */
|
|
3
|
+
/** @import { JsxPlatform, JsxTransformOptions, JsxTransformResult } from '@tsrx/core/types' */
|
|
4
|
+
|
|
5
|
+
import { walk } from 'zimmerframe';
|
|
6
|
+
import { print } from 'esrap';
|
|
7
|
+
import {
|
|
8
|
+
ensure_function_metadata,
|
|
9
|
+
in_jsx_child_context,
|
|
10
|
+
tsx_node_to_jsx_expression,
|
|
11
|
+
tsx_with_ts_locations,
|
|
12
|
+
} from './helpers.js';
|
|
13
|
+
import {
|
|
14
|
+
clone_expression_node,
|
|
15
|
+
clone_identifier,
|
|
16
|
+
clone_jsx_name,
|
|
17
|
+
create_compile_error,
|
|
18
|
+
create_generated_identifier,
|
|
19
|
+
create_null_literal,
|
|
20
|
+
flatten_switch_consequent,
|
|
21
|
+
get_for_of_iteration_params,
|
|
22
|
+
identifier_to_jsx_name,
|
|
23
|
+
is_dynamic_element_id,
|
|
24
|
+
is_jsx_child,
|
|
25
|
+
set_loc,
|
|
26
|
+
to_text_expression,
|
|
27
|
+
} from './ast-builders.js';
|
|
28
|
+
import { render_stylesheets as renderStylesheets } from '../stylesheet.js';
|
|
29
|
+
import { set_location as setLocation } from '../../utils/builders.js';
|
|
30
|
+
import {
|
|
31
|
+
apply_lazy_transforms,
|
|
32
|
+
collect_lazy_bindings_from_component,
|
|
33
|
+
preallocate_lazy_ids,
|
|
34
|
+
replace_lazy_params,
|
|
35
|
+
} from '../lazy.js';
|
|
36
|
+
import { find_first_top_level_await_in_component_body } from '../await.js';
|
|
37
|
+
import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
|
|
38
|
+
import {
|
|
39
|
+
is_interleaved_body as is_interleaved_body_core,
|
|
40
|
+
is_capturable_jsx_child,
|
|
41
|
+
capture_jsx_child as captureJsxChild,
|
|
42
|
+
} from '../jsx-interleave.js';
|
|
43
|
+
import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {{
|
|
47
|
+
* platform: JsxPlatform,
|
|
48
|
+
* local_statement_component_index: number,
|
|
49
|
+
* needs_error_boundary: boolean,
|
|
50
|
+
* needs_suspense: boolean,
|
|
51
|
+
* helper_state: { base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] } | null,
|
|
52
|
+
* available_bindings: Map<string, AST.Identifier>,
|
|
53
|
+
* lazy_next_id: number,
|
|
54
|
+
* current_css_hash: string | null,
|
|
55
|
+
* }} TransformContext
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {{ source_name: string, read: () => any }} LazyBinding
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a `transform()` function for a specific JSX platform (React, Preact,
|
|
64
|
+
* Solid). Given a `JsxPlatform` descriptor, returns a transform that parses
|
|
65
|
+
* Ripple's `Component`/`Element`/`Text`/`TSRXExpression` AST into a plain
|
|
66
|
+
* TSX module for that platform.
|
|
67
|
+
*
|
|
68
|
+
* Any `<style>` element declared inside a component is collected, rendered
|
|
69
|
+
* via `@tsrx/core`'s stylesheet renderer, and returned alongside the JS
|
|
70
|
+
* output so a downstream plugin can inject it. The compiler also augments
|
|
71
|
+
* every non-style Element in a scoped component with the stylesheet's hash
|
|
72
|
+
* class so scoped selectors match correctly.
|
|
73
|
+
*
|
|
74
|
+
* @param {JsxPlatform} platform
|
|
75
|
+
* @returns {(ast: AST.Program, source: string, filename?: string, options?: JsxTransformOptions) => JsxTransformResult}
|
|
76
|
+
*/
|
|
77
|
+
export function createJsxTransform(platform) {
|
|
78
|
+
/**
|
|
79
|
+
* @param {AST.Program} ast
|
|
80
|
+
* @param {string} source
|
|
81
|
+
* @param {string} [filename]
|
|
82
|
+
* @param {JsxTransformOptions} [options]
|
|
83
|
+
* @returns {JsxTransformResult}
|
|
84
|
+
*/
|
|
85
|
+
function transform(ast, source, filename, options) {
|
|
86
|
+
const suspense_source = options?.suspenseSource ?? platform.imports.suspense;
|
|
87
|
+
const should_scan_use_server_directive =
|
|
88
|
+
platform.validation.requireUseServerForAwait &&
|
|
89
|
+
(!platform.hooks?.validateComponentAwait ||
|
|
90
|
+
platform.validation.scanUseServerDirectiveForAwaitWithCustomValidator !== false);
|
|
91
|
+
const module_uses_server_directive = should_scan_use_server_directive
|
|
92
|
+
? has_use_server_directive(ast)
|
|
93
|
+
: true;
|
|
94
|
+
/** @type {any[]} */
|
|
95
|
+
const stylesheets = [];
|
|
96
|
+
|
|
97
|
+
/** @type {TransformContext} */
|
|
98
|
+
const transform_context = /** @type {any} */ ({
|
|
99
|
+
platform,
|
|
100
|
+
local_statement_component_index: 0,
|
|
101
|
+
needs_error_boundary: false,
|
|
102
|
+
needs_suspense: false,
|
|
103
|
+
helper_state: null,
|
|
104
|
+
available_bindings: new Map(),
|
|
105
|
+
lazy_next_id: 0,
|
|
106
|
+
current_css_hash: null,
|
|
107
|
+
// Platforms can seed their own tracking state (e.g. solid's
|
|
108
|
+
// needs_show / needs_for flags) via `hooks.initialState`.
|
|
109
|
+
...(platform.hooks?.initialState?.() ?? {}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
|
|
113
|
+
|
|
114
|
+
walk(/** @type {any} */ (ast), transform_context, {
|
|
115
|
+
Component(node, { next, state }) {
|
|
116
|
+
const as_any = /** @type {any} */ (node);
|
|
117
|
+
const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
|
|
118
|
+
|
|
119
|
+
if (await_expression) {
|
|
120
|
+
// Let a platform reject component-level await entirely (solid)
|
|
121
|
+
// or customize the error. Otherwise fall back to the default
|
|
122
|
+
// `requireUseServerForAwait` check.
|
|
123
|
+
if (platform.hooks?.validateComponentAwait) {
|
|
124
|
+
platform.hooks.validateComponentAwait(
|
|
125
|
+
await_expression,
|
|
126
|
+
as_any,
|
|
127
|
+
state,
|
|
128
|
+
module_uses_server_directive,
|
|
129
|
+
source,
|
|
130
|
+
);
|
|
131
|
+
} else if (!module_uses_server_directive) {
|
|
132
|
+
throw create_compile_error(
|
|
133
|
+
await_expression,
|
|
134
|
+
`${platform.name} components can only use \`await\` when the module has a top-level "use server" directive.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
as_any.metadata = /** @type {any} */ ({
|
|
139
|
+
...(as_any.metadata || {}),
|
|
140
|
+
contains_top_level_await: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const css = as_any.css;
|
|
145
|
+
if (css) {
|
|
146
|
+
stylesheets.push(css);
|
|
147
|
+
const hash = css.hash;
|
|
148
|
+
annotate_component_with_hash(
|
|
149
|
+
as_any,
|
|
150
|
+
hash,
|
|
151
|
+
platform.jsx.rewriteClassAttr ? 'className' : 'class',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return next(state);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const transformed = walk(/** @type {any} */ (ast), transform_context, {
|
|
159
|
+
Component(node, { next, state }) {
|
|
160
|
+
const as_any = /** @type {any} */ (node);
|
|
161
|
+
|
|
162
|
+
// Set up helper_state and bindings BEFORE next() so that nested
|
|
163
|
+
// hook_safe_* calls (inside Element children) can register helpers
|
|
164
|
+
// and access available bindings during the bottom-up walk.
|
|
165
|
+
const helper_state = create_helper_state(as_any.id?.name || 'Component');
|
|
166
|
+
const saved_helper_state = state.helper_state;
|
|
167
|
+
const saved_bindings = state.available_bindings;
|
|
168
|
+
const saved_css_hash = state.current_css_hash;
|
|
169
|
+
state.helper_state = helper_state;
|
|
170
|
+
state.current_css_hash = as_any.css ? as_any.css.hash : null;
|
|
171
|
+
|
|
172
|
+
// Pre-collect component body bindings (params + top-level statements)
|
|
173
|
+
// so that Element children processed during the bottom-up walk can see
|
|
174
|
+
// the full scope. Without this, hoisted helpers would miss body-level
|
|
175
|
+
// variables like `const [x] = useState(...)` and produce ReferenceErrors.
|
|
176
|
+
// Only collect up to the split point — bindings declared after a
|
|
177
|
+
// hook-safe split aren't in scope at the return statement and would
|
|
178
|
+
// cause ReferenceErrors if passed as helper props.
|
|
179
|
+
const body_bindings = collect_param_bindings(as_any.params || []);
|
|
180
|
+
const body = as_any.body || [];
|
|
181
|
+
const split_index = find_hook_safe_split_index(body);
|
|
182
|
+
const collect_end = split_index === -1 ? body.length : split_index;
|
|
183
|
+
for (let i = 0; i < collect_end; i += 1) {
|
|
184
|
+
collect_statement_bindings(body[i], body_bindings);
|
|
185
|
+
}
|
|
186
|
+
state.available_bindings = body_bindings;
|
|
187
|
+
|
|
188
|
+
const inner = /** @type {any} */ (next() ?? node);
|
|
189
|
+
|
|
190
|
+
// Restore context
|
|
191
|
+
state.helper_state = saved_helper_state;
|
|
192
|
+
state.available_bindings = saved_bindings;
|
|
193
|
+
state.current_css_hash = saved_css_hash;
|
|
194
|
+
|
|
195
|
+
const convert = platform.hooks?.componentToFunction ?? component_to_function_declaration;
|
|
196
|
+
return /** @type {any} */ (convert(inner, state, helper_state));
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
Tsx(node, { next, path }) {
|
|
200
|
+
const inner = /** @type {any} */ (next() ?? node);
|
|
201
|
+
return /** @type {any} */ (tsx_node_to_jsx_expression(inner, in_jsx_child_context(path)));
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
TsxCompat(node, { next, path }) {
|
|
205
|
+
const inner = /** @type {any} */ (next() ?? node);
|
|
206
|
+
return /** @type {any} */ (
|
|
207
|
+
tsx_compat_node_to_jsx_expression(inner, platform, in_jsx_child_context(path))
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
Element(node, { next, state }) {
|
|
212
|
+
// Capture raw children BEFORE the walker transforms them so a
|
|
213
|
+
// platform hook (e.g. Solid's textContent optimization) can
|
|
214
|
+
// inspect the original Text / TSRXExpression nodes rather than
|
|
215
|
+
// their walker-lowered JSXExpressionContainer equivalents.
|
|
216
|
+
const raw_children = /** @type {any} */ (node).children || [];
|
|
217
|
+
const inner = /** @type {any} */ (next() ?? node);
|
|
218
|
+
const hook = platform.hooks?.transformElement;
|
|
219
|
+
if (hook) return /** @type {any} */ (hook(inner, state, raw_children));
|
|
220
|
+
return /** @type {any} */ (to_jsx_element(inner, state));
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
Text(node, { next }) {
|
|
224
|
+
const inner = /** @type {any} */ (next() ?? node);
|
|
225
|
+
return /** @type {any} */ (
|
|
226
|
+
to_jsx_expression_container(to_text_expression(inner.expression, inner), inner)
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
TSRXExpression(node, { next }) {
|
|
231
|
+
const inner = /** @type {any} */ (next() ?? node);
|
|
232
|
+
return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
MemberExpression(node, { next, state }) {
|
|
236
|
+
const as_any = /** @type {any} */ (node);
|
|
237
|
+
if (as_any.object && as_any.object.type === 'StyleIdentifier' && state.current_css_hash) {
|
|
238
|
+
const class_name = as_any.computed ? as_any.property.value : as_any.property.name;
|
|
239
|
+
const value = `${state.current_css_hash} ${class_name}`;
|
|
240
|
+
return /** @type {any} */ ({ type: 'Literal', value, raw: JSON.stringify(value) });
|
|
241
|
+
}
|
|
242
|
+
return next();
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Default .metadata on every function-like node so downstream consumers
|
|
246
|
+
// (e.g. segments.js reading node.value.metadata.is_component on class
|
|
247
|
+
// methods) don't trip on an undefined metadata object. Ripple's analyze
|
|
248
|
+
// phase does this via visit_function; tsrx-react has no analyze phase.
|
|
249
|
+
FunctionDeclaration: ensure_function_metadata,
|
|
250
|
+
FunctionExpression: ensure_function_metadata,
|
|
251
|
+
ArrowFunctionExpression: ensure_function_metadata,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const expanded = expand_component_helpers(/** @type {AST.Program} */ (transformed));
|
|
255
|
+
if (platform.hooks?.injectImports) {
|
|
256
|
+
platform.hooks.injectImports(expanded, transform_context, suspense_source);
|
|
257
|
+
} else {
|
|
258
|
+
inject_try_imports(expanded, transform_context, platform, suspense_source);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Apply lazy destructuring transforms to module-level code (top-level function
|
|
262
|
+
// declarations, arrow functions, etc.). Component bodies have already been
|
|
263
|
+
// transformed inside component_to_function_declaration; this catches plain
|
|
264
|
+
// functions outside components and any lazy patterns in module scope.
|
|
265
|
+
const final_program = /** @type {any} */ (
|
|
266
|
+
apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
|
|
270
|
+
sourceMapSource: filename,
|
|
271
|
+
sourceMapContent: source,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const css =
|
|
275
|
+
stylesheets.length > 0
|
|
276
|
+
? {
|
|
277
|
+
code: renderStylesheets(
|
|
278
|
+
/** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
|
|
279
|
+
),
|
|
280
|
+
hash: stylesheets.map((s) => s.hash).join(' '),
|
|
281
|
+
}
|
|
282
|
+
: null;
|
|
283
|
+
|
|
284
|
+
return { ast: final_program, code: result.code, map: result.map, css };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return transform;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Detect a top-level `"use server"` directive. Used by platforms whose
|
|
292
|
+
* validation rule requires the directive to enable top-level `await`
|
|
293
|
+
* in components (currently: Preact).
|
|
294
|
+
*
|
|
295
|
+
* @param {AST.Program} program
|
|
296
|
+
* @returns {boolean}
|
|
297
|
+
*/
|
|
298
|
+
function has_use_server_directive(program) {
|
|
299
|
+
for (const statement of program.body || []) {
|
|
300
|
+
const directive = /** @type {any} */ (statement).directive;
|
|
301
|
+
|
|
302
|
+
if (directive === 'use server') {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
statement.type === 'ExpressionStatement' &&
|
|
308
|
+
statement.expression?.type === 'Literal' &&
|
|
309
|
+
statement.expression.value === 'use server'
|
|
310
|
+
) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (directive == null) {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {any} component
|
|
324
|
+
* @param {TransformContext} transform_context
|
|
325
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
|
|
326
|
+
* @returns {AST.FunctionDeclaration}
|
|
327
|
+
*/
|
|
328
|
+
function component_to_function_declaration(component, transform_context, walk_helper_state) {
|
|
329
|
+
const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
|
|
330
|
+
const params = component.params || [];
|
|
331
|
+
const body = /** @type {any[]} */ (component.body || []);
|
|
332
|
+
const is_async_component =
|
|
333
|
+
!!component?.metadata?.contains_top_level_await ||
|
|
334
|
+
find_first_top_level_await_in_component_body(body) !== null;
|
|
335
|
+
|
|
336
|
+
// Collect param bindings from original patterns (lazy patterns still intact).
|
|
337
|
+
const param_bindings = collect_param_bindings(params);
|
|
338
|
+
|
|
339
|
+
// Collect lazy binding info WITHOUT mutating patterns. Stores lazy_id on metadata
|
|
340
|
+
// for later replacement. Body bindings (count, setCount, etc.) are still in the
|
|
341
|
+
// original patterns, so collect_statement_bindings during build will find them.
|
|
342
|
+
const lazy_bindings = collect_lazy_bindings_from_component(params, body, transform_context);
|
|
343
|
+
|
|
344
|
+
// Save and set context for this component scope
|
|
345
|
+
const saved_helper_state = transform_context.helper_state;
|
|
346
|
+
const saved_bindings = transform_context.available_bindings;
|
|
347
|
+
transform_context.helper_state = helper_state;
|
|
348
|
+
transform_context.available_bindings = new Map(param_bindings);
|
|
349
|
+
|
|
350
|
+
const body_statements = build_component_statements(
|
|
351
|
+
body,
|
|
352
|
+
helper_state,
|
|
353
|
+
param_bindings,
|
|
354
|
+
transform_context,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Replace lazy param patterns with generated identifiers
|
|
358
|
+
const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
|
|
359
|
+
|
|
360
|
+
// Wrap body_statements in a BlockStatement so that apply_lazy_transforms
|
|
361
|
+
// runs collect_block_shadowed_names and detects body-level declarations
|
|
362
|
+
// (e.g. `const name = ...`) that shadow lazy binding names.
|
|
363
|
+
const body_block = /** @type {any} */ ({
|
|
364
|
+
type: 'BlockStatement',
|
|
365
|
+
body: body_statements,
|
|
366
|
+
metadata: { path: [] },
|
|
367
|
+
});
|
|
368
|
+
const final_body =
|
|
369
|
+
lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
|
|
370
|
+
|
|
371
|
+
const fn = /** @type {any} */ ({
|
|
372
|
+
type: 'FunctionDeclaration',
|
|
373
|
+
id: component.id,
|
|
374
|
+
params: final_params,
|
|
375
|
+
body: final_body,
|
|
376
|
+
async: is_async_component,
|
|
377
|
+
generator: false,
|
|
378
|
+
metadata: {
|
|
379
|
+
path: [],
|
|
380
|
+
is_component: true,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Restore context
|
|
385
|
+
transform_context.helper_state = saved_helper_state;
|
|
386
|
+
transform_context.available_bindings = saved_bindings;
|
|
387
|
+
|
|
388
|
+
fn.metadata.generated_helpers = helper_state.helpers;
|
|
389
|
+
fn.metadata.generated_statics = helper_state.statics;
|
|
390
|
+
|
|
391
|
+
if (fn.id) {
|
|
392
|
+
fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
|
|
393
|
+
...fn.id.metadata,
|
|
394
|
+
is_component: true,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
setLocation(fn, /** @type {any} */ (component), true);
|
|
399
|
+
return fn;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {any[]} body_nodes
|
|
404
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
405
|
+
* @param {Map<string, AST.Identifier>} available_bindings
|
|
406
|
+
* @param {TransformContext} transform_context
|
|
407
|
+
* @returns {any[]}
|
|
408
|
+
*/
|
|
409
|
+
function build_component_statements(
|
|
410
|
+
body_nodes,
|
|
411
|
+
helper_state,
|
|
412
|
+
available_bindings,
|
|
413
|
+
transform_context,
|
|
414
|
+
) {
|
|
415
|
+
const split_index = find_hook_safe_split_index(body_nodes);
|
|
416
|
+
if (split_index === -1) {
|
|
417
|
+
return build_render_statements(body_nodes, false, transform_context);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const statements = [];
|
|
421
|
+
const render_nodes = [];
|
|
422
|
+
const bindings = new Map(available_bindings);
|
|
423
|
+
|
|
424
|
+
const pre_split_body = body_nodes.slice(0, split_index);
|
|
425
|
+
const interleaved = is_interleaved_body(pre_split_body);
|
|
426
|
+
let capture_index = 0;
|
|
427
|
+
|
|
428
|
+
for (let i = 0; i < split_index; i += 1) {
|
|
429
|
+
const child = body_nodes[i];
|
|
430
|
+
|
|
431
|
+
if (is_bare_return_statement(child)) {
|
|
432
|
+
statements.push(create_component_return_statement(render_nodes, child));
|
|
433
|
+
return statements;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (is_lone_return_if_statement(child)) {
|
|
437
|
+
statements.push(create_component_lone_return_if_statement(child, render_nodes));
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (is_jsx_child(child)) {
|
|
442
|
+
const jsx = to_jsx_child(child, transform_context);
|
|
443
|
+
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
444
|
+
const { declaration, reference } = captureJsxChild(jsx, capture_index++);
|
|
445
|
+
statements.push(declaration);
|
|
446
|
+
render_nodes.push(reference);
|
|
447
|
+
} else {
|
|
448
|
+
render_nodes.push(jsx);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
statements.push(child);
|
|
452
|
+
collect_statement_bindings(child, bindings);
|
|
453
|
+
transform_context.available_bindings = bindings;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!interleaved) {
|
|
458
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const split_node = body_nodes[split_index];
|
|
462
|
+
const consequent_body =
|
|
463
|
+
split_node.consequent.type === 'BlockStatement'
|
|
464
|
+
? split_node.consequent.body
|
|
465
|
+
: [split_node.consequent];
|
|
466
|
+
const short_branch_body = consequent_body.filter(
|
|
467
|
+
(/** @type {any} */ child) => !is_bare_return_statement(child),
|
|
468
|
+
);
|
|
469
|
+
const continuation_body = body_nodes.slice(split_index + 1);
|
|
470
|
+
const short_branch = create_helper_component_expression(
|
|
471
|
+
short_branch_body,
|
|
472
|
+
helper_state,
|
|
473
|
+
bindings,
|
|
474
|
+
split_node.consequent,
|
|
475
|
+
'Exit',
|
|
476
|
+
transform_context,
|
|
477
|
+
);
|
|
478
|
+
const continuation = create_helper_component_expression(
|
|
479
|
+
continuation_body,
|
|
480
|
+
helper_state,
|
|
481
|
+
bindings,
|
|
482
|
+
split_node,
|
|
483
|
+
'Continue',
|
|
484
|
+
transform_context,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
render_nodes.push(
|
|
488
|
+
to_jsx_expression_container(
|
|
489
|
+
set_loc(
|
|
490
|
+
/** @type {any} */ ({
|
|
491
|
+
type: 'ConditionalExpression',
|
|
492
|
+
test: split_node.test,
|
|
493
|
+
consequent: short_branch,
|
|
494
|
+
alternate: continuation,
|
|
495
|
+
metadata: { path: [] },
|
|
496
|
+
}),
|
|
497
|
+
split_node,
|
|
498
|
+
),
|
|
499
|
+
split_node,
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
statements.push(create_component_return_statement(render_nodes, split_node));
|
|
504
|
+
return statements;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* @param {any[]} body_nodes
|
|
509
|
+
* @param {boolean} return_null_when_empty
|
|
510
|
+
* @param {TransformContext} transform_context
|
|
511
|
+
* @returns {any[]}
|
|
512
|
+
*/
|
|
513
|
+
function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
|
|
514
|
+
const statements = [];
|
|
515
|
+
const render_nodes = [];
|
|
516
|
+
|
|
517
|
+
// Create a new bindings map so inner-scope bindings from
|
|
518
|
+
// collect_statement_bindings don't leak to the caller's scope.
|
|
519
|
+
const saved_bindings = transform_context.available_bindings;
|
|
520
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
521
|
+
|
|
522
|
+
// When non-JSX statements are interleaved with JSX children, we must
|
|
523
|
+
// preserve source order so each JSX expression sees the variable state
|
|
524
|
+
// at its textual position. Otherwise statements would all run before
|
|
525
|
+
// any JSX is constructed, and every JSX child would observe the final
|
|
526
|
+
// state of mutable variables.
|
|
527
|
+
const interleaved = is_interleaved_body(body_nodes);
|
|
528
|
+
let capture_index = 0;
|
|
529
|
+
|
|
530
|
+
for (const child of body_nodes) {
|
|
531
|
+
if (is_bare_return_statement(child)) {
|
|
532
|
+
statements.push(create_component_return_statement(render_nodes, child));
|
|
533
|
+
transform_context.available_bindings = saved_bindings;
|
|
534
|
+
return statements;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (is_lone_return_if_statement(child)) {
|
|
538
|
+
statements.push(create_component_lone_return_if_statement(child, render_nodes));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (is_jsx_child(child)) {
|
|
543
|
+
const jsx = to_jsx_child(child, transform_context);
|
|
544
|
+
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
545
|
+
const { declaration, reference } = captureJsxChild(jsx, capture_index++);
|
|
546
|
+
statements.push(declaration);
|
|
547
|
+
render_nodes.push(reference);
|
|
548
|
+
} else {
|
|
549
|
+
render_nodes.push(jsx);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
statements.push(child);
|
|
553
|
+
collect_statement_bindings(child, transform_context.available_bindings);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!interleaved) {
|
|
558
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const return_arg = build_return_expression(render_nodes);
|
|
562
|
+
if (return_arg || return_null_when_empty) {
|
|
563
|
+
statements.push({
|
|
564
|
+
type: 'ReturnStatement',
|
|
565
|
+
argument: return_arg || { type: 'Literal', value: null, raw: 'null' },
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
transform_context.available_bindings = saved_bindings;
|
|
570
|
+
return statements;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* React-specific wrapper around the core `isInterleavedBody` helper that
|
|
575
|
+
* ignores bare `return` / lone return-if statements. Those are rewriting
|
|
576
|
+
* signals rather than user-visible side effects, so JSX children around
|
|
577
|
+
* them don't need capturing.
|
|
578
|
+
*
|
|
579
|
+
* @param {any[]} body_nodes
|
|
580
|
+
* @returns {boolean}
|
|
581
|
+
*/
|
|
582
|
+
function is_interleaved_body(body_nodes) {
|
|
583
|
+
const filtered = body_nodes.filter(
|
|
584
|
+
(child) => !is_bare_return_statement(child) && !is_lone_return_if_statement(child),
|
|
585
|
+
);
|
|
586
|
+
return is_interleaved_body_core(filtered, is_jsx_child);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* @param {any[]} body_nodes
|
|
591
|
+
* @returns {number}
|
|
592
|
+
*/
|
|
593
|
+
function find_hook_safe_split_index(body_nodes) {
|
|
594
|
+
for (let i = 0; i < body_nodes.length; i += 1) {
|
|
595
|
+
if (!is_lone_return_if_statement(body_nodes[i])) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (body_contains_top_level_hook_call(body_nodes.slice(i + 1))) {
|
|
600
|
+
return i;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return -1;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* @param {any[]} body_nodes
|
|
609
|
+
* @returns {boolean}
|
|
610
|
+
*/
|
|
611
|
+
function body_contains_top_level_hook_call(body_nodes) {
|
|
612
|
+
return body_nodes.some(statement_contains_top_level_hook_call);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @param {any} node
|
|
617
|
+
* @returns {boolean}
|
|
618
|
+
*/
|
|
619
|
+
function statement_contains_top_level_hook_call(node) {
|
|
620
|
+
return node_contains_top_level_hook_call(node, false);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* @param {any} node
|
|
625
|
+
* @param {boolean} inside_nested_function
|
|
626
|
+
* @returns {boolean}
|
|
627
|
+
*/
|
|
628
|
+
function node_contains_top_level_hook_call(node, inside_nested_function) {
|
|
629
|
+
if (!node || typeof node !== 'object') {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (
|
|
634
|
+
inside_nested_function &&
|
|
635
|
+
(node.type === 'FunctionDeclaration' ||
|
|
636
|
+
node.type === 'FunctionExpression' ||
|
|
637
|
+
node.type === 'ArrowFunctionExpression')
|
|
638
|
+
) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (
|
|
643
|
+
node.type === 'FunctionDeclaration' ||
|
|
644
|
+
node.type === 'FunctionExpression' ||
|
|
645
|
+
node.type === 'ArrowFunctionExpression'
|
|
646
|
+
) {
|
|
647
|
+
const next_inside_nested_function = true;
|
|
648
|
+
for (const key of Object.keys(node)) {
|
|
649
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (node_contains_top_level_hook_call(node[key], next_inside_nested_function)) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!inside_nested_function && node.type === 'CallExpression' && is_hook_callee(node.callee)) {
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (Array.isArray(node)) {
|
|
664
|
+
return node.some((child) => node_contains_top_level_hook_call(child, inside_nested_function));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
for (const key of Object.keys(node)) {
|
|
668
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (node_contains_top_level_hook_call(node[key], inside_nested_function)) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* @param {any} callee
|
|
681
|
+
* @returns {boolean}
|
|
682
|
+
*/
|
|
683
|
+
function is_hook_callee(callee) {
|
|
684
|
+
if (!callee) return false;
|
|
685
|
+
|
|
686
|
+
if (callee.type === 'Identifier') {
|
|
687
|
+
return /^use[A-Z0-9]/.test(callee.name);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (
|
|
691
|
+
!callee.computed &&
|
|
692
|
+
callee.type === 'MemberExpression' &&
|
|
693
|
+
callee.property?.type === 'Identifier'
|
|
694
|
+
) {
|
|
695
|
+
return /^use[A-Z0-9]/.test(callee.property.name);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param {any[]} body_nodes
|
|
703
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
704
|
+
* @param {Map<string, AST.Identifier>} available_bindings
|
|
705
|
+
* @param {any} source_node
|
|
706
|
+
* @param {string} suffix
|
|
707
|
+
* @param {TransformContext} transform_context
|
|
708
|
+
* @returns {any}
|
|
709
|
+
*/
|
|
710
|
+
function create_helper_component_expression(
|
|
711
|
+
body_nodes,
|
|
712
|
+
helper_state,
|
|
713
|
+
available_bindings,
|
|
714
|
+
source_node,
|
|
715
|
+
suffix,
|
|
716
|
+
transform_context,
|
|
717
|
+
) {
|
|
718
|
+
if (body_nodes.length === 0) {
|
|
719
|
+
return create_null_literal();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const helper_name = create_helper_name(helper_state, suffix);
|
|
723
|
+
const helper_id = set_loc(create_generated_identifier(helper_name), source_node);
|
|
724
|
+
const helper_bindings = Array.from(available_bindings.values());
|
|
725
|
+
const helper_fn = create_helper_function_declaration(
|
|
726
|
+
helper_id,
|
|
727
|
+
body_nodes,
|
|
728
|
+
helper_state,
|
|
729
|
+
available_bindings,
|
|
730
|
+
helper_bindings,
|
|
731
|
+
source_node,
|
|
732
|
+
transform_context,
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
helper_state.helpers.push(helper_fn);
|
|
736
|
+
|
|
737
|
+
return create_helper_component_element(helper_id, helper_bindings, source_node);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* @param {AST.Identifier} helper_id
|
|
742
|
+
* @param {any[]} body_nodes
|
|
743
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
744
|
+
* @param {Map<string, AST.Identifier>} available_bindings
|
|
745
|
+
* @param {AST.Identifier[]} helper_bindings
|
|
746
|
+
* @param {any} source_node
|
|
747
|
+
* @param {TransformContext} transform_context
|
|
748
|
+
* @returns {AST.FunctionDeclaration}
|
|
749
|
+
*/
|
|
750
|
+
function create_helper_function_declaration(
|
|
751
|
+
helper_id,
|
|
752
|
+
body_nodes,
|
|
753
|
+
helper_state,
|
|
754
|
+
available_bindings,
|
|
755
|
+
helper_bindings,
|
|
756
|
+
source_node,
|
|
757
|
+
transform_context,
|
|
758
|
+
) {
|
|
759
|
+
const fn = /** @type {any} */ ({
|
|
760
|
+
type: 'FunctionDeclaration',
|
|
761
|
+
id: helper_id,
|
|
762
|
+
params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
|
|
763
|
+
body: {
|
|
764
|
+
type: 'BlockStatement',
|
|
765
|
+
body: build_component_statements(
|
|
766
|
+
body_nodes,
|
|
767
|
+
helper_state,
|
|
768
|
+
new Map(available_bindings),
|
|
769
|
+
transform_context,
|
|
770
|
+
),
|
|
771
|
+
metadata: { path: [] },
|
|
772
|
+
},
|
|
773
|
+
async: false,
|
|
774
|
+
generator: false,
|
|
775
|
+
metadata: {
|
|
776
|
+
path: [],
|
|
777
|
+
is_component: true,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
if (fn.id) {
|
|
782
|
+
fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
|
|
783
|
+
...fn.id.metadata,
|
|
784
|
+
is_component: true,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return set_loc(fn, source_node);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* @param {AST.Identifier[]} bindings
|
|
793
|
+
* @returns {AST.ObjectPattern}
|
|
794
|
+
*/
|
|
795
|
+
function create_helper_props_pattern(bindings) {
|
|
796
|
+
return /** @type {any} */ ({
|
|
797
|
+
type: 'ObjectPattern',
|
|
798
|
+
properties: bindings.map((binding) => create_helper_props_property(binding)),
|
|
799
|
+
metadata: { path: [] },
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @param {AST.Identifier} binding
|
|
805
|
+
* @returns {AST.Property}
|
|
806
|
+
*/
|
|
807
|
+
function create_helper_props_property(binding) {
|
|
808
|
+
const key = clone_identifier(binding);
|
|
809
|
+
const value = clone_identifier(binding);
|
|
810
|
+
|
|
811
|
+
return /** @type {any} */ ({
|
|
812
|
+
type: 'Property',
|
|
813
|
+
key,
|
|
814
|
+
value,
|
|
815
|
+
kind: 'init',
|
|
816
|
+
method: false,
|
|
817
|
+
shorthand: true,
|
|
818
|
+
computed: false,
|
|
819
|
+
metadata: { path: [] },
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* @param {AST.Identifier} helper_id
|
|
825
|
+
* @param {AST.Identifier[]} bindings
|
|
826
|
+
* @param {any} source_node
|
|
827
|
+
* @returns {ESTreeJSX.JSXElement}
|
|
828
|
+
*/
|
|
829
|
+
function create_helper_component_element(helper_id, bindings, source_node) {
|
|
830
|
+
const attributes = bindings.map(
|
|
831
|
+
(binding) =>
|
|
832
|
+
/** @type {any} */ ({
|
|
833
|
+
type: 'JSXAttribute',
|
|
834
|
+
name: identifier_to_jsx_name(clone_identifier(binding)),
|
|
835
|
+
value: to_jsx_expression_container(clone_identifier(binding), binding),
|
|
836
|
+
metadata: { path: [] },
|
|
837
|
+
}),
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
return set_loc(
|
|
841
|
+
/** @type {any} */ ({
|
|
842
|
+
type: 'JSXElement',
|
|
843
|
+
openingElement: set_loc(
|
|
844
|
+
{
|
|
845
|
+
type: 'JSXOpeningElement',
|
|
846
|
+
name: identifier_to_jsx_name(clone_identifier(helper_id)),
|
|
847
|
+
attributes,
|
|
848
|
+
selfClosing: true,
|
|
849
|
+
metadata: { path: [] },
|
|
850
|
+
},
|
|
851
|
+
source_node,
|
|
852
|
+
),
|
|
853
|
+
closingElement: null,
|
|
854
|
+
children: [],
|
|
855
|
+
metadata: { path: [] },
|
|
856
|
+
}),
|
|
857
|
+
source_node,
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
863
|
+
* @param {string} suffix
|
|
864
|
+
* @returns {string}
|
|
865
|
+
*/
|
|
866
|
+
function create_helper_name(helper_state, suffix) {
|
|
867
|
+
helper_state.next_id += 1;
|
|
868
|
+
return `${helper_state.base_name}__${suffix}${helper_state.next_id}`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* @param {string} base_name
|
|
873
|
+
* @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }}
|
|
874
|
+
*/
|
|
875
|
+
function create_helper_state(base_name) {
|
|
876
|
+
return {
|
|
877
|
+
base_name,
|
|
878
|
+
next_id: 0,
|
|
879
|
+
helpers: [],
|
|
880
|
+
statics: [],
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* @param {any[]} params
|
|
886
|
+
* @returns {Map<string, AST.Identifier>}
|
|
887
|
+
*/
|
|
888
|
+
function collect_param_bindings(params) {
|
|
889
|
+
const bindings = new Map();
|
|
890
|
+
for (const param of params) {
|
|
891
|
+
collect_pattern_bindings(param, bindings);
|
|
892
|
+
}
|
|
893
|
+
return bindings;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* @param {any} statement
|
|
898
|
+
* @param {Map<string, AST.Identifier>} bindings
|
|
899
|
+
* @returns {void}
|
|
900
|
+
*/
|
|
901
|
+
function collect_statement_bindings(statement, bindings) {
|
|
902
|
+
if (!statement) return;
|
|
903
|
+
|
|
904
|
+
if (statement.type === 'VariableDeclaration') {
|
|
905
|
+
for (const declaration of statement.declarations || []) {
|
|
906
|
+
collect_pattern_bindings(declaration.id, bindings);
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (
|
|
912
|
+
(statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
|
|
913
|
+
statement.id
|
|
914
|
+
) {
|
|
915
|
+
bindings.set(statement.id.name, statement.id);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Statement-level lazy assignment: `&[x] = expr;` introduces `x` as a binding.
|
|
919
|
+
if (
|
|
920
|
+
statement.type === 'ExpressionStatement' &&
|
|
921
|
+
statement.expression?.type === 'AssignmentExpression' &&
|
|
922
|
+
statement.expression.operator === '=' &&
|
|
923
|
+
(statement.expression.left?.type === 'ObjectPattern' ||
|
|
924
|
+
statement.expression.left?.type === 'ArrayPattern') &&
|
|
925
|
+
statement.expression.left.lazy
|
|
926
|
+
) {
|
|
927
|
+
collect_pattern_bindings(statement.expression.left, bindings);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* @param {any} pattern
|
|
933
|
+
* @param {Map<string, AST.Identifier>} bindings
|
|
934
|
+
* @returns {void}
|
|
935
|
+
*/
|
|
936
|
+
function collect_pattern_bindings(pattern, bindings) {
|
|
937
|
+
if (!pattern || typeof pattern !== 'object') return;
|
|
938
|
+
|
|
939
|
+
if (pattern.type === 'Identifier') {
|
|
940
|
+
bindings.set(pattern.name, pattern);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (pattern.type === 'RestElement') {
|
|
945
|
+
collect_pattern_bindings(pattern.argument, bindings);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (pattern.type === 'AssignmentPattern') {
|
|
950
|
+
collect_pattern_bindings(pattern.left, bindings);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (pattern.type === 'ArrayPattern') {
|
|
955
|
+
for (const element of pattern.elements || []) {
|
|
956
|
+
collect_pattern_bindings(element, bindings);
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (pattern.type === 'ObjectPattern') {
|
|
962
|
+
for (const property of pattern.properties || []) {
|
|
963
|
+
if (property.type === 'RestElement') {
|
|
964
|
+
collect_pattern_bindings(property.argument, bindings);
|
|
965
|
+
} else {
|
|
966
|
+
collect_pattern_bindings(property.value, bindings);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Check if a node references any of the given scope bindings.
|
|
974
|
+
* Used to determine if a JSX element is static and can be hoisted to module level.
|
|
975
|
+
*
|
|
976
|
+
* @param {any} node
|
|
977
|
+
* @param {Map<string, AST.Identifier>} scope_bindings
|
|
978
|
+
* @returns {boolean}
|
|
979
|
+
*/
|
|
980
|
+
function references_scope_bindings(node, scope_bindings) {
|
|
981
|
+
if (!node || typeof node !== 'object') return false;
|
|
982
|
+
if (scope_bindings.size === 0) return false;
|
|
983
|
+
|
|
984
|
+
if (node.type === 'Identifier') {
|
|
985
|
+
return scope_bindings.has(node.name);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// JSXIdentifier is a variable reference when capitalized (tag name like <MyComponent />)
|
|
989
|
+
// or when it's the object of a JSXMemberExpression (e.g. ui in <ui.Button />)
|
|
990
|
+
if (node.type === 'JSXIdentifier') {
|
|
991
|
+
return scope_bindings.has(node.name);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (Array.isArray(node)) {
|
|
995
|
+
return node.some((child) => references_scope_bindings(child, scope_bindings));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
for (const key of Object.keys(node)) {
|
|
999
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
1000
|
+
|
|
1001
|
+
// Skip non-computed, non-shorthand property keys (they are labels, not references)
|
|
1002
|
+
if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) continue;
|
|
1003
|
+
|
|
1004
|
+
// Skip non-computed member expression property access
|
|
1005
|
+
if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue;
|
|
1006
|
+
|
|
1007
|
+
// Skip JSXMemberExpression property (e.g. Button in <Icons.Button /> is a label, not a reference)
|
|
1008
|
+
if (key === 'property' && node.type === 'JSXMemberExpression') continue;
|
|
1009
|
+
|
|
1010
|
+
// Skip JSXAttribute names — they are attribute labels, not variable references
|
|
1011
|
+
if (key === 'name' && node.type === 'JSXAttribute') continue;
|
|
1012
|
+
|
|
1013
|
+
if (references_scope_bindings(node[key], scope_bindings)) return true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Hoist static JSX elements from render_nodes to module level.
|
|
1021
|
+
* A JSX element is static if it doesn't reference any component-scope bindings.
|
|
1022
|
+
* Hoisting prevents React from recreating the element on every render, allowing
|
|
1023
|
+
* the reconciler to skip diffing when it sees the same element identity.
|
|
1024
|
+
*
|
|
1025
|
+
* @param {any[]} render_nodes
|
|
1026
|
+
* @param {TransformContext} transform_context
|
|
1027
|
+
*/
|
|
1028
|
+
function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
1029
|
+
if (!transform_context.helper_state) return;
|
|
1030
|
+
|
|
1031
|
+
for (let i = 0; i < render_nodes.length; i++) {
|
|
1032
|
+
const node = render_nodes[i];
|
|
1033
|
+
if (node.type !== 'JSXElement') continue;
|
|
1034
|
+
if (!is_hoist_safe_jsx_node(node)) continue;
|
|
1035
|
+
if (references_scope_bindings(node, transform_context.available_bindings)) continue;
|
|
1036
|
+
|
|
1037
|
+
const name = create_helper_name(transform_context.helper_state, 'static');
|
|
1038
|
+
const id = create_generated_identifier(name);
|
|
1039
|
+
|
|
1040
|
+
transform_context.helper_state.statics.push(
|
|
1041
|
+
/** @type {any} */ ({
|
|
1042
|
+
type: 'VariableDeclaration',
|
|
1043
|
+
kind: 'const',
|
|
1044
|
+
declarations: [
|
|
1045
|
+
{
|
|
1046
|
+
type: 'VariableDeclarator',
|
|
1047
|
+
id,
|
|
1048
|
+
init: node,
|
|
1049
|
+
metadata: { path: [] },
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
metadata: { path: [] },
|
|
1053
|
+
}),
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* @param {AST.Program} program
|
|
1062
|
+
* @returns {AST.Program}
|
|
1063
|
+
*/
|
|
1064
|
+
function expand_component_helpers(program) {
|
|
1065
|
+
program.body = program.body.flatMap((statement) => {
|
|
1066
|
+
if (statement.type === 'FunctionDeclaration') {
|
|
1067
|
+
const meta = /** @type {any} */ (statement.metadata);
|
|
1068
|
+
const statics = meta?.generated_statics || [];
|
|
1069
|
+
const helpers = meta?.generated_helpers || [];
|
|
1070
|
+
if (statics.length || helpers.length) {
|
|
1071
|
+
return [...statics, ...helpers, statement];
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (
|
|
1076
|
+
(statement.type === 'ExportNamedDeclaration' ||
|
|
1077
|
+
statement.type === 'ExportDefaultDeclaration') &&
|
|
1078
|
+
statement.declaration?.type === 'FunctionDeclaration'
|
|
1079
|
+
) {
|
|
1080
|
+
const meta = /** @type {any} */ (statement.declaration.metadata);
|
|
1081
|
+
const statics = meta?.generated_statics || [];
|
|
1082
|
+
const helpers = meta?.generated_helpers || [];
|
|
1083
|
+
if (statics.length || helpers.length) {
|
|
1084
|
+
return [...statics, ...helpers, statement];
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return [statement];
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
return program;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* @param {any} node
|
|
1096
|
+
* @returns {boolean}
|
|
1097
|
+
*/
|
|
1098
|
+
function is_bare_return_statement(node) {
|
|
1099
|
+
return node?.type === 'ReturnStatement' && node.argument == null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* @param {any} node
|
|
1104
|
+
* @returns {boolean}
|
|
1105
|
+
*/
|
|
1106
|
+
function is_lone_return_if_statement(node) {
|
|
1107
|
+
if (node?.type !== 'IfStatement' || node.alternate) {
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const consequent_body =
|
|
1112
|
+
node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
|
|
1113
|
+
|
|
1114
|
+
return consequent_body.length === 1 && is_bare_return_statement(consequent_body[0]);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* @param {any[]} render_nodes
|
|
1119
|
+
* @param {any} source_node
|
|
1120
|
+
* @returns {any}
|
|
1121
|
+
*/
|
|
1122
|
+
function create_component_return_statement(render_nodes, source_node) {
|
|
1123
|
+
return /** @type {any} */ ({
|
|
1124
|
+
type: 'ReturnStatement',
|
|
1125
|
+
argument: build_return_expression(render_nodes.slice()) || {
|
|
1126
|
+
type: 'Literal',
|
|
1127
|
+
value: null,
|
|
1128
|
+
raw: 'null',
|
|
1129
|
+
metadata: { path: [] },
|
|
1130
|
+
},
|
|
1131
|
+
metadata: { path: [] },
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* @param {any} node
|
|
1137
|
+
* @param {any[]} render_nodes
|
|
1138
|
+
* @returns {any}
|
|
1139
|
+
*/
|
|
1140
|
+
function create_component_lone_return_if_statement(node, render_nodes) {
|
|
1141
|
+
const consequent_body =
|
|
1142
|
+
node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
|
|
1143
|
+
|
|
1144
|
+
return set_loc(
|
|
1145
|
+
/** @type {any} */ ({
|
|
1146
|
+
type: 'IfStatement',
|
|
1147
|
+
test: node.test,
|
|
1148
|
+
consequent: set_loc(
|
|
1149
|
+
/** @type {any} */ ({
|
|
1150
|
+
type: 'BlockStatement',
|
|
1151
|
+
body: [create_component_return_statement(render_nodes, consequent_body[0])],
|
|
1152
|
+
metadata: { path: [] },
|
|
1153
|
+
}),
|
|
1154
|
+
node.consequent,
|
|
1155
|
+
),
|
|
1156
|
+
alternate: null,
|
|
1157
|
+
metadata: { path: [] },
|
|
1158
|
+
}),
|
|
1159
|
+
node,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const TEMPLATE_FRAGMENT_ERROR =
|
|
1164
|
+
'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* @param {any} node
|
|
1168
|
+
* @param {TransformContext} transform_context
|
|
1169
|
+
* @returns {any}
|
|
1170
|
+
*/
|
|
1171
|
+
function to_jsx_element(node, transform_context) {
|
|
1172
|
+
if (node.type === 'JSXElement') return node;
|
|
1173
|
+
if ((node.children || []).some((/** @type {any} */ c) => c && c.type === 'Html')) {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
if (!node.id) {
|
|
1179
|
+
throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
|
|
1180
|
+
}
|
|
1181
|
+
if (is_dynamic_element_id(node.id)) {
|
|
1182
|
+
return dynamic_element_to_jsx_child(node, transform_context);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const name = identifier_to_jsx_name(node.id);
|
|
1186
|
+
const attributes = transform_element_attributes_dispatch(
|
|
1187
|
+
node.attributes || [],
|
|
1188
|
+
transform_context,
|
|
1189
|
+
node,
|
|
1190
|
+
);
|
|
1191
|
+
const selfClosing = !!node.selfClosing;
|
|
1192
|
+
const children = create_element_children(node.children || [], transform_context);
|
|
1193
|
+
const has_unmappable_attribute = attributes.some(
|
|
1194
|
+
(/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
/** @type {ESTreeJSX.JSXOpeningElement} */
|
|
1198
|
+
const openingElement = /** @type {ESTreeJSX.JSXOpeningElement} */ (
|
|
1199
|
+
has_unmappable_attribute
|
|
1200
|
+
? {
|
|
1201
|
+
type: 'JSXOpeningElement',
|
|
1202
|
+
name,
|
|
1203
|
+
attributes,
|
|
1204
|
+
selfClosing,
|
|
1205
|
+
metadata: { path: [] },
|
|
1206
|
+
}
|
|
1207
|
+
: set_loc(
|
|
1208
|
+
/** @type {any} */ ({
|
|
1209
|
+
type: 'JSXOpeningElement',
|
|
1210
|
+
name,
|
|
1211
|
+
attributes,
|
|
1212
|
+
selfClosing,
|
|
1213
|
+
}),
|
|
1214
|
+
node.openingElement || node,
|
|
1215
|
+
)
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
/** @type {ESTreeJSX.JSXClosingElement | null} */
|
|
1219
|
+
const closingElement = selfClosing
|
|
1220
|
+
? null
|
|
1221
|
+
: set_loc(
|
|
1222
|
+
/** @type {any} */ ({
|
|
1223
|
+
type: 'JSXClosingElement',
|
|
1224
|
+
name: clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
|
|
1225
|
+
}),
|
|
1226
|
+
node.closingElement || node,
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
return set_loc(
|
|
1230
|
+
/** @type {any} */ ({
|
|
1231
|
+
type: 'JSXElement',
|
|
1232
|
+
openingElement,
|
|
1233
|
+
closingElement,
|
|
1234
|
+
children,
|
|
1235
|
+
}),
|
|
1236
|
+
node,
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* @param {any[]} children
|
|
1242
|
+
* @param {TransformContext} transform_context
|
|
1243
|
+
* @returns {any[]}
|
|
1244
|
+
*/
|
|
1245
|
+
|
|
1246
|
+
function create_element_children(children, transform_context) {
|
|
1247
|
+
if (children.length === 0) {
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (children.every(is_inline_element_child) && !children_contain_return_semantics(children)) {
|
|
1252
|
+
return children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return [statement_body_to_jsx_child(children, transform_context)];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* @param {any[]} children
|
|
1260
|
+
* @returns {boolean}
|
|
1261
|
+
*/
|
|
1262
|
+
function children_contain_return_semantics(children) {
|
|
1263
|
+
return children.some(child_contains_return_semantics);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* @param {any} node
|
|
1268
|
+
* @returns {boolean}
|
|
1269
|
+
*/
|
|
1270
|
+
function child_contains_return_semantics(node) {
|
|
1271
|
+
if (!node || typeof node !== 'object') {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (node.type === 'ReturnStatement' || is_lone_return_if_statement(node)) {
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (
|
|
1280
|
+
node.type === 'FunctionDeclaration' ||
|
|
1281
|
+
node.type === 'FunctionExpression' ||
|
|
1282
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
1283
|
+
node.type === 'Component'
|
|
1284
|
+
) {
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (Array.isArray(node)) {
|
|
1289
|
+
return node.some(child_contains_return_semantics);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
for (const key of Object.keys(node)) {
|
|
1293
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (child_contains_return_semantics(node[key])) {
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* @param {any} node
|
|
1306
|
+
* @returns {boolean}
|
|
1307
|
+
*/
|
|
1308
|
+
function is_inline_element_child(node) {
|
|
1309
|
+
return node && is_jsx_child(node);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* @param {any[]} body_nodes
|
|
1314
|
+
* @param {TransformContext} transform_context
|
|
1315
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1316
|
+
*/
|
|
1317
|
+
function statement_body_to_jsx_child(body_nodes, transform_context) {
|
|
1318
|
+
if (body_contains_top_level_hook_call(body_nodes)) {
|
|
1319
|
+
return hook_safe_statement_body_to_jsx_child(body_nodes, transform_context);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return to_jsx_expression_container(
|
|
1323
|
+
/** @type {any} */ ({
|
|
1324
|
+
type: 'CallExpression',
|
|
1325
|
+
callee: {
|
|
1326
|
+
type: 'ArrowFunctionExpression',
|
|
1327
|
+
params: [],
|
|
1328
|
+
body: /** @type {any} */ ({
|
|
1329
|
+
type: 'BlockStatement',
|
|
1330
|
+
body: build_render_statements(body_nodes, true, transform_context),
|
|
1331
|
+
metadata: { path: [] },
|
|
1332
|
+
}),
|
|
1333
|
+
async: false,
|
|
1334
|
+
generator: false,
|
|
1335
|
+
expression: false,
|
|
1336
|
+
metadata: { path: [] },
|
|
1337
|
+
},
|
|
1338
|
+
arguments: [],
|
|
1339
|
+
optional: false,
|
|
1340
|
+
metadata: { path: [] },
|
|
1341
|
+
}),
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* @param {any[]} body_nodes
|
|
1347
|
+
* @param {TransformContext} transform_context
|
|
1348
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1349
|
+
*/
|
|
1350
|
+
function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
|
|
1351
|
+
const source_node = get_body_source_node(body_nodes);
|
|
1352
|
+
const helper_id = set_loc(
|
|
1353
|
+
create_generated_identifier(create_local_statement_component_name(transform_context)),
|
|
1354
|
+
source_node,
|
|
1355
|
+
);
|
|
1356
|
+
const helper_bindings = Array.from(transform_context.available_bindings.values());
|
|
1357
|
+
|
|
1358
|
+
// Save and isolate bindings for the helper body
|
|
1359
|
+
const saved_bindings = transform_context.available_bindings;
|
|
1360
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
1361
|
+
|
|
1362
|
+
const helper_fn = set_loc(
|
|
1363
|
+
/** @type {any} */ ({
|
|
1364
|
+
type: 'FunctionDeclaration',
|
|
1365
|
+
id: helper_id,
|
|
1366
|
+
params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
|
|
1367
|
+
body: {
|
|
1368
|
+
type: 'BlockStatement',
|
|
1369
|
+
body: build_render_statements(body_nodes, true, transform_context),
|
|
1370
|
+
metadata: { path: [] },
|
|
1371
|
+
},
|
|
1372
|
+
async: false,
|
|
1373
|
+
generator: false,
|
|
1374
|
+
metadata: {
|
|
1375
|
+
path: [],
|
|
1376
|
+
is_component: true,
|
|
1377
|
+
is_method: false,
|
|
1378
|
+
},
|
|
1379
|
+
}),
|
|
1380
|
+
source_node,
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
// Restore bindings
|
|
1384
|
+
transform_context.available_bindings = saved_bindings;
|
|
1385
|
+
|
|
1386
|
+
// Register helper for hoisting to module level
|
|
1387
|
+
if (transform_context.helper_state) {
|
|
1388
|
+
transform_context.helper_state.helpers.push(helper_fn);
|
|
1389
|
+
|
|
1390
|
+
return to_jsx_expression_container(
|
|
1391
|
+
/** @type {any} */ (create_helper_component_element(helper_id, helper_bindings, source_node)),
|
|
1392
|
+
source_node,
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return to_jsx_expression_container(
|
|
1397
|
+
/** @type {any} */ ({
|
|
1398
|
+
type: 'CallExpression',
|
|
1399
|
+
callee: {
|
|
1400
|
+
type: 'ArrowFunctionExpression',
|
|
1401
|
+
params: [],
|
|
1402
|
+
body: /** @type {any} */ ({
|
|
1403
|
+
type: 'BlockStatement',
|
|
1404
|
+
body: [
|
|
1405
|
+
helper_fn,
|
|
1406
|
+
{
|
|
1407
|
+
type: 'ReturnStatement',
|
|
1408
|
+
argument: create_helper_component_element(helper_id, helper_bindings, source_node),
|
|
1409
|
+
metadata: { path: [] },
|
|
1410
|
+
},
|
|
1411
|
+
],
|
|
1412
|
+
metadata: { path: [] },
|
|
1413
|
+
}),
|
|
1414
|
+
async: false,
|
|
1415
|
+
generator: false,
|
|
1416
|
+
expression: false,
|
|
1417
|
+
metadata: { path: [] },
|
|
1418
|
+
},
|
|
1419
|
+
arguments: [],
|
|
1420
|
+
optional: false,
|
|
1421
|
+
metadata: { path: [] },
|
|
1422
|
+
}),
|
|
1423
|
+
source_node,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* @param {TransformContext} transform_context
|
|
1429
|
+
* @returns {string}
|
|
1430
|
+
*/
|
|
1431
|
+
function create_local_statement_component_name(transform_context) {
|
|
1432
|
+
transform_context.local_statement_component_index += 1;
|
|
1433
|
+
return `StatementBodyHook${transform_context.local_statement_component_index}`;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Wraps a list of body nodes into a component and returns
|
|
1438
|
+
* statements that return `<ComponentName prop1={prop1} ... />`.
|
|
1439
|
+
* The component is hoisted to module level via helper_state to avoid
|
|
1440
|
+
* recreating the component identity on every render.
|
|
1441
|
+
* Used when a control flow branch contains hook calls that must be moved
|
|
1442
|
+
* into their own component boundary to satisfy the Rules of Hooks.
|
|
1443
|
+
*
|
|
1444
|
+
* @param {any[]} body_nodes
|
|
1445
|
+
* @param {any} key_expression - Optional key expression to add to the component element (for for-of loops)
|
|
1446
|
+
* @param {TransformContext} transform_context
|
|
1447
|
+
* @returns {any[]}
|
|
1448
|
+
*/
|
|
1449
|
+
function hook_safe_render_statements(body_nodes, key_expression, transform_context) {
|
|
1450
|
+
const source_node = get_body_source_node(body_nodes);
|
|
1451
|
+
const helper_id = set_loc(
|
|
1452
|
+
create_generated_identifier(create_local_statement_component_name(transform_context)),
|
|
1453
|
+
source_node,
|
|
1454
|
+
);
|
|
1455
|
+
const helper_bindings = Array.from(transform_context.available_bindings.values());
|
|
1456
|
+
|
|
1457
|
+
// Save and isolate bindings for the helper body
|
|
1458
|
+
const saved_bindings = transform_context.available_bindings;
|
|
1459
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
1460
|
+
|
|
1461
|
+
const helper_fn = set_loc(
|
|
1462
|
+
/** @type {any} */ ({
|
|
1463
|
+
type: 'FunctionDeclaration',
|
|
1464
|
+
id: helper_id,
|
|
1465
|
+
params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
|
|
1466
|
+
body: {
|
|
1467
|
+
type: 'BlockStatement',
|
|
1468
|
+
body: build_render_statements(body_nodes, true, transform_context),
|
|
1469
|
+
metadata: { path: [] },
|
|
1470
|
+
},
|
|
1471
|
+
async: false,
|
|
1472
|
+
generator: false,
|
|
1473
|
+
metadata: {
|
|
1474
|
+
path: [],
|
|
1475
|
+
is_component: true,
|
|
1476
|
+
is_method: false,
|
|
1477
|
+
},
|
|
1478
|
+
}),
|
|
1479
|
+
source_node,
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
// Restore bindings
|
|
1483
|
+
transform_context.available_bindings = saved_bindings;
|
|
1484
|
+
|
|
1485
|
+
// Register helper for hoisting to module level
|
|
1486
|
+
if (transform_context.helper_state) {
|
|
1487
|
+
transform_context.helper_state.helpers.push(helper_fn);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const component_element = create_helper_component_element(
|
|
1491
|
+
helper_id,
|
|
1492
|
+
helper_bindings,
|
|
1493
|
+
source_node,
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
if (key_expression) {
|
|
1497
|
+
component_element.openingElement.attributes.push(
|
|
1498
|
+
/** @type {any} */ ({
|
|
1499
|
+
type: 'JSXAttribute',
|
|
1500
|
+
name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
|
|
1501
|
+
value: to_jsx_expression_container(key_expression, key_expression),
|
|
1502
|
+
metadata: { path: [] },
|
|
1503
|
+
}),
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// When helper_state is null (no enclosing component context), inline the
|
|
1508
|
+
// helper via an IIFE so the function declaration isn't silently dropped.
|
|
1509
|
+
if (!transform_context.helper_state) {
|
|
1510
|
+
return [
|
|
1511
|
+
helper_fn,
|
|
1512
|
+
{
|
|
1513
|
+
type: 'ReturnStatement',
|
|
1514
|
+
argument: component_element,
|
|
1515
|
+
metadata: { path: [] },
|
|
1516
|
+
},
|
|
1517
|
+
];
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return [
|
|
1521
|
+
{
|
|
1522
|
+
type: 'ReturnStatement',
|
|
1523
|
+
argument: component_element,
|
|
1524
|
+
metadata: { path: [] },
|
|
1525
|
+
},
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* @param {any[]} body_nodes
|
|
1531
|
+
* @returns {any}
|
|
1532
|
+
*/
|
|
1533
|
+
function get_body_source_node(body_nodes) {
|
|
1534
|
+
const first = body_nodes[0];
|
|
1535
|
+
const last = body_nodes[body_nodes.length - 1];
|
|
1536
|
+
|
|
1537
|
+
if (first?.loc && last?.loc) {
|
|
1538
|
+
return {
|
|
1539
|
+
start: first.start,
|
|
1540
|
+
end: last.end,
|
|
1541
|
+
loc: {
|
|
1542
|
+
start: first.loc.start,
|
|
1543
|
+
end: last.loc.end,
|
|
1544
|
+
},
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return first;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* @param {any} node
|
|
1553
|
+
* @param {TransformContext} transform_context
|
|
1554
|
+
* @returns {any}
|
|
1555
|
+
*/
|
|
1556
|
+
function to_jsx_child(node, transform_context) {
|
|
1557
|
+
if (!node) return node;
|
|
1558
|
+
switch (node.type) {
|
|
1559
|
+
case 'Tsx':
|
|
1560
|
+
// We're inside a JSX child position by construction, so keep a
|
|
1561
|
+
// JSXExpressionContainer wrapper for bare `{expr}` children.
|
|
1562
|
+
return tsx_node_to_jsx_expression(node, true);
|
|
1563
|
+
case 'TsxCompat':
|
|
1564
|
+
return tsx_compat_node_to_jsx_expression(node, transform_context.platform, true);
|
|
1565
|
+
case 'Element':
|
|
1566
|
+
return to_jsx_element(node, transform_context);
|
|
1567
|
+
case 'Text':
|
|
1568
|
+
return to_jsx_expression_container(to_text_expression(node.expression, node), node);
|
|
1569
|
+
case 'TSRXExpression':
|
|
1570
|
+
return to_jsx_expression_container(node.expression, node);
|
|
1571
|
+
case 'Html':
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
|
|
1574
|
+
);
|
|
1575
|
+
case 'IfStatement':
|
|
1576
|
+
return (
|
|
1577
|
+
transform_context.platform.hooks?.controlFlow?.ifStatement ?? if_statement_to_jsx_child
|
|
1578
|
+
)(node, transform_context);
|
|
1579
|
+
case 'ForOfStatement':
|
|
1580
|
+
return (
|
|
1581
|
+
transform_context.platform.hooks?.controlFlow?.forOf ?? for_of_statement_to_jsx_child
|
|
1582
|
+
)(node, transform_context);
|
|
1583
|
+
case 'SwitchStatement':
|
|
1584
|
+
return (
|
|
1585
|
+
transform_context.platform.hooks?.controlFlow?.switchStatement ??
|
|
1586
|
+
switch_statement_to_jsx_child
|
|
1587
|
+
)(node, transform_context);
|
|
1588
|
+
case 'TryStatement':
|
|
1589
|
+
return (
|
|
1590
|
+
transform_context.platform.hooks?.controlFlow?.tryStatement ?? try_statement_to_jsx_child
|
|
1591
|
+
)(node, transform_context);
|
|
1592
|
+
default:
|
|
1593
|
+
return node;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* @param {any} node
|
|
1599
|
+
* @param {TransformContext} transform_context
|
|
1600
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1601
|
+
*/
|
|
1602
|
+
function if_statement_to_jsx_child(node, transform_context) {
|
|
1603
|
+
return to_jsx_expression_container(
|
|
1604
|
+
/** @type {any} */ ({
|
|
1605
|
+
type: 'CallExpression',
|
|
1606
|
+
callee: {
|
|
1607
|
+
type: 'ArrowFunctionExpression',
|
|
1608
|
+
params: [],
|
|
1609
|
+
body: /** @type {any} */ ({
|
|
1610
|
+
type: 'BlockStatement',
|
|
1611
|
+
body: [
|
|
1612
|
+
create_render_if_statement(node, transform_context),
|
|
1613
|
+
create_null_return_statement(),
|
|
1614
|
+
],
|
|
1615
|
+
metadata: { path: [] },
|
|
1616
|
+
}),
|
|
1617
|
+
async: false,
|
|
1618
|
+
generator: false,
|
|
1619
|
+
expression: false,
|
|
1620
|
+
metadata: { path: [] },
|
|
1621
|
+
},
|
|
1622
|
+
arguments: [],
|
|
1623
|
+
optional: false,
|
|
1624
|
+
metadata: { path: [] },
|
|
1625
|
+
}),
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Find the first `key` attribute expression in the top-level elements of a body.
|
|
1631
|
+
* Used to propagate keys from loop body elements to wrapper components.
|
|
1632
|
+
* Works on both pre-transform (Ripple Element) and post-transform (JSXElement) nodes.
|
|
1633
|
+
*
|
|
1634
|
+
* @param {any[]} body_nodes
|
|
1635
|
+
* @returns {any | undefined}
|
|
1636
|
+
*/
|
|
1637
|
+
function find_key_expression_in_body(body_nodes) {
|
|
1638
|
+
for (const node of body_nodes) {
|
|
1639
|
+
// Pre-transform: Ripple Element node
|
|
1640
|
+
if (node.type === 'Element') {
|
|
1641
|
+
for (const attr of node.attributes || []) {
|
|
1642
|
+
if (attr.type === 'Attribute') {
|
|
1643
|
+
const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
|
|
1644
|
+
if (attr_name === 'key') {
|
|
1645
|
+
return attr.value?.expression ?? attr.value;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
// Post-transform: JSXElement node
|
|
1651
|
+
if (node.type === 'JSXElement') {
|
|
1652
|
+
for (const attr of node.openingElement?.attributes || []) {
|
|
1653
|
+
if (
|
|
1654
|
+
attr.type === 'JSXAttribute' &&
|
|
1655
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
1656
|
+
attr.name.name === 'key'
|
|
1657
|
+
) {
|
|
1658
|
+
// Value is a JSXExpressionContainer
|
|
1659
|
+
if (attr.value?.type === 'JSXExpressionContainer') {
|
|
1660
|
+
return attr.value.expression;
|
|
1661
|
+
}
|
|
1662
|
+
return attr.value;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return undefined;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* @param {any} node
|
|
1672
|
+
* @param {TransformContext} transform_context
|
|
1673
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1674
|
+
*/
|
|
1675
|
+
function for_of_statement_to_jsx_child(node, transform_context) {
|
|
1676
|
+
if (node.await) {
|
|
1677
|
+
throw create_compile_error(
|
|
1678
|
+
node,
|
|
1679
|
+
`${transform_context.platform.name} TSRX does not support \`for await...of\` in component templates.`,
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const loop_params = get_for_of_iteration_params(node.left, node.index);
|
|
1684
|
+
const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
|
|
1685
|
+
const has_hooks = body_contains_top_level_hook_call(loop_body);
|
|
1686
|
+
const body_key_expression = find_key_expression_in_body(loop_body);
|
|
1687
|
+
const explicit_key_expression =
|
|
1688
|
+
body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
|
|
1689
|
+
const key_expression =
|
|
1690
|
+
has_hooks && explicit_key_expression == null && node.index
|
|
1691
|
+
? clone_expression_node(node.index)
|
|
1692
|
+
: explicit_key_expression;
|
|
1693
|
+
const implicit_non_hook_key_expression =
|
|
1694
|
+
!has_hooks && body_key_expression == null
|
|
1695
|
+
? node.key
|
|
1696
|
+
? clone_expression_node(node.key)
|
|
1697
|
+
: node.index
|
|
1698
|
+
? clone_expression_node(node.index)
|
|
1699
|
+
: undefined
|
|
1700
|
+
: undefined;
|
|
1701
|
+
|
|
1702
|
+
// Add loop params to available bindings so hoisted helpers receive them as props
|
|
1703
|
+
const saved_bindings = transform_context.available_bindings;
|
|
1704
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
1705
|
+
for (const param of loop_params) {
|
|
1706
|
+
collect_pattern_bindings(param, transform_context.available_bindings);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const body_statements = has_hooks
|
|
1710
|
+
? hook_safe_render_statements(loop_body, key_expression, transform_context)
|
|
1711
|
+
: build_render_statements(loop_body, true, transform_context);
|
|
1712
|
+
|
|
1713
|
+
if (implicit_non_hook_key_expression) {
|
|
1714
|
+
apply_key_to_render_statements(body_statements, implicit_non_hook_key_expression);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Restore bindings
|
|
1718
|
+
transform_context.available_bindings = saved_bindings;
|
|
1719
|
+
|
|
1720
|
+
return to_jsx_expression_container(
|
|
1721
|
+
/** @type {any} */ ({
|
|
1722
|
+
type: 'CallExpression',
|
|
1723
|
+
callee: {
|
|
1724
|
+
type: 'MemberExpression',
|
|
1725
|
+
object: node.right,
|
|
1726
|
+
property: create_generated_identifier('map'),
|
|
1727
|
+
computed: false,
|
|
1728
|
+
optional: false,
|
|
1729
|
+
metadata: { path: [] },
|
|
1730
|
+
},
|
|
1731
|
+
arguments: [
|
|
1732
|
+
{
|
|
1733
|
+
type: 'ArrowFunctionExpression',
|
|
1734
|
+
params: loop_params,
|
|
1735
|
+
body: /** @type {any} */ ({
|
|
1736
|
+
type: 'BlockStatement',
|
|
1737
|
+
body: body_statements,
|
|
1738
|
+
metadata: { path: [] },
|
|
1739
|
+
}),
|
|
1740
|
+
async: false,
|
|
1741
|
+
generator: false,
|
|
1742
|
+
expression: false,
|
|
1743
|
+
metadata: { path: [] },
|
|
1744
|
+
},
|
|
1745
|
+
],
|
|
1746
|
+
async: false,
|
|
1747
|
+
optional: false,
|
|
1748
|
+
metadata: { path: [] },
|
|
1749
|
+
}),
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* @param {any[]} statements
|
|
1755
|
+
* @param {any} key_expression
|
|
1756
|
+
* @returns {void}
|
|
1757
|
+
*/
|
|
1758
|
+
function apply_key_to_render_statements(statements, key_expression) {
|
|
1759
|
+
for (let i = statements.length - 1; i >= 0; i -= 1) {
|
|
1760
|
+
const statement = statements[i];
|
|
1761
|
+
if (statement?.type !== 'ReturnStatement' || !statement.argument) {
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (statement.argument.type === 'JSXElement') {
|
|
1766
|
+
const attributes = statement.argument.openingElement?.attributes || [];
|
|
1767
|
+
const has_key = attributes.some(
|
|
1768
|
+
(/** @type {any} */ attr) =>
|
|
1769
|
+
attr.type === 'JSXAttribute' &&
|
|
1770
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
1771
|
+
attr.name.name === 'key',
|
|
1772
|
+
);
|
|
1773
|
+
|
|
1774
|
+
if (!has_key) {
|
|
1775
|
+
attributes.push(
|
|
1776
|
+
/** @type {any} */ ({
|
|
1777
|
+
type: 'JSXAttribute',
|
|
1778
|
+
name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
|
|
1779
|
+
value: to_jsx_expression_container(
|
|
1780
|
+
clone_expression_node(key_expression),
|
|
1781
|
+
key_expression,
|
|
1782
|
+
),
|
|
1783
|
+
metadata: { path: [] },
|
|
1784
|
+
}),
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* @param {any} node
|
|
1795
|
+
* @param {TransformContext} transform_context
|
|
1796
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1797
|
+
*/
|
|
1798
|
+
function switch_statement_to_jsx_child(node, transform_context) {
|
|
1799
|
+
return to_jsx_expression_container(
|
|
1800
|
+
/** @type {any} */ ({
|
|
1801
|
+
type: 'CallExpression',
|
|
1802
|
+
callee: {
|
|
1803
|
+
type: 'ArrowFunctionExpression',
|
|
1804
|
+
params: [],
|
|
1805
|
+
body: /** @type {any} */ ({
|
|
1806
|
+
type: 'BlockStatement',
|
|
1807
|
+
body: [
|
|
1808
|
+
create_render_switch_statement(node, transform_context),
|
|
1809
|
+
create_null_return_statement(),
|
|
1810
|
+
],
|
|
1811
|
+
metadata: { path: [] },
|
|
1812
|
+
}),
|
|
1813
|
+
async: false,
|
|
1814
|
+
generator: false,
|
|
1815
|
+
expression: false,
|
|
1816
|
+
metadata: { path: [] },
|
|
1817
|
+
},
|
|
1818
|
+
arguments: [],
|
|
1819
|
+
optional: false,
|
|
1820
|
+
metadata: { path: [] },
|
|
1821
|
+
}),
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* Transform a `try { ... } pending { ... } catch (err, reset) { ... }` block
|
|
1827
|
+
* into React `<TsrxErrorBoundary>` and/or `<Suspense>` JSX elements.
|
|
1828
|
+
*
|
|
1829
|
+
* - `pending` → `<Suspense fallback={...}>`
|
|
1830
|
+
* - `catch` → `<TsrxErrorBoundary fallback={(err, reset) => ...}>`
|
|
1831
|
+
* - both → ErrorBoundary wraps Suspense
|
|
1832
|
+
* - `finally` blocks are not supported in component template context
|
|
1833
|
+
*
|
|
1834
|
+
* @param {any} node
|
|
1835
|
+
* @param {TransformContext} transform_context
|
|
1836
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
1837
|
+
*/
|
|
1838
|
+
function try_statement_to_jsx_child(node, transform_context) {
|
|
1839
|
+
const pending = node.pending;
|
|
1840
|
+
const handler = node.handler;
|
|
1841
|
+
const finalizer = node.finalizer;
|
|
1842
|
+
|
|
1843
|
+
if (finalizer) {
|
|
1844
|
+
throw create_compile_error(
|
|
1845
|
+
finalizer,
|
|
1846
|
+
`${transform_context.platform.name} TSRX does not support \`finally\` blocks in component templates. Move the try statement into a function if you need a finally block.`,
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
if (!pending && !handler) {
|
|
1851
|
+
throw create_compile_error(
|
|
1852
|
+
node,
|
|
1853
|
+
'Component try statements must have a `pending` or `catch` block.',
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Validate that try body contains JSX if pending block is present
|
|
1858
|
+
if (pending) {
|
|
1859
|
+
const try_body = node.block.body || [];
|
|
1860
|
+
if (!try_body.some(is_jsx_child)) {
|
|
1861
|
+
throw create_compile_error(
|
|
1862
|
+
node.block,
|
|
1863
|
+
'Component try statements must contain a template in their main body. Move the try statement into a function if it does not render anything.',
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
const pending_body = pending.body || [];
|
|
1867
|
+
if (!pending_body.some(is_jsx_child)) {
|
|
1868
|
+
throw create_compile_error(
|
|
1869
|
+
pending,
|
|
1870
|
+
'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Build the try body content as JSX children
|
|
1876
|
+
const try_body_nodes = node.block.body || [];
|
|
1877
|
+
const try_content = statement_body_to_jsx_child(try_body_nodes, transform_context);
|
|
1878
|
+
|
|
1879
|
+
/** @type {any} */
|
|
1880
|
+
let result = try_content;
|
|
1881
|
+
|
|
1882
|
+
// Wrap in <Suspense> if pending block exists
|
|
1883
|
+
if (pending) {
|
|
1884
|
+
transform_context.needs_suspense = true;
|
|
1885
|
+
const pending_body_nodes = pending.body || [];
|
|
1886
|
+
const fallback_content = statement_body_to_jsx_child(pending_body_nodes, transform_context);
|
|
1887
|
+
|
|
1888
|
+
result = create_jsx_element(
|
|
1889
|
+
'Suspense',
|
|
1890
|
+
[
|
|
1891
|
+
{
|
|
1892
|
+
type: 'JSXAttribute',
|
|
1893
|
+
name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
|
|
1894
|
+
value: fallback_content,
|
|
1895
|
+
metadata: { path: [] },
|
|
1896
|
+
},
|
|
1897
|
+
],
|
|
1898
|
+
[result],
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Wrap in <TsrxErrorBoundary> if catch block exists
|
|
1903
|
+
if (handler) {
|
|
1904
|
+
transform_context.needs_error_boundary = true;
|
|
1905
|
+
|
|
1906
|
+
const catch_params = [];
|
|
1907
|
+
if (handler.param) {
|
|
1908
|
+
catch_params.push(handler.param);
|
|
1909
|
+
} else {
|
|
1910
|
+
catch_params.push(create_generated_identifier('_error'));
|
|
1911
|
+
}
|
|
1912
|
+
if (handler.resetParam) {
|
|
1913
|
+
catch_params.push(handler.resetParam);
|
|
1914
|
+
} else {
|
|
1915
|
+
catch_params.push(create_generated_identifier('_reset'));
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const catch_body_nodes = handler.body.body || [];
|
|
1919
|
+
|
|
1920
|
+
// Add catch params to available_bindings so static hoisting
|
|
1921
|
+
// correctly identifies references to err/reset as non-static
|
|
1922
|
+
const saved_catch_bindings = transform_context.available_bindings;
|
|
1923
|
+
transform_context.available_bindings = new Map(saved_catch_bindings);
|
|
1924
|
+
for (const param of catch_params) {
|
|
1925
|
+
collect_pattern_bindings(param, transform_context.available_bindings);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const fallback_fn = {
|
|
1929
|
+
type: 'ArrowFunctionExpression',
|
|
1930
|
+
params: catch_params,
|
|
1931
|
+
body: /** @type {any} */ ({
|
|
1932
|
+
type: 'BlockStatement',
|
|
1933
|
+
body: build_render_statements(catch_body_nodes, true, transform_context),
|
|
1934
|
+
metadata: { path: [] },
|
|
1935
|
+
}),
|
|
1936
|
+
async: false,
|
|
1937
|
+
generator: false,
|
|
1938
|
+
expression: false,
|
|
1939
|
+
metadata: { path: [] },
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
transform_context.available_bindings = saved_catch_bindings;
|
|
1943
|
+
|
|
1944
|
+
result = create_jsx_element(
|
|
1945
|
+
'TsrxErrorBoundary',
|
|
1946
|
+
[
|
|
1947
|
+
{
|
|
1948
|
+
type: 'JSXAttribute',
|
|
1949
|
+
name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
|
|
1950
|
+
value: to_jsx_expression_container(/** @type {any} */ (fallback_fn)),
|
|
1951
|
+
metadata: { path: [] },
|
|
1952
|
+
},
|
|
1953
|
+
],
|
|
1954
|
+
[result],
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// result is a JSXElement, but we need to return a JSXExpressionContainer
|
|
1959
|
+
// for embedding in the parent component's render return
|
|
1960
|
+
if (result.type === 'JSXElement') {
|
|
1961
|
+
return to_jsx_expression_container(result);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return result;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Create a simple JSX element AST node.
|
|
1969
|
+
*
|
|
1970
|
+
* @param {string} tag_name
|
|
1971
|
+
* @param {any[]} attributes
|
|
1972
|
+
* @param {any[]} children
|
|
1973
|
+
* @returns {any}
|
|
1974
|
+
*/
|
|
1975
|
+
function create_jsx_element(tag_name, attributes, children) {
|
|
1976
|
+
const name = { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } };
|
|
1977
|
+
return {
|
|
1978
|
+
type: 'JSXElement',
|
|
1979
|
+
openingElement: {
|
|
1980
|
+
type: 'JSXOpeningElement',
|
|
1981
|
+
name,
|
|
1982
|
+
attributes,
|
|
1983
|
+
selfClosing: children.length === 0,
|
|
1984
|
+
metadata: { path: [] },
|
|
1985
|
+
},
|
|
1986
|
+
closingElement:
|
|
1987
|
+
children.length > 0
|
|
1988
|
+
? {
|
|
1989
|
+
type: 'JSXClosingElement',
|
|
1990
|
+
name: { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } },
|
|
1991
|
+
metadata: { path: [] },
|
|
1992
|
+
}
|
|
1993
|
+
: null,
|
|
1994
|
+
children,
|
|
1995
|
+
metadata: { path: [] },
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Inject import declarations for `Suspense` and `TsrxErrorBoundary` if the
|
|
2001
|
+
* transform determined they are needed. The import sources are platform-
|
|
2002
|
+
* specific (e.g. `react` vs `preact/compat`, `@tsrx/react/error-boundary`
|
|
2003
|
+
* vs `@tsrx/preact/error-boundary`).
|
|
2004
|
+
*
|
|
2005
|
+
* @param {AST.Program} program
|
|
2006
|
+
* @param {TransformContext} transform_context
|
|
2007
|
+
* @param {JsxPlatform} platform
|
|
2008
|
+
* @param {string} suspense_source - effective suspense import source after
|
|
2009
|
+
* applying any per-call override from JsxTransformOptions.suspenseSource.
|
|
2010
|
+
*/
|
|
2011
|
+
function inject_try_imports(program, transform_context, platform, suspense_source) {
|
|
2012
|
+
/** @type {any[]} */
|
|
2013
|
+
const imports = [];
|
|
2014
|
+
|
|
2015
|
+
if (transform_context.needs_suspense) {
|
|
2016
|
+
imports.push({
|
|
2017
|
+
type: 'ImportDeclaration',
|
|
2018
|
+
specifiers: [
|
|
2019
|
+
{
|
|
2020
|
+
type: 'ImportSpecifier',
|
|
2021
|
+
imported: { type: 'Identifier', name: 'Suspense', metadata: { path: [] } },
|
|
2022
|
+
local: { type: 'Identifier', name: 'Suspense', metadata: { path: [] } },
|
|
2023
|
+
metadata: { path: [] },
|
|
2024
|
+
},
|
|
2025
|
+
],
|
|
2026
|
+
source: {
|
|
2027
|
+
type: 'Literal',
|
|
2028
|
+
value: suspense_source,
|
|
2029
|
+
raw: `'${suspense_source}'`,
|
|
2030
|
+
},
|
|
2031
|
+
metadata: { path: [] },
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
if (transform_context.needs_error_boundary) {
|
|
2036
|
+
const error_boundary_source = platform.imports.errorBoundary;
|
|
2037
|
+
imports.push({
|
|
2038
|
+
type: 'ImportDeclaration',
|
|
2039
|
+
specifiers: [
|
|
2040
|
+
{
|
|
2041
|
+
type: 'ImportSpecifier',
|
|
2042
|
+
imported: {
|
|
2043
|
+
type: 'Identifier',
|
|
2044
|
+
name: 'TsrxErrorBoundary',
|
|
2045
|
+
metadata: { path: [] },
|
|
2046
|
+
},
|
|
2047
|
+
local: {
|
|
2048
|
+
type: 'Identifier',
|
|
2049
|
+
name: 'TsrxErrorBoundary',
|
|
2050
|
+
metadata: { path: [] },
|
|
2051
|
+
},
|
|
2052
|
+
metadata: { path: [] },
|
|
2053
|
+
},
|
|
2054
|
+
],
|
|
2055
|
+
source: {
|
|
2056
|
+
type: 'Literal',
|
|
2057
|
+
value: error_boundary_source,
|
|
2058
|
+
raw: `'${error_boundary_source}'`,
|
|
2059
|
+
},
|
|
2060
|
+
metadata: { path: [] },
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (imports.length > 0) {
|
|
2065
|
+
program.body.unshift(...imports);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* @param {any} node
|
|
2071
|
+
* @param {TransformContext} transform_context
|
|
2072
|
+
* @returns {any}
|
|
2073
|
+
*/
|
|
2074
|
+
function create_render_if_statement(node, transform_context) {
|
|
2075
|
+
const consequent_body =
|
|
2076
|
+
node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
|
|
2077
|
+
const consequent_has_hooks = body_contains_top_level_hook_call(consequent_body);
|
|
2078
|
+
|
|
2079
|
+
let alternate = null;
|
|
2080
|
+
if (node.alternate) {
|
|
2081
|
+
if (node.alternate.type === 'IfStatement') {
|
|
2082
|
+
alternate = create_render_if_statement(node.alternate, transform_context);
|
|
2083
|
+
} else {
|
|
2084
|
+
const alternate_body = node.alternate.body || [node.alternate];
|
|
2085
|
+
const alternate_has_hooks = body_contains_top_level_hook_call(alternate_body);
|
|
2086
|
+
alternate = set_loc(
|
|
2087
|
+
/** @type {any} */ ({
|
|
2088
|
+
type: 'BlockStatement',
|
|
2089
|
+
body: alternate_has_hooks
|
|
2090
|
+
? hook_safe_render_statements(alternate_body, undefined, transform_context)
|
|
2091
|
+
: build_render_statements(alternate_body, true, transform_context),
|
|
2092
|
+
metadata: { path: [] },
|
|
2093
|
+
}),
|
|
2094
|
+
node.alternate,
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
return set_loc(
|
|
2100
|
+
{
|
|
2101
|
+
type: 'IfStatement',
|
|
2102
|
+
test: node.test,
|
|
2103
|
+
consequent: set_loc(
|
|
2104
|
+
/** @type {any} */ ({
|
|
2105
|
+
type: 'BlockStatement',
|
|
2106
|
+
body: consequent_has_hooks
|
|
2107
|
+
? hook_safe_render_statements(consequent_body, undefined, transform_context)
|
|
2108
|
+
: build_render_statements(consequent_body, true, transform_context),
|
|
2109
|
+
metadata: { path: [] },
|
|
2110
|
+
}),
|
|
2111
|
+
node.consequent,
|
|
2112
|
+
),
|
|
2113
|
+
alternate,
|
|
2114
|
+
},
|
|
2115
|
+
node,
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
/**
|
|
2120
|
+
* @param {any} node
|
|
2121
|
+
* @param {TransformContext} transform_context
|
|
2122
|
+
* @returns {any}
|
|
2123
|
+
*/
|
|
2124
|
+
function create_render_switch_statement(node, transform_context) {
|
|
2125
|
+
return /** @type {any} */ ({
|
|
2126
|
+
type: 'SwitchStatement',
|
|
2127
|
+
discriminant: node.discriminant,
|
|
2128
|
+
cases: node.cases.map((/** @type {any} */ c) =>
|
|
2129
|
+
create_render_switch_case(c, transform_context),
|
|
2130
|
+
),
|
|
2131
|
+
metadata: { path: [] },
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
/**
|
|
2136
|
+
* @param {any} switch_case
|
|
2137
|
+
* @param {TransformContext} transform_context
|
|
2138
|
+
* @returns {any}
|
|
2139
|
+
*/
|
|
2140
|
+
function create_render_switch_case(switch_case, transform_context) {
|
|
2141
|
+
const consequent = flatten_switch_consequent(switch_case.consequent || []);
|
|
2142
|
+
|
|
2143
|
+
// Strip trailing break statements for hook analysis
|
|
2144
|
+
const body_without_break = [];
|
|
2145
|
+
for (const child of consequent) {
|
|
2146
|
+
if (child.type === 'BreakStatement') break;
|
|
2147
|
+
body_without_break.push(child);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
if (body_contains_top_level_hook_call(body_without_break)) {
|
|
2151
|
+
return /** @type {any} */ ({
|
|
2152
|
+
type: 'SwitchCase',
|
|
2153
|
+
test: switch_case.test,
|
|
2154
|
+
consequent: hook_safe_render_statements(body_without_break, undefined, transform_context),
|
|
2155
|
+
metadata: { path: [] },
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
const case_body = [];
|
|
2160
|
+
const render_nodes = [];
|
|
2161
|
+
let has_terminal = false;
|
|
2162
|
+
|
|
2163
|
+
for (const child of consequent) {
|
|
2164
|
+
if (child.type === 'BreakStatement') {
|
|
2165
|
+
if (render_nodes.length > 0 && !has_terminal) {
|
|
2166
|
+
case_body.push(create_component_return_statement(render_nodes, switch_case));
|
|
2167
|
+
} else if (!has_terminal) {
|
|
2168
|
+
case_body.push(child);
|
|
2169
|
+
}
|
|
2170
|
+
has_terminal = true;
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
if (is_bare_return_statement(child)) {
|
|
2175
|
+
case_body.push(create_component_return_statement(render_nodes, child));
|
|
2176
|
+
has_terminal = true;
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
if (is_jsx_child(child)) {
|
|
2181
|
+
render_nodes.push(to_jsx_child(child, transform_context));
|
|
2182
|
+
} else {
|
|
2183
|
+
case_body.push(child);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
if (!has_terminal && render_nodes.length > 0) {
|
|
2188
|
+
case_body.push(create_component_return_statement(render_nodes, switch_case));
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
return /** @type {any} */ ({
|
|
2192
|
+
type: 'SwitchCase',
|
|
2193
|
+
test: switch_case.test,
|
|
2194
|
+
consequent: case_body,
|
|
2195
|
+
metadata: { path: [] },
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* @returns {any}
|
|
2201
|
+
*/
|
|
2202
|
+
function create_null_return_statement() {
|
|
2203
|
+
return {
|
|
2204
|
+
type: 'ReturnStatement',
|
|
2205
|
+
argument: { type: 'Literal', value: null, raw: 'null' },
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* @param {AST.Expression} expression
|
|
2211
|
+
* @param {any} [source_node]
|
|
2212
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
2213
|
+
*/
|
|
2214
|
+
function to_jsx_expression_container(expression, source_node = expression) {
|
|
2215
|
+
// NOTE: JSXExpressionContainer nodes are intentionally created without loc.
|
|
2216
|
+
// They are synthetic wrappers whose source positions do not correspond to
|
|
2217
|
+
// entries in the generated source map, so adding loc causes Volar mapping failures.
|
|
2218
|
+
return /** @type {any} */ ({
|
|
2219
|
+
type: 'JSXExpressionContainer',
|
|
2220
|
+
expression: /** @type {any} */ (expression),
|
|
2221
|
+
metadata: { path: [] },
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Dispatch point for element attribute transformation. Platforms can replace
|
|
2227
|
+
* the default "map over `to_jsx_attribute`" via
|
|
2228
|
+
* `hooks.transformElementAttributes` — Solid uses this to collapse
|
|
2229
|
+
* `<elem>{'text'}</elem>` into a `textContent` attribute and to route
|
|
2230
|
+
* attributes through its composite-element handling.
|
|
2231
|
+
*
|
|
2232
|
+
* @param {any[]} attrs
|
|
2233
|
+
* @param {TransformContext} transform_context
|
|
2234
|
+
* @param {any} element
|
|
2235
|
+
* @returns {any[]}
|
|
2236
|
+
*/
|
|
2237
|
+
function transform_element_attributes_dispatch(attrs, transform_context, element) {
|
|
2238
|
+
const hook = transform_context.platform.hooks?.transformElementAttributes;
|
|
2239
|
+
if (hook) return hook(attrs, transform_context, element);
|
|
2240
|
+
return attrs.map((/** @type {any} */ a) => to_jsx_attribute(a, transform_context));
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
/**
|
|
2244
|
+
* @param {any} attr
|
|
2245
|
+
* @param {TransformContext} transform_context
|
|
2246
|
+
* @returns {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute}
|
|
2247
|
+
*/
|
|
2248
|
+
function to_jsx_attribute(attr, transform_context) {
|
|
2249
|
+
if (!attr) return attr;
|
|
2250
|
+
if (attr.type === 'JSXAttribute' || attr.type === 'JSXSpreadAttribute') {
|
|
2251
|
+
return attr;
|
|
2252
|
+
}
|
|
2253
|
+
if (attr.type === 'SpreadAttribute') {
|
|
2254
|
+
return set_loc(
|
|
2255
|
+
/** @type {any} */ ({
|
|
2256
|
+
type: 'JSXSpreadAttribute',
|
|
2257
|
+
argument: attr.argument,
|
|
2258
|
+
}),
|
|
2259
|
+
attr,
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
if (attr.type === 'RefAttribute') {
|
|
2263
|
+
// RefAttribute uses `{ref expr}` syntax whose source positions don't map to the
|
|
2264
|
+
// generated `ref={expr}` JSX attribute, so we intentionally omit loc.
|
|
2265
|
+
return /** @type {any} */ ({
|
|
2266
|
+
type: 'JSXAttribute',
|
|
2267
|
+
name: { type: 'JSXIdentifier', name: 'ref', metadata: { path: [] } },
|
|
2268
|
+
value: to_jsx_expression_container(attr.argument),
|
|
2269
|
+
shorthand: false,
|
|
2270
|
+
metadata: { path: [] },
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Platforms that expect React-style DOM attrs (React) rewrite `class` to
|
|
2275
|
+
// `className`; Preact and Solid accept `class` natively and keep it.
|
|
2276
|
+
let attr_name = attr.name;
|
|
2277
|
+
if (
|
|
2278
|
+
transform_context.platform.jsx.rewriteClassAttr &&
|
|
2279
|
+
attr_name &&
|
|
2280
|
+
attr_name.type === 'Identifier' &&
|
|
2281
|
+
attr_name.name === 'class'
|
|
2282
|
+
) {
|
|
2283
|
+
attr_name = set_loc(
|
|
2284
|
+
/** @type {any} */ ({ type: 'Identifier', name: 'className', metadata: { path: [] } }),
|
|
2285
|
+
attr.name,
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const name =
|
|
2290
|
+
attr_name && attr_name.type === 'Identifier' ? identifier_to_jsx_name(attr_name) : attr_name;
|
|
2291
|
+
|
|
2292
|
+
let value = attr.value;
|
|
2293
|
+
if (value) {
|
|
2294
|
+
if (value.type === 'Literal' && typeof value.value === 'string') {
|
|
2295
|
+
// Keep string literal as attribute string.
|
|
2296
|
+
} else if (value.type !== 'JSXExpressionContainer') {
|
|
2297
|
+
value = to_jsx_expression_container(value);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
const jsx_attribute = /** @type {any} */ ({
|
|
2302
|
+
type: 'JSXAttribute',
|
|
2303
|
+
name,
|
|
2304
|
+
value: value || null,
|
|
2305
|
+
shorthand: false,
|
|
2306
|
+
metadata: { path: [] },
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
if (value_has_unmappable_jsx_loc(value)) {
|
|
2310
|
+
/** @type {any} */ (jsx_attribute.metadata).has_unmappable_value = true;
|
|
2311
|
+
return jsx_attribute;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
return set_loc(jsx_attribute, attr);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* @param {any} value
|
|
2319
|
+
* @returns {boolean}
|
|
2320
|
+
*/
|
|
2321
|
+
function value_has_unmappable_jsx_loc(value) {
|
|
2322
|
+
return !!(
|
|
2323
|
+
value?.type === 'JSXExpressionContainer' &&
|
|
2324
|
+
(value.expression?.type === 'JSXElement' || value.expression?.type === 'JSXFragment') &&
|
|
2325
|
+
!value.expression.loc
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* @param {any} node
|
|
2331
|
+
* @param {TransformContext} transform_context
|
|
2332
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
2333
|
+
*/
|
|
2334
|
+
function dynamic_element_to_jsx_child(node, transform_context) {
|
|
2335
|
+
const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
|
|
2336
|
+
const alias_declaration = set_loc(
|
|
2337
|
+
/** @type {any} */ ({
|
|
2338
|
+
type: 'VariableDeclaration',
|
|
2339
|
+
kind: 'const',
|
|
2340
|
+
declarations: [
|
|
2341
|
+
{
|
|
2342
|
+
type: 'VariableDeclarator',
|
|
2343
|
+
id: dynamic_id,
|
|
2344
|
+
init: clone_expression_node(node.id),
|
|
2345
|
+
metadata: { path: [] },
|
|
2346
|
+
},
|
|
2347
|
+
],
|
|
2348
|
+
metadata: { path: [] },
|
|
2349
|
+
}),
|
|
2350
|
+
node,
|
|
2351
|
+
);
|
|
2352
|
+
const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
|
|
2353
|
+
|
|
2354
|
+
return to_jsx_expression_container(
|
|
2355
|
+
/** @type {any} */ ({
|
|
2356
|
+
type: 'CallExpression',
|
|
2357
|
+
callee: {
|
|
2358
|
+
type: 'ArrowFunctionExpression',
|
|
2359
|
+
params: [],
|
|
2360
|
+
body: /** @type {any} */ ({
|
|
2361
|
+
type: 'BlockStatement',
|
|
2362
|
+
body: [
|
|
2363
|
+
alias_declaration,
|
|
2364
|
+
{
|
|
2365
|
+
type: 'ReturnStatement',
|
|
2366
|
+
argument: {
|
|
2367
|
+
type: 'ConditionalExpression',
|
|
2368
|
+
test: clone_identifier(dynamic_id),
|
|
2369
|
+
consequent: jsx_element,
|
|
2370
|
+
alternate: create_null_literal(),
|
|
2371
|
+
metadata: { path: [] },
|
|
2372
|
+
},
|
|
2373
|
+
metadata: { path: [] },
|
|
2374
|
+
},
|
|
2375
|
+
],
|
|
2376
|
+
metadata: { path: [] },
|
|
2377
|
+
}),
|
|
2378
|
+
async: false,
|
|
2379
|
+
generator: false,
|
|
2380
|
+
expression: false,
|
|
2381
|
+
metadata: { path: [] },
|
|
2382
|
+
},
|
|
2383
|
+
arguments: [],
|
|
2384
|
+
optional: false,
|
|
2385
|
+
metadata: { path: [] },
|
|
2386
|
+
}),
|
|
2387
|
+
node,
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
/**
|
|
2392
|
+
* @param {AST.Identifier} dynamic_id
|
|
2393
|
+
* @param {any} node
|
|
2394
|
+
* @param {TransformContext} transform_context
|
|
2395
|
+
* @returns {ESTreeJSX.JSXElement}
|
|
2396
|
+
*/
|
|
2397
|
+
function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
|
|
2398
|
+
const attributes = transform_element_attributes_dispatch(
|
|
2399
|
+
node.attributes || [],
|
|
2400
|
+
transform_context,
|
|
2401
|
+
node,
|
|
2402
|
+
);
|
|
2403
|
+
const selfClosing = !!node.selfClosing;
|
|
2404
|
+
const children = create_element_children(node.children || [], transform_context);
|
|
2405
|
+
const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
|
|
2406
|
+
|
|
2407
|
+
return /** @type {any} */ ({
|
|
2408
|
+
type: 'JSXElement',
|
|
2409
|
+
openingElement: {
|
|
2410
|
+
type: 'JSXOpeningElement',
|
|
2411
|
+
name,
|
|
2412
|
+
attributes,
|
|
2413
|
+
selfClosing,
|
|
2414
|
+
metadata: { path: [] },
|
|
2415
|
+
},
|
|
2416
|
+
closingElement: selfClosing
|
|
2417
|
+
? null
|
|
2418
|
+
: {
|
|
2419
|
+
type: 'JSXClosingElement',
|
|
2420
|
+
name: clone_jsx_name(name),
|
|
2421
|
+
metadata: { path: [] },
|
|
2422
|
+
},
|
|
2423
|
+
children,
|
|
2424
|
+
metadata: { path: [] },
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/**
|
|
2429
|
+
* @param {any[]} render_nodes
|
|
2430
|
+
* @returns {any}
|
|
2431
|
+
*/
|
|
2432
|
+
function build_return_expression(render_nodes) {
|
|
2433
|
+
if (render_nodes.length === 0) return null;
|
|
2434
|
+
if (render_nodes.length === 1) {
|
|
2435
|
+
const only = render_nodes[0];
|
|
2436
|
+
if (only.type === 'JSXExpressionContainer') {
|
|
2437
|
+
return only.expression;
|
|
2438
|
+
}
|
|
2439
|
+
return only;
|
|
2440
|
+
}
|
|
2441
|
+
const first = render_nodes[0];
|
|
2442
|
+
const last = render_nodes[render_nodes.length - 1];
|
|
2443
|
+
return set_loc(
|
|
2444
|
+
{
|
|
2445
|
+
type: 'JSXFragment',
|
|
2446
|
+
openingFragment: /** @type {any} */ ({
|
|
2447
|
+
type: 'JSXOpeningFragment',
|
|
2448
|
+
metadata: { path: [] },
|
|
2449
|
+
}),
|
|
2450
|
+
closingFragment: /** @type {any} */ ({
|
|
2451
|
+
type: 'JSXClosingFragment',
|
|
2452
|
+
metadata: { path: [] },
|
|
2453
|
+
}),
|
|
2454
|
+
children: render_nodes,
|
|
2455
|
+
metadata: { path: [] },
|
|
2456
|
+
},
|
|
2457
|
+
first?.loc && last?.loc
|
|
2458
|
+
? {
|
|
2459
|
+
start: first.start,
|
|
2460
|
+
end: last.end,
|
|
2461
|
+
loc: {
|
|
2462
|
+
start: first.loc.start,
|
|
2463
|
+
end: last.loc.end,
|
|
2464
|
+
},
|
|
2465
|
+
}
|
|
2466
|
+
: undefined,
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* @param {any} node
|
|
2472
|
+
* @param {JsxPlatform} platform
|
|
2473
|
+
* @param {boolean} [in_jsx_child]
|
|
2474
|
+
* @returns {any}
|
|
2475
|
+
*/
|
|
2476
|
+
function tsx_compat_node_to_jsx_expression(node, platform, in_jsx_child = false) {
|
|
2477
|
+
if (!platform.jsx.acceptedTsxKinds.includes(node.kind)) {
|
|
2478
|
+
const accepted = platform.jsx.acceptedTsxKinds.map((k) => `<tsx:${k}>`).join(', ');
|
|
2479
|
+
throw create_compile_error(
|
|
2480
|
+
node,
|
|
2481
|
+
`${platform.name} TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or one of: ${accepted}.`,
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
return tsx_node_to_jsx_expression(node, in_jsx_child);
|
|
2486
|
+
}
|