@tsrx/react 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +63 -6
- package/src/transform.js +873 -39
- package/types/index.d.ts +7 -3
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import { CodeMapping, ParseOptions } from '@tsrx/core/types' */
|
|
2
3
|
|
|
3
4
|
import { createVolarMappingsResult, parseModule } from '@tsrx/core';
|
|
4
5
|
import { transform } from './transform.js';
|
|
@@ -7,10 +8,11 @@ import { transform } from './transform.js';
|
|
|
7
8
|
* Parse tsrx-react source code to an ESTree AST.
|
|
8
9
|
* @param {string} source
|
|
9
10
|
* @param {string} [filename]
|
|
11
|
+
* @param {ParseOptions} [options]
|
|
10
12
|
* @returns {AST.Program}
|
|
11
13
|
*/
|
|
12
|
-
export function parse(source, filename) {
|
|
13
|
-
return parseModule(source, filename);
|
|
14
|
+
export function parse(source, filename, options) {
|
|
15
|
+
return parseModule(source, filename, options);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -32,13 +34,13 @@ export function compile(source, filename) {
|
|
|
32
34
|
*
|
|
33
35
|
* @param {string} source
|
|
34
36
|
* @param {string} [filename]
|
|
37
|
+
* @param {ParseOptions} [options]
|
|
35
38
|
* @returns {import('@tsrx/core/types').VolarMappingsResult}
|
|
36
39
|
*/
|
|
37
|
-
export function compile_to_volar_mappings(source, filename) {
|
|
38
|
-
const ast = parseModule(source, filename);
|
|
40
|
+
export function compile_to_volar_mappings(source, filename, options) {
|
|
41
|
+
const ast = parseModule(source, filename, options);
|
|
39
42
|
const transformed = transform(ast, source, filename);
|
|
40
|
-
|
|
41
|
-
return createVolarMappingsResult({
|
|
43
|
+
const result = createVolarMappingsResult({
|
|
42
44
|
ast: transformed.ast,
|
|
43
45
|
ast_from_source: ast,
|
|
44
46
|
source,
|
|
@@ -46,4 +48,59 @@ export function compile_to_volar_mappings(source, filename) {
|
|
|
46
48
|
source_map: transformed.map,
|
|
47
49
|
errors: [],
|
|
48
50
|
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
mappings: dedupe_mappings(result.mappings),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Remove byte-for-byte duplicate mappings. React helper extraction can emit
|
|
60
|
+
* identical mapping entries for the same source and generated span, which
|
|
61
|
+
* causes Volar to merge duplicate hover/navigation results.
|
|
62
|
+
*
|
|
63
|
+
* @param {CodeMapping[]} mappings
|
|
64
|
+
* @returns {CodeMapping[]}
|
|
65
|
+
*/
|
|
66
|
+
function dedupe_mappings(mappings) {
|
|
67
|
+
const deduped = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
|
|
70
|
+
for (const mapping of mappings) {
|
|
71
|
+
const key = JSON.stringify(serialize_mapping_value(mapping));
|
|
72
|
+
|
|
73
|
+
if (seen.has(key)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
seen.add(key);
|
|
78
|
+
deduped.push(mapping);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return deduped;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {unknown} value
|
|
86
|
+
* @returns {unknown}
|
|
87
|
+
*/
|
|
88
|
+
function serialize_mapping_value(value) {
|
|
89
|
+
if (typeof value === 'function') {
|
|
90
|
+
return value.toString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return value.map(serialize_mapping_value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (value && typeof value === 'object') {
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(value)
|
|
100
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
101
|
+
.map(([key, nested_value]) => [key, serialize_mapping_value(nested_value)]),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return value;
|
|
49
106
|
}
|
package/src/transform.js
CHANGED
|
@@ -11,9 +11,16 @@ import { renderStylesheets, setLocation } from '@tsrx/core';
|
|
|
11
11
|
* local_statement_component_index: number,
|
|
12
12
|
* needs_error_boundary: boolean,
|
|
13
13
|
* needs_suspense: boolean,
|
|
14
|
+
* helper_state: { base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] } | null,
|
|
15
|
+
* available_bindings: Map<string, AST.Identifier>,
|
|
16
|
+
* lazy_next_id: number,
|
|
14
17
|
* }} TransformContext
|
|
15
18
|
*/
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{ source_name: string, read: () => any }} LazyBinding
|
|
22
|
+
*/
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Transform a parsed tsrx-react AST into a TSX/JSX module.
|
|
19
26
|
*
|
|
@@ -39,6 +46,9 @@ export function transform(ast, source, filename) {
|
|
|
39
46
|
local_statement_component_index: 0,
|
|
40
47
|
needs_error_boundary: false,
|
|
41
48
|
needs_suspense: false,
|
|
49
|
+
helper_state: null,
|
|
50
|
+
available_bindings: new Map(),
|
|
51
|
+
lazy_next_id: 0,
|
|
42
52
|
};
|
|
43
53
|
|
|
44
54
|
walk(/** @type {any} */ (ast), transform_context, {
|
|
@@ -56,8 +66,39 @@ export function transform(ast, source, filename) {
|
|
|
56
66
|
|
|
57
67
|
const transformed = walk(/** @type {any} */ (ast), transform_context, {
|
|
58
68
|
Component(node, { next, state }) {
|
|
69
|
+
const as_any = /** @type {any} */ (node);
|
|
70
|
+
|
|
71
|
+
// Set up helper_state and bindings BEFORE next() so that nested
|
|
72
|
+
// hook_safe_* calls (inside Element children) can register helpers
|
|
73
|
+
// and access available bindings during the bottom-up walk.
|
|
74
|
+
const helper_state = create_helper_state(as_any.id?.name || 'Component');
|
|
75
|
+
const saved_helper_state = state.helper_state;
|
|
76
|
+
const saved_bindings = state.available_bindings;
|
|
77
|
+
state.helper_state = helper_state;
|
|
78
|
+
|
|
79
|
+
// Pre-collect component body bindings (params + top-level statements)
|
|
80
|
+
// so that Element children processed during the bottom-up walk can see
|
|
81
|
+
// the full scope. Without this, hoisted helpers would miss body-level
|
|
82
|
+
// variables like `const [x] = useState(...)` and produce ReferenceErrors.
|
|
83
|
+
// Only collect up to the split point — bindings declared after a
|
|
84
|
+
// hook-safe split aren't in scope at the return statement and would
|
|
85
|
+
// cause ReferenceErrors if passed as helper props.
|
|
86
|
+
const body_bindings = collect_param_bindings(as_any.params || []);
|
|
87
|
+
const body = as_any.body || [];
|
|
88
|
+
const split_index = find_hook_safe_split_index(body);
|
|
89
|
+
const collect_end = split_index === -1 ? body.length : split_index;
|
|
90
|
+
for (let i = 0; i < collect_end; i += 1) {
|
|
91
|
+
collect_statement_bindings(body[i], body_bindings);
|
|
92
|
+
}
|
|
93
|
+
state.available_bindings = body_bindings;
|
|
94
|
+
|
|
59
95
|
const inner = /** @type {any} */ (next() ?? node);
|
|
60
|
-
|
|
96
|
+
|
|
97
|
+
// Restore context
|
|
98
|
+
state.helper_state = saved_helper_state;
|
|
99
|
+
state.available_bindings = saved_bindings;
|
|
100
|
+
|
|
101
|
+
return /** @type {any} */ (component_to_function_declaration(inner, state, helper_state));
|
|
61
102
|
},
|
|
62
103
|
|
|
63
104
|
Tsx(node, { next }) {
|
|
@@ -77,7 +118,9 @@ export function transform(ast, source, filename) {
|
|
|
77
118
|
|
|
78
119
|
Text(node, { next }) {
|
|
79
120
|
const inner = /** @type {any} */ (next() ?? node);
|
|
80
|
-
return /** @type {any} */ (
|
|
121
|
+
return /** @type {any} */ (
|
|
122
|
+
to_jsx_expression_container(to_text_expression(inner.expression, inner), inner)
|
|
123
|
+
);
|
|
81
124
|
},
|
|
82
125
|
|
|
83
126
|
TSRXExpression(node, { next }) {
|
|
@@ -107,37 +150,579 @@ export function transform(ast, source, filename) {
|
|
|
107
150
|
return { ast: expanded, code: result.code, map: result.map, css };
|
|
108
151
|
}
|
|
109
152
|
|
|
153
|
+
// --- Lazy destructuring support ---
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate a unique lazy identifier name for a lazy destructuring pattern.
|
|
157
|
+
* @param {TransformContext} transform_context
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
function generate_lazy_id(transform_context) {
|
|
161
|
+
return `__lazy${transform_context.lazy_next_id++}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Collect lazy bindings from a destructuring pattern.
|
|
166
|
+
* For `&{name, age}`, maps `name` → `source.name`, `age` → `source.age`.
|
|
167
|
+
* For `&[a, b]`, maps `a` → `source[0]`, `b` → `source[1]`.
|
|
168
|
+
* Handles nested AssignmentPattern (default values) and RestElement.
|
|
169
|
+
*
|
|
170
|
+
* @param {any} pattern - The ObjectPattern or ArrayPattern with lazy: true
|
|
171
|
+
* @param {string} source_name - The generated identifier name for the source
|
|
172
|
+
* @param {Map<string, LazyBinding>} lazy_bindings - Map to populate
|
|
173
|
+
*/
|
|
174
|
+
function collect_lazy_bindings(pattern, source_name, lazy_bindings) {
|
|
175
|
+
if (pattern.type === 'ObjectPattern') {
|
|
176
|
+
for (const prop of pattern.properties || []) {
|
|
177
|
+
if (prop.type === 'RestElement') {
|
|
178
|
+
// Rest element in object pattern — skip for now (complex to transform)
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const value = prop.value;
|
|
182
|
+
const actual = value.type === 'AssignmentPattern' ? value.left : value;
|
|
183
|
+
if (actual.type === 'Identifier') {
|
|
184
|
+
const key = prop.key;
|
|
185
|
+
const computed = prop.computed || key.type !== 'Identifier';
|
|
186
|
+
lazy_bindings.set(actual.name, {
|
|
187
|
+
source_name,
|
|
188
|
+
read: () => ({
|
|
189
|
+
type: 'MemberExpression',
|
|
190
|
+
object: create_generated_identifier(source_name),
|
|
191
|
+
property: computed
|
|
192
|
+
? { ...key }
|
|
193
|
+
: { type: 'Identifier', name: key.name, metadata: { path: [] } },
|
|
194
|
+
computed,
|
|
195
|
+
optional: false,
|
|
196
|
+
metadata: { path: [] },
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else if (pattern.type === 'ArrayPattern') {
|
|
202
|
+
for (let i = 0; i < (pattern.elements || []).length; i++) {
|
|
203
|
+
const element = pattern.elements[i];
|
|
204
|
+
if (!element) continue;
|
|
205
|
+
if (element.type === 'RestElement') {
|
|
206
|
+
// Rest element in array pattern — skip for now
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const actual = element.type === 'AssignmentPattern' ? element.left : element;
|
|
210
|
+
if (actual.type === 'Identifier') {
|
|
211
|
+
const index = i;
|
|
212
|
+
lazy_bindings.set(actual.name, {
|
|
213
|
+
source_name,
|
|
214
|
+
read: () => ({
|
|
215
|
+
type: 'MemberExpression',
|
|
216
|
+
object: create_generated_identifier(source_name),
|
|
217
|
+
property: { type: 'Literal', value: index, raw: String(index), metadata: { path: [] } },
|
|
218
|
+
computed: true,
|
|
219
|
+
optional: false,
|
|
220
|
+
metadata: { path: [] },
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Collect lazy bindings from component params and body variable declarations
|
|
230
|
+
* WITHOUT modifying any AST nodes. Returns a map of binding name → accessor info.
|
|
231
|
+
* Stores the generated identifier name on the pattern's metadata for later replacement.
|
|
232
|
+
*
|
|
233
|
+
* @param {any[]} params - Component params (metadata annotated, not structurally mutated)
|
|
234
|
+
* @param {any[]} body - Component body (metadata annotated, not structurally mutated)
|
|
235
|
+
* @param {TransformContext} transform_context
|
|
236
|
+
* @returns {Map<string, LazyBinding>}
|
|
237
|
+
*/
|
|
238
|
+
function collect_lazy_bindings_from_component(params, body, transform_context) {
|
|
239
|
+
/** @type {Map<string, LazyBinding>} */
|
|
240
|
+
const lazy_bindings = new Map();
|
|
241
|
+
|
|
242
|
+
// Collect from lazy params
|
|
243
|
+
for (const param of params) {
|
|
244
|
+
const pattern = param.type === 'AssignmentPattern' ? param.left : param;
|
|
245
|
+
|
|
246
|
+
if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
|
|
247
|
+
const lazy_name = generate_lazy_id(transform_context);
|
|
248
|
+
collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
|
|
249
|
+
pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Collect from lazy variable declarations in body
|
|
254
|
+
for (const statement of body) {
|
|
255
|
+
if (statement.type !== 'VariableDeclaration') continue;
|
|
256
|
+
|
|
257
|
+
for (const declarator of statement.declarations || []) {
|
|
258
|
+
const pattern = declarator.id;
|
|
259
|
+
if ((pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') && pattern.lazy) {
|
|
260
|
+
const lazy_name = generate_lazy_id(transform_context);
|
|
261
|
+
collect_lazy_bindings(pattern, lazy_name, lazy_bindings);
|
|
262
|
+
pattern.metadata = { ...pattern.metadata, lazy_id: lazy_name };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return lazy_bindings;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Walk an AST node tree and replace identifier references that match lazy bindings
|
|
272
|
+
* with their corresponding member expressions (e.g., `name` → `__lazy0.name`).
|
|
273
|
+
* Also handles AssignmentExpression and UpdateExpression targets.
|
|
274
|
+
*
|
|
275
|
+
* @param {any} node - The AST node to walk
|
|
276
|
+
* @param {Map<string, LazyBinding>} lazy_bindings - Map of lazy binding names
|
|
277
|
+
* @returns {any}
|
|
278
|
+
*/
|
|
279
|
+
function apply_lazy_transforms(node, lazy_bindings) {
|
|
280
|
+
if (!node || typeof node !== 'object') return node;
|
|
281
|
+
if (Array.isArray(node)) return node.map((child) => apply_lazy_transforms(child, lazy_bindings));
|
|
282
|
+
|
|
283
|
+
// Don't recurse into nested function declarations (helper components have their own scope)
|
|
284
|
+
if (
|
|
285
|
+
node.type === 'FunctionDeclaration' ||
|
|
286
|
+
node.type === 'FunctionExpression' ||
|
|
287
|
+
node.type === 'ArrowFunctionExpression'
|
|
288
|
+
) {
|
|
289
|
+
// Transform default parameter values (e.g. (step = count) => ...) with the
|
|
290
|
+
// outer lazy_bindings, since defaults are evaluated in the outer scope.
|
|
291
|
+
let params_changed = false;
|
|
292
|
+
const new_params = (node.params || []).map((/** @type {any} */ param) => {
|
|
293
|
+
const transformed = transform_param_defaults(param, lazy_bindings);
|
|
294
|
+
if (transformed !== param) params_changed = true;
|
|
295
|
+
return transformed;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Check if any params shadow a lazy binding — if so, exclude those names
|
|
299
|
+
/** @type {Set<string>} */
|
|
300
|
+
const shadowed = new Set();
|
|
301
|
+
for (const param of node.params || []) {
|
|
302
|
+
collect_shadowed_names(param, lazy_bindings, shadowed);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const inner_bindings =
|
|
306
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
307
|
+
if (inner_bindings.size === 0 && !params_changed) return node;
|
|
308
|
+
|
|
309
|
+
const new_body =
|
|
310
|
+
inner_bindings.size > 0 ? apply_lazy_transforms(node.body, inner_bindings) : node.body;
|
|
311
|
+
|
|
312
|
+
if (new_body !== node.body || params_changed) {
|
|
313
|
+
return {
|
|
314
|
+
...node,
|
|
315
|
+
params: params_changed ? new_params : node.params,
|
|
316
|
+
body: new_body,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return node;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Handle block-scoped variable shadowing (const/let/var that shadows a lazy name)
|
|
323
|
+
if (node.type === 'BlockStatement' || node.type === 'Program') {
|
|
324
|
+
const block_bindings = collect_block_shadowed_names(node.body, lazy_bindings);
|
|
325
|
+
const effective_bindings =
|
|
326
|
+
block_bindings.size > 0 ? remove_shadowed(lazy_bindings, block_bindings) : lazy_bindings;
|
|
327
|
+
if (effective_bindings.size === 0 && block_bindings.size > 0) return node;
|
|
328
|
+
|
|
329
|
+
let changed = false;
|
|
330
|
+
const new_body = node.body.map((/** @type {any} */ stmt) => {
|
|
331
|
+
const transformed = apply_lazy_transforms(stmt, effective_bindings);
|
|
332
|
+
if (transformed !== stmt) changed = true;
|
|
333
|
+
return transformed;
|
|
334
|
+
});
|
|
335
|
+
return changed ? { ...node, body: new_body } : node;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Handle catch clause parameter shadowing
|
|
339
|
+
if (node.type === 'CatchClause') {
|
|
340
|
+
/** @type {Set<string>} */
|
|
341
|
+
const shadowed = new Set();
|
|
342
|
+
if (node.param) collect_shadowed_names(node.param, lazy_bindings, shadowed);
|
|
343
|
+
const effective_bindings =
|
|
344
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
345
|
+
const new_body = apply_lazy_transforms(node.body, effective_bindings);
|
|
346
|
+
if (new_body !== node.body) return { ...node, body: new_body };
|
|
347
|
+
return node;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Handle for-loop variable shadowing
|
|
351
|
+
if (node.type === 'ForStatement') {
|
|
352
|
+
/** @type {Set<string>} */
|
|
353
|
+
const shadowed = new Set();
|
|
354
|
+
if (node.init?.type === 'VariableDeclaration') {
|
|
355
|
+
for (const decl of node.init.declarations) {
|
|
356
|
+
if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const effective_bindings =
|
|
360
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
361
|
+
let changed = false;
|
|
362
|
+
const new_init = apply_lazy_transforms(node.init, effective_bindings);
|
|
363
|
+
if (new_init !== node.init) changed = true;
|
|
364
|
+
const new_test = apply_lazy_transforms(node.test, effective_bindings);
|
|
365
|
+
if (new_test !== node.test) changed = true;
|
|
366
|
+
const new_update = apply_lazy_transforms(node.update, effective_bindings);
|
|
367
|
+
if (new_update !== node.update) changed = true;
|
|
368
|
+
const new_body = apply_lazy_transforms(node.body, effective_bindings);
|
|
369
|
+
if (new_body !== node.body) changed = true;
|
|
370
|
+
return changed
|
|
371
|
+
? { ...node, init: new_init, test: new_test, update: new_update, body: new_body }
|
|
372
|
+
: node;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (node.type === 'ForOfStatement' || node.type === 'ForInStatement') {
|
|
376
|
+
/** @type {Set<string>} */
|
|
377
|
+
const shadowed = new Set();
|
|
378
|
+
if (node.left?.type === 'VariableDeclaration') {
|
|
379
|
+
for (const decl of node.left.declarations) {
|
|
380
|
+
if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const effective_bindings =
|
|
384
|
+
shadowed.size > 0 ? remove_shadowed(lazy_bindings, shadowed) : lazy_bindings;
|
|
385
|
+
let changed = false;
|
|
386
|
+
const new_right = apply_lazy_transforms(node.right, lazy_bindings);
|
|
387
|
+
if (new_right !== node.right) changed = true;
|
|
388
|
+
const new_body = apply_lazy_transforms(node.body, effective_bindings);
|
|
389
|
+
if (new_body !== node.body) changed = true;
|
|
390
|
+
return changed ? { ...node, right: new_right, body: new_body } : node;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Handle switch-case variable shadowing (const/let inside case consequent arrays)
|
|
394
|
+
if (node.type === 'SwitchStatement') {
|
|
395
|
+
let changed = false;
|
|
396
|
+
const new_discriminant = apply_lazy_transforms(node.discriminant, lazy_bindings);
|
|
397
|
+
if (new_discriminant !== node.discriminant) changed = true;
|
|
398
|
+
const new_cases = node.cases.map((/** @type {any} */ switch_case) => {
|
|
399
|
+
const case_bindings = collect_block_shadowed_names(switch_case.consequent, lazy_bindings);
|
|
400
|
+
const effective_bindings =
|
|
401
|
+
case_bindings.size > 0 ? remove_shadowed(lazy_bindings, case_bindings) : lazy_bindings;
|
|
402
|
+
let case_changed = false;
|
|
403
|
+
const new_test = switch_case.test
|
|
404
|
+
? apply_lazy_transforms(switch_case.test, lazy_bindings)
|
|
405
|
+
: null;
|
|
406
|
+
if (new_test !== switch_case.test) case_changed = true;
|
|
407
|
+
const new_consequent = switch_case.consequent.map((/** @type {any} */ stmt) => {
|
|
408
|
+
const transformed = apply_lazy_transforms(stmt, effective_bindings);
|
|
409
|
+
if (transformed !== stmt) case_changed = true;
|
|
410
|
+
return transformed;
|
|
411
|
+
});
|
|
412
|
+
if (case_changed) {
|
|
413
|
+
changed = true;
|
|
414
|
+
return { ...switch_case, test: new_test, consequent: new_consequent };
|
|
415
|
+
}
|
|
416
|
+
return switch_case;
|
|
417
|
+
});
|
|
418
|
+
return changed ? { ...node, discriminant: new_discriminant, cases: new_cases } : node;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Handle assignment: `name = value` → `__lazy0.name = value`
|
|
422
|
+
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
|
|
423
|
+
const binding = lazy_bindings.get(node.left.name);
|
|
424
|
+
if (binding) {
|
|
425
|
+
return {
|
|
426
|
+
...node,
|
|
427
|
+
left: binding.read(),
|
|
428
|
+
right: apply_lazy_transforms(node.right, lazy_bindings),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Handle update: `count++` → `__lazy0[0]++`
|
|
434
|
+
if (node.type === 'UpdateExpression' && node.argument.type === 'Identifier') {
|
|
435
|
+
const binding = lazy_bindings.get(node.argument.name);
|
|
436
|
+
if (binding) {
|
|
437
|
+
return { ...node, argument: binding.read() };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Replace lazy variable declaration patterns with generated identifiers
|
|
442
|
+
if (node.type === 'VariableDeclarator' && node.id?.metadata?.lazy_id) {
|
|
443
|
+
const lazy_id = create_generated_identifier(node.id.metadata.lazy_id);
|
|
444
|
+
if (node.id.typeAnnotation) {
|
|
445
|
+
lazy_id.typeAnnotation = node.id.typeAnnotation;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
...node,
|
|
449
|
+
id: lazy_id,
|
|
450
|
+
init: apply_lazy_transforms(node.init, lazy_bindings),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Handle identifier references in expression position
|
|
455
|
+
if (node.type === 'Identifier') {
|
|
456
|
+
const binding = lazy_bindings.get(node.name);
|
|
457
|
+
if (binding) {
|
|
458
|
+
return binding.read();
|
|
459
|
+
}
|
|
460
|
+
return node;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Skip JSXIdentifier (component/element names)
|
|
464
|
+
if (node.type === 'JSXIdentifier') return node;
|
|
465
|
+
|
|
466
|
+
// Handle shorthand properties: `{ name }` → `{ name: __lazy0.name }`
|
|
467
|
+
if (node.type === 'Property' && node.shorthand && node.value?.type === 'Identifier') {
|
|
468
|
+
const binding = lazy_bindings.get(node.value.name);
|
|
469
|
+
if (binding) {
|
|
470
|
+
return {
|
|
471
|
+
...node,
|
|
472
|
+
shorthand: false,
|
|
473
|
+
value: binding.read(),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Recurse into child nodes
|
|
479
|
+
let changed = false;
|
|
480
|
+
const result = /** @type {any} */ ({});
|
|
481
|
+
|
|
482
|
+
for (const key of Object.keys(node)) {
|
|
483
|
+
if (key === 'loc' || key === 'start' || key === 'end') {
|
|
484
|
+
result[key] = node[key];
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Skip non-computed property keys (they're labels, not references)
|
|
489
|
+
if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) {
|
|
490
|
+
result[key] = node[key];
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Skip non-computed member expression property names
|
|
495
|
+
if (key === 'property' && node.type === 'MemberExpression' && !node.computed) {
|
|
496
|
+
result[key] = node[key];
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Skip JSXAttribute name
|
|
501
|
+
if (key === 'name' && node.type === 'JSXAttribute') {
|
|
502
|
+
result[key] = node[key];
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Skip variable declaration id (the lazy declaration itself was already replaced)
|
|
507
|
+
if (key === 'id' && node.type === 'VariableDeclarator') {
|
|
508
|
+
result[key] = node[key];
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const child = node[key];
|
|
513
|
+
const transformed = apply_lazy_transforms(child, lazy_bindings);
|
|
514
|
+
result[key] = transformed;
|
|
515
|
+
if (transformed !== child) changed = true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return changed ? result : node;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Transform default values in function parameters without touching param names.
|
|
523
|
+
* E.g. `(step = count)` where `count` is a lazy binding → `(step = __lazy0[0])`.
|
|
524
|
+
*
|
|
525
|
+
* @param {any} param
|
|
526
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
527
|
+
* @returns {any}
|
|
528
|
+
*/
|
|
529
|
+
function transform_param_defaults(param, lazy_bindings) {
|
|
530
|
+
if (param?.type === 'AssignmentPattern') {
|
|
531
|
+
const new_right = apply_lazy_transforms(param.right, lazy_bindings);
|
|
532
|
+
if (new_right !== param.right) {
|
|
533
|
+
return { ...param, right: new_right };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return param;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Collect names from a pattern that shadow lazy bindings.
|
|
541
|
+
* @param {any} pattern
|
|
542
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
543
|
+
* @param {Set<string>} shadowed
|
|
544
|
+
*/
|
|
545
|
+
function collect_shadowed_names(pattern, lazy_bindings, shadowed) {
|
|
546
|
+
if (!pattern || typeof pattern !== 'object') return;
|
|
547
|
+
|
|
548
|
+
if (pattern.type === 'Identifier' && lazy_bindings.has(pattern.name)) {
|
|
549
|
+
shadowed.add(pattern.name);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (pattern.type === 'AssignmentPattern') {
|
|
554
|
+
collect_shadowed_names(pattern.left, lazy_bindings, shadowed);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (pattern.type === 'RestElement') {
|
|
559
|
+
collect_shadowed_names(pattern.argument, lazy_bindings, shadowed);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (pattern.type === 'ObjectPattern') {
|
|
564
|
+
for (const prop of pattern.properties || []) {
|
|
565
|
+
if (prop.type === 'RestElement') {
|
|
566
|
+
collect_shadowed_names(prop.argument, lazy_bindings, shadowed);
|
|
567
|
+
} else {
|
|
568
|
+
collect_shadowed_names(prop.value, lazy_bindings, shadowed);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (pattern.type === 'ArrayPattern') {
|
|
575
|
+
for (const element of pattern.elements || []) {
|
|
576
|
+
if (element) collect_shadowed_names(element, lazy_bindings, shadowed);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Collect variable names declared in block-level statements that shadow lazy bindings.
|
|
583
|
+
* Scans VariableDeclarations (const/let/var) and FunctionDeclarations at the top level of a block.
|
|
584
|
+
*
|
|
585
|
+
* @param {any[]} statements
|
|
586
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
587
|
+
* @returns {Set<string>}
|
|
588
|
+
*/
|
|
589
|
+
function collect_block_shadowed_names(statements, lazy_bindings) {
|
|
590
|
+
/** @type {Set<string>} */
|
|
591
|
+
const shadowed = new Set();
|
|
592
|
+
for (const stmt of statements) {
|
|
593
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
594
|
+
for (const decl of stmt.declarations) {
|
|
595
|
+
// Skip lazy destructuring patterns — they ARE the lazy bindings,
|
|
596
|
+
// not local declarations that shadow them.
|
|
597
|
+
if (decl.id?.metadata?.lazy_id) continue;
|
|
598
|
+
if (decl.id) collect_shadowed_names(decl.id, lazy_bindings, shadowed);
|
|
599
|
+
}
|
|
600
|
+
} else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
|
|
601
|
+
if (lazy_bindings.has(stmt.id.name)) {
|
|
602
|
+
shadowed.add(stmt.id.name);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return shadowed;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Create a new lazy_bindings map with the shadowed names removed.
|
|
611
|
+
*
|
|
612
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
613
|
+
* @param {Set<string>} shadowed
|
|
614
|
+
* @returns {Map<string, LazyBinding>}
|
|
615
|
+
*/
|
|
616
|
+
function remove_shadowed(lazy_bindings, shadowed) {
|
|
617
|
+
const result = new Map(lazy_bindings);
|
|
618
|
+
for (const name of shadowed) {
|
|
619
|
+
result.delete(name);
|
|
620
|
+
}
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Replace lazy parameter patterns with their generated identifiers.
|
|
626
|
+
* A param `&{name, age}: Props` becomes `__lazy0: Props`.
|
|
627
|
+
*
|
|
628
|
+
* @param {any[]} params
|
|
629
|
+
* @returns {any[]}
|
|
630
|
+
*/
|
|
631
|
+
function replace_lazy_params(params) {
|
|
632
|
+
return params.map((param) => {
|
|
633
|
+
const pattern = param.type === 'AssignmentPattern' ? param.left : param;
|
|
634
|
+
|
|
635
|
+
if (
|
|
636
|
+
(pattern.type === 'ObjectPattern' || pattern.type === 'ArrayPattern') &&
|
|
637
|
+
pattern.lazy &&
|
|
638
|
+
pattern.metadata?.lazy_id
|
|
639
|
+
) {
|
|
640
|
+
const lazy_id = create_generated_identifier(pattern.metadata.lazy_id);
|
|
641
|
+
if (pattern.typeAnnotation) {
|
|
642
|
+
lazy_id.typeAnnotation = pattern.typeAnnotation;
|
|
643
|
+
}
|
|
644
|
+
if (param.type === 'AssignmentPattern') {
|
|
645
|
+
return { ...param, left: lazy_id };
|
|
646
|
+
}
|
|
647
|
+
return lazy_id;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return param;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
110
654
|
/**
|
|
111
655
|
* @param {any} component
|
|
112
656
|
* @param {TransformContext} transform_context
|
|
657
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
|
|
113
658
|
* @returns {AST.FunctionDeclaration}
|
|
114
659
|
*/
|
|
115
|
-
function component_to_function_declaration(component, transform_context) {
|
|
116
|
-
const helper_state = create_helper_state(component.id?.name || 'Component');
|
|
660
|
+
function component_to_function_declaration(component, transform_context, walk_helper_state) {
|
|
661
|
+
const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
|
|
662
|
+
const params = component.params || [];
|
|
663
|
+
const body = /** @type {any[]} */ (component.body || []);
|
|
664
|
+
|
|
665
|
+
// Collect param bindings from original patterns (lazy patterns still intact).
|
|
666
|
+
const param_bindings = collect_param_bindings(params);
|
|
667
|
+
|
|
668
|
+
// Collect lazy binding info WITHOUT mutating patterns. Stores lazy_id on metadata
|
|
669
|
+
// for later replacement. Body bindings (count, setCount, etc.) are still in the
|
|
670
|
+
// original patterns, so collect_statement_bindings during build will find them.
|
|
671
|
+
const lazy_bindings = collect_lazy_bindings_from_component(params, body, transform_context);
|
|
672
|
+
|
|
673
|
+
// Save and set context for this component scope
|
|
674
|
+
const saved_helper_state = transform_context.helper_state;
|
|
675
|
+
const saved_bindings = transform_context.available_bindings;
|
|
676
|
+
transform_context.helper_state = helper_state;
|
|
677
|
+
transform_context.available_bindings = new Map(param_bindings);
|
|
678
|
+
|
|
679
|
+
const body_statements = build_component_statements(
|
|
680
|
+
body,
|
|
681
|
+
helper_state,
|
|
682
|
+
param_bindings,
|
|
683
|
+
transform_context,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
// Replace lazy param patterns with generated identifiers
|
|
687
|
+
const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
|
|
688
|
+
|
|
689
|
+
// Wrap body_statements in a BlockStatement so that apply_lazy_transforms
|
|
690
|
+
// runs collect_block_shadowed_names and detects body-level declarations
|
|
691
|
+
// (e.g. `const name = ...`) that shadow lazy binding names.
|
|
692
|
+
const body_block = /** @type {any} */ ({
|
|
693
|
+
type: 'BlockStatement',
|
|
694
|
+
body: body_statements,
|
|
695
|
+
metadata: { path: [] },
|
|
696
|
+
});
|
|
697
|
+
const final_body =
|
|
698
|
+
lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
|
|
699
|
+
|
|
117
700
|
const fn = /** @type {any} */ ({
|
|
118
701
|
type: 'FunctionDeclaration',
|
|
119
702
|
id: component.id,
|
|
120
|
-
params:
|
|
121
|
-
body:
|
|
122
|
-
type: 'BlockStatement',
|
|
123
|
-
body: build_component_statements(
|
|
124
|
-
/** @type {any[]} */ (component.body),
|
|
125
|
-
helper_state,
|
|
126
|
-
collect_param_bindings(component.params || []),
|
|
127
|
-
transform_context,
|
|
128
|
-
),
|
|
129
|
-
metadata: { path: [] },
|
|
130
|
-
},
|
|
703
|
+
params: final_params,
|
|
704
|
+
body: final_body,
|
|
131
705
|
async: false,
|
|
132
706
|
generator: false,
|
|
133
707
|
metadata: {
|
|
134
708
|
path: [],
|
|
135
709
|
is_component: true,
|
|
136
|
-
is_method: true,
|
|
137
710
|
},
|
|
138
711
|
});
|
|
139
712
|
|
|
713
|
+
// Restore context
|
|
714
|
+
transform_context.helper_state = saved_helper_state;
|
|
715
|
+
transform_context.available_bindings = saved_bindings;
|
|
716
|
+
|
|
140
717
|
fn.metadata.generated_helpers = helper_state.helpers;
|
|
718
|
+
fn.metadata.generated_statics = helper_state.statics;
|
|
719
|
+
|
|
720
|
+
if (fn.id) {
|
|
721
|
+
fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
|
|
722
|
+
...fn.id.metadata,
|
|
723
|
+
is_component: true,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
141
726
|
|
|
142
727
|
setLocation(fn, /** @type {any} */ (component), true);
|
|
143
728
|
return fn;
|
|
@@ -145,7 +730,7 @@ function component_to_function_declaration(component, transform_context) {
|
|
|
145
730
|
|
|
146
731
|
/**
|
|
147
732
|
* @param {any[]} body_nodes
|
|
148
|
-
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
|
|
733
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
149
734
|
* @param {Map<string, AST.Identifier>} available_bindings
|
|
150
735
|
* @param {TransformContext} transform_context
|
|
151
736
|
* @returns {any[]}
|
|
@@ -183,9 +768,12 @@ function build_component_statements(
|
|
|
183
768
|
} else {
|
|
184
769
|
statements.push(child);
|
|
185
770
|
collect_statement_bindings(child, bindings);
|
|
771
|
+
transform_context.available_bindings = bindings;
|
|
186
772
|
}
|
|
187
773
|
}
|
|
188
774
|
|
|
775
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
776
|
+
|
|
189
777
|
const split_node = body_nodes[split_index];
|
|
190
778
|
const consequent_body =
|
|
191
779
|
split_node.consequent.type === 'BlockStatement'
|
|
@@ -242,9 +830,15 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
242
830
|
const statements = [];
|
|
243
831
|
const render_nodes = [];
|
|
244
832
|
|
|
833
|
+
// Create a new bindings map so inner-scope bindings from
|
|
834
|
+
// collect_statement_bindings don't leak to the caller's scope.
|
|
835
|
+
const saved_bindings = transform_context.available_bindings;
|
|
836
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
837
|
+
|
|
245
838
|
for (const child of body_nodes) {
|
|
246
839
|
if (is_bare_return_statement(child)) {
|
|
247
840
|
statements.push(create_component_return_statement(render_nodes, child));
|
|
841
|
+
transform_context.available_bindings = saved_bindings;
|
|
248
842
|
return statements;
|
|
249
843
|
}
|
|
250
844
|
|
|
@@ -257,9 +851,12 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
257
851
|
render_nodes.push(to_jsx_child(child, transform_context));
|
|
258
852
|
} else {
|
|
259
853
|
statements.push(child);
|
|
854
|
+
collect_statement_bindings(child, transform_context.available_bindings);
|
|
260
855
|
}
|
|
261
856
|
}
|
|
262
857
|
|
|
858
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
859
|
+
|
|
263
860
|
const return_arg = build_return_expression(render_nodes);
|
|
264
861
|
if (return_arg || return_null_when_empty) {
|
|
265
862
|
statements.push({
|
|
@@ -268,6 +865,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
268
865
|
});
|
|
269
866
|
}
|
|
270
867
|
|
|
868
|
+
transform_context.available_bindings = saved_bindings;
|
|
271
869
|
return statements;
|
|
272
870
|
}
|
|
273
871
|
|
|
@@ -385,7 +983,7 @@ function is_hook_callee(callee) {
|
|
|
385
983
|
|
|
386
984
|
/**
|
|
387
985
|
* @param {any[]} body_nodes
|
|
388
|
-
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
|
|
986
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
389
987
|
* @param {Map<string, AST.Identifier>} available_bindings
|
|
390
988
|
* @param {any} source_node
|
|
391
989
|
* @param {string} suffix
|
|
@@ -425,7 +1023,7 @@ function create_helper_component_expression(
|
|
|
425
1023
|
/**
|
|
426
1024
|
* @param {AST.Identifier} helper_id
|
|
427
1025
|
* @param {any[]} body_nodes
|
|
428
|
-
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
|
|
1026
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
429
1027
|
* @param {Map<string, AST.Identifier>} available_bindings
|
|
430
1028
|
* @param {AST.Identifier[]} helper_bindings
|
|
431
1029
|
* @param {any} source_node
|
|
@@ -460,10 +1058,16 @@ function create_helper_function_declaration(
|
|
|
460
1058
|
metadata: {
|
|
461
1059
|
path: [],
|
|
462
1060
|
is_component: true,
|
|
463
|
-
is_method: true,
|
|
464
1061
|
},
|
|
465
1062
|
});
|
|
466
1063
|
|
|
1064
|
+
if (fn.id) {
|
|
1065
|
+
fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
|
|
1066
|
+
...fn.id.metadata,
|
|
1067
|
+
is_component: true,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
467
1071
|
return set_loc(fn, source_node);
|
|
468
1072
|
}
|
|
469
1073
|
|
|
@@ -538,7 +1142,7 @@ function create_helper_component_element(helper_id, bindings, source_node) {
|
|
|
538
1142
|
}
|
|
539
1143
|
|
|
540
1144
|
/**
|
|
541
|
-
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }} helper_state
|
|
1145
|
+
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
542
1146
|
* @param {string} suffix
|
|
543
1147
|
* @returns {string}
|
|
544
1148
|
*/
|
|
@@ -549,13 +1153,14 @@ function create_helper_name(helper_state, suffix) {
|
|
|
549
1153
|
|
|
550
1154
|
/**
|
|
551
1155
|
* @param {string} base_name
|
|
552
|
-
* @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[] }}
|
|
1156
|
+
* @returns {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }}
|
|
553
1157
|
*/
|
|
554
1158
|
function create_helper_state(base_name) {
|
|
555
1159
|
return {
|
|
556
1160
|
base_name,
|
|
557
1161
|
next_id: 0,
|
|
558
1162
|
helpers: [],
|
|
1163
|
+
statics: [],
|
|
559
1164
|
};
|
|
560
1165
|
}
|
|
561
1166
|
|
|
@@ -635,6 +1240,93 @@ function collect_pattern_bindings(pattern, bindings) {
|
|
|
635
1240
|
}
|
|
636
1241
|
}
|
|
637
1242
|
|
|
1243
|
+
/**
|
|
1244
|
+
* Check if a node references any of the given scope bindings.
|
|
1245
|
+
* Used to determine if a JSX element is static and can be hoisted to module level.
|
|
1246
|
+
*
|
|
1247
|
+
* @param {any} node
|
|
1248
|
+
* @param {Map<string, AST.Identifier>} scope_bindings
|
|
1249
|
+
* @returns {boolean}
|
|
1250
|
+
*/
|
|
1251
|
+
function references_scope_bindings(node, scope_bindings) {
|
|
1252
|
+
if (!node || typeof node !== 'object') return false;
|
|
1253
|
+
if (scope_bindings.size === 0) return false;
|
|
1254
|
+
|
|
1255
|
+
if (node.type === 'Identifier') {
|
|
1256
|
+
return scope_bindings.has(node.name);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// JSXIdentifier is a variable reference when capitalized (tag name like <MyComponent />)
|
|
1260
|
+
// or when it's the object of a JSXMemberExpression (e.g. ui in <ui.Button />)
|
|
1261
|
+
if (node.type === 'JSXIdentifier') {
|
|
1262
|
+
return scope_bindings.has(node.name);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (Array.isArray(node)) {
|
|
1266
|
+
return node.some((child) => references_scope_bindings(child, scope_bindings));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
for (const key of Object.keys(node)) {
|
|
1270
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
1271
|
+
|
|
1272
|
+
// Skip non-computed, non-shorthand property keys (they are labels, not references)
|
|
1273
|
+
if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) continue;
|
|
1274
|
+
|
|
1275
|
+
// Skip non-computed member expression property access
|
|
1276
|
+
if (key === 'property' && node.type === 'MemberExpression' && !node.computed) continue;
|
|
1277
|
+
|
|
1278
|
+
// Skip JSXMemberExpression property (e.g. Button in <Icons.Button /> is a label, not a reference)
|
|
1279
|
+
if (key === 'property' && node.type === 'JSXMemberExpression') continue;
|
|
1280
|
+
|
|
1281
|
+
// Skip JSXAttribute names — they are attribute labels, not variable references
|
|
1282
|
+
if (key === 'name' && node.type === 'JSXAttribute') continue;
|
|
1283
|
+
|
|
1284
|
+
if (references_scope_bindings(node[key], scope_bindings)) return true;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Hoist static JSX elements from render_nodes to module level.
|
|
1292
|
+
* A JSX element is static if it doesn't reference any component-scope bindings.
|
|
1293
|
+
* Hoisting prevents React from recreating the element on every render, allowing
|
|
1294
|
+
* the reconciler to skip diffing when it sees the same element identity.
|
|
1295
|
+
*
|
|
1296
|
+
* @param {any[]} render_nodes
|
|
1297
|
+
* @param {TransformContext} transform_context
|
|
1298
|
+
*/
|
|
1299
|
+
function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
1300
|
+
if (!transform_context.helper_state) return;
|
|
1301
|
+
|
|
1302
|
+
for (let i = 0; i < render_nodes.length; i++) {
|
|
1303
|
+
const node = render_nodes[i];
|
|
1304
|
+
if (node.type !== 'JSXElement') continue;
|
|
1305
|
+
if (references_scope_bindings(node, transform_context.available_bindings)) continue;
|
|
1306
|
+
|
|
1307
|
+
const name = create_helper_name(transform_context.helper_state, 'static');
|
|
1308
|
+
const id = create_generated_identifier(name);
|
|
1309
|
+
|
|
1310
|
+
transform_context.helper_state.statics.push(
|
|
1311
|
+
/** @type {any} */ ({
|
|
1312
|
+
type: 'VariableDeclaration',
|
|
1313
|
+
kind: 'const',
|
|
1314
|
+
declarations: [
|
|
1315
|
+
{
|
|
1316
|
+
type: 'VariableDeclarator',
|
|
1317
|
+
id,
|
|
1318
|
+
init: node,
|
|
1319
|
+
metadata: { path: [] },
|
|
1320
|
+
},
|
|
1321
|
+
],
|
|
1322
|
+
metadata: { path: [] },
|
|
1323
|
+
}),
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
638
1330
|
/**
|
|
639
1331
|
* @param {AST.Identifier} identifier
|
|
640
1332
|
* @returns {AST.Identifier}
|
|
@@ -669,9 +1361,11 @@ function create_null_literal() {
|
|
|
669
1361
|
function expand_component_helpers(program) {
|
|
670
1362
|
program.body = program.body.flatMap((statement) => {
|
|
671
1363
|
if (statement.type === 'FunctionDeclaration') {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
1364
|
+
const meta = /** @type {any} */ (statement.metadata);
|
|
1365
|
+
const statics = meta?.generated_statics || [];
|
|
1366
|
+
const helpers = meta?.generated_helpers || [];
|
|
1367
|
+
if (statics.length || helpers.length) {
|
|
1368
|
+
return [...statics, ...helpers, statement];
|
|
675
1369
|
}
|
|
676
1370
|
}
|
|
677
1371
|
|
|
@@ -680,9 +1374,11 @@ function expand_component_helpers(program) {
|
|
|
680
1374
|
statement.type === 'ExportDefaultDeclaration') &&
|
|
681
1375
|
statement.declaration?.type === 'FunctionDeclaration'
|
|
682
1376
|
) {
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
1377
|
+
const meta = /** @type {any} */ (statement.declaration.metadata);
|
|
1378
|
+
const statics = meta?.generated_statics || [];
|
|
1379
|
+
const helpers = meta?.generated_helpers || [];
|
|
1380
|
+
if (statics.length || helpers.length) {
|
|
1381
|
+
return [...statics, ...helpers, statement];
|
|
686
1382
|
}
|
|
687
1383
|
}
|
|
688
1384
|
|
|
@@ -799,6 +1495,22 @@ function is_style_element(node) {
|
|
|
799
1495
|
);
|
|
800
1496
|
}
|
|
801
1497
|
|
|
1498
|
+
/**
|
|
1499
|
+
* @param {any} node
|
|
1500
|
+
* @returns {boolean}
|
|
1501
|
+
*/
|
|
1502
|
+
function is_composite_element(node) {
|
|
1503
|
+
if (!node || node.type !== 'Element' || !node.id) {
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (node.id.type === 'Identifier') {
|
|
1508
|
+
return /^[A-Z]/.test(node.id.name);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return node.id.type === 'MemberExpression';
|
|
1512
|
+
}
|
|
1513
|
+
|
|
802
1514
|
/**
|
|
803
1515
|
* Recursively walk Element nodes within a component body and add the hash
|
|
804
1516
|
* class name so scope-qualified selectors (e.g. `.foo.hash`) match.
|
|
@@ -819,7 +1531,7 @@ function annotate_with_hash(node, hash) {
|
|
|
819
1531
|
}
|
|
820
1532
|
|
|
821
1533
|
if (node.type === 'Element') {
|
|
822
|
-
if (!is_style_element(node)) {
|
|
1534
|
+
if (!is_style_element(node) && !is_composite_element(node)) {
|
|
823
1535
|
add_hash_class(node, hash);
|
|
824
1536
|
}
|
|
825
1537
|
if (Array.isArray(node.children)) {
|
|
@@ -1114,11 +1826,17 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
|
|
|
1114
1826
|
create_generated_identifier(create_local_statement_component_name(transform_context)),
|
|
1115
1827
|
source_node,
|
|
1116
1828
|
);
|
|
1829
|
+
const helper_bindings = Array.from(transform_context.available_bindings.values());
|
|
1830
|
+
|
|
1831
|
+
// Save and isolate bindings for the helper body
|
|
1832
|
+
const saved_bindings = transform_context.available_bindings;
|
|
1833
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
1834
|
+
|
|
1117
1835
|
const helper_fn = set_loc(
|
|
1118
1836
|
/** @type {any} */ ({
|
|
1119
1837
|
type: 'FunctionDeclaration',
|
|
1120
1838
|
id: helper_id,
|
|
1121
|
-
params: [],
|
|
1839
|
+
params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
|
|
1122
1840
|
body: {
|
|
1123
1841
|
type: 'BlockStatement',
|
|
1124
1842
|
body: build_render_statements(body_nodes, true, transform_context),
|
|
@@ -1135,6 +1853,19 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
|
|
|
1135
1853
|
source_node,
|
|
1136
1854
|
);
|
|
1137
1855
|
|
|
1856
|
+
// Restore bindings
|
|
1857
|
+
transform_context.available_bindings = saved_bindings;
|
|
1858
|
+
|
|
1859
|
+
// Register helper for hoisting to module level
|
|
1860
|
+
if (transform_context.helper_state) {
|
|
1861
|
+
transform_context.helper_state.helpers.push(helper_fn);
|
|
1862
|
+
|
|
1863
|
+
return to_jsx_expression_container(
|
|
1864
|
+
/** @type {any} */ (create_helper_component_element(helper_id, helper_bindings, source_node)),
|
|
1865
|
+
source_node,
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1138
1869
|
return to_jsx_expression_container(
|
|
1139
1870
|
/** @type {any} */ ({
|
|
1140
1871
|
type: 'CallExpression',
|
|
@@ -1147,7 +1878,7 @@ function hook_safe_statement_body_to_jsx_child(body_nodes, transform_context) {
|
|
|
1147
1878
|
helper_fn,
|
|
1148
1879
|
{
|
|
1149
1880
|
type: 'ReturnStatement',
|
|
1150
|
-
argument: create_helper_component_element(helper_id,
|
|
1881
|
+
argument: create_helper_component_element(helper_id, helper_bindings, source_node),
|
|
1151
1882
|
metadata: { path: [] },
|
|
1152
1883
|
},
|
|
1153
1884
|
],
|
|
@@ -1176,8 +1907,10 @@ function create_local_statement_component_name(transform_context) {
|
|
|
1176
1907
|
}
|
|
1177
1908
|
|
|
1178
1909
|
/**
|
|
1179
|
-
* Wraps a list of body nodes into a
|
|
1180
|
-
* statements that
|
|
1910
|
+
* Wraps a list of body nodes into a component and returns
|
|
1911
|
+
* statements that return `<ComponentName prop1={prop1} ... />`.
|
|
1912
|
+
* The component is hoisted to module level via helper_state to avoid
|
|
1913
|
+
* recreating the component identity on every render.
|
|
1181
1914
|
* Used when a control flow branch contains hook calls that must be moved
|
|
1182
1915
|
* into their own component boundary to satisfy the Rules of Hooks.
|
|
1183
1916
|
*
|
|
@@ -1192,12 +1925,17 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
|
|
|
1192
1925
|
create_generated_identifier(create_local_statement_component_name(transform_context)),
|
|
1193
1926
|
source_node,
|
|
1194
1927
|
);
|
|
1928
|
+
const helper_bindings = Array.from(transform_context.available_bindings.values());
|
|
1929
|
+
|
|
1930
|
+
// Save and isolate bindings for the helper body
|
|
1931
|
+
const saved_bindings = transform_context.available_bindings;
|
|
1932
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
1195
1933
|
|
|
1196
1934
|
const helper_fn = set_loc(
|
|
1197
1935
|
/** @type {any} */ ({
|
|
1198
1936
|
type: 'FunctionDeclaration',
|
|
1199
1937
|
id: helper_id,
|
|
1200
|
-
params: [],
|
|
1938
|
+
params: helper_bindings.length > 0 ? [create_helper_props_pattern(helper_bindings)] : [],
|
|
1201
1939
|
body: {
|
|
1202
1940
|
type: 'BlockStatement',
|
|
1203
1941
|
body: build_render_statements(body_nodes, true, transform_context),
|
|
@@ -1214,7 +1952,19 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
|
|
|
1214
1952
|
source_node,
|
|
1215
1953
|
);
|
|
1216
1954
|
|
|
1217
|
-
|
|
1955
|
+
// Restore bindings
|
|
1956
|
+
transform_context.available_bindings = saved_bindings;
|
|
1957
|
+
|
|
1958
|
+
// Register helper for hoisting to module level
|
|
1959
|
+
if (transform_context.helper_state) {
|
|
1960
|
+
transform_context.helper_state.helpers.push(helper_fn);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
const component_element = create_helper_component_element(
|
|
1964
|
+
helper_id,
|
|
1965
|
+
helper_bindings,
|
|
1966
|
+
source_node,
|
|
1967
|
+
);
|
|
1218
1968
|
|
|
1219
1969
|
if (key_expression) {
|
|
1220
1970
|
component_element.openingElement.attributes.push(
|
|
@@ -1227,8 +1977,20 @@ function hook_safe_render_statements(body_nodes, key_expression, transform_conte
|
|
|
1227
1977
|
);
|
|
1228
1978
|
}
|
|
1229
1979
|
|
|
1980
|
+
// When helper_state is null (no enclosing component context), inline the
|
|
1981
|
+
// helper via an IIFE so the function declaration isn't silently dropped.
|
|
1982
|
+
if (!transform_context.helper_state) {
|
|
1983
|
+
return [
|
|
1984
|
+
helper_fn,
|
|
1985
|
+
{
|
|
1986
|
+
type: 'ReturnStatement',
|
|
1987
|
+
argument: component_element,
|
|
1988
|
+
metadata: { path: [] },
|
|
1989
|
+
},
|
|
1990
|
+
];
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1230
1993
|
return [
|
|
1231
|
-
helper_fn,
|
|
1232
1994
|
{
|
|
1233
1995
|
type: 'ReturnStatement',
|
|
1234
1996
|
argument: component_element,
|
|
@@ -1274,6 +2036,7 @@ function to_jsx_child(node, transform_context) {
|
|
|
1274
2036
|
case 'Element':
|
|
1275
2037
|
return to_jsx_element(node, transform_context);
|
|
1276
2038
|
case 'Text':
|
|
2039
|
+
return to_jsx_expression_container(to_text_expression(node.expression, node), node);
|
|
1277
2040
|
case 'TSRXExpression':
|
|
1278
2041
|
return to_jsx_expression_container(node.expression, node);
|
|
1279
2042
|
case 'IfStatement':
|
|
@@ -1380,6 +2143,20 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
1380
2143
|
const has_hooks = body_contains_top_level_hook_call(loop_body);
|
|
1381
2144
|
const key_expression = has_hooks ? find_key_expression_in_body(loop_body) : undefined;
|
|
1382
2145
|
|
|
2146
|
+
// Add loop params to available bindings so hoisted helpers receive them as props
|
|
2147
|
+
const saved_bindings = transform_context.available_bindings;
|
|
2148
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
2149
|
+
for (const param of loop_params) {
|
|
2150
|
+
collect_pattern_bindings(param, transform_context.available_bindings);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const body_statements = has_hooks
|
|
2154
|
+
? hook_safe_render_statements(loop_body, key_expression, transform_context)
|
|
2155
|
+
: build_render_statements(loop_body, true, transform_context);
|
|
2156
|
+
|
|
2157
|
+
// Restore bindings
|
|
2158
|
+
transform_context.available_bindings = saved_bindings;
|
|
2159
|
+
|
|
1383
2160
|
return to_jsx_expression_container(
|
|
1384
2161
|
/** @type {any} */ ({
|
|
1385
2162
|
type: 'CallExpression',
|
|
@@ -1397,9 +2174,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
1397
2174
|
params: loop_params,
|
|
1398
2175
|
body: /** @type {any} */ ({
|
|
1399
2176
|
type: 'BlockStatement',
|
|
1400
|
-
body:
|
|
1401
|
-
? hook_safe_render_statements(loop_body, key_expression, transform_context)
|
|
1402
|
-
: build_render_statements(loop_body, true, transform_context),
|
|
2177
|
+
body: body_statements,
|
|
1403
2178
|
metadata: { path: [] },
|
|
1404
2179
|
}),
|
|
1405
2180
|
async: false,
|
|
@@ -1541,6 +2316,15 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
1541
2316
|
}
|
|
1542
2317
|
|
|
1543
2318
|
const catch_body_nodes = handler.body.body || [];
|
|
2319
|
+
|
|
2320
|
+
// Add catch params to available_bindings so static hoisting
|
|
2321
|
+
// correctly identifies references to err/reset as non-static
|
|
2322
|
+
const saved_catch_bindings = transform_context.available_bindings;
|
|
2323
|
+
transform_context.available_bindings = new Map(saved_catch_bindings);
|
|
2324
|
+
for (const param of catch_params) {
|
|
2325
|
+
collect_pattern_bindings(param, transform_context.available_bindings);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
1544
2328
|
const fallback_fn = {
|
|
1545
2329
|
type: 'ArrowFunctionExpression',
|
|
1546
2330
|
params: catch_params,
|
|
@@ -1555,6 +2339,8 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
1555
2339
|
metadata: { path: [] },
|
|
1556
2340
|
};
|
|
1557
2341
|
|
|
2342
|
+
transform_context.available_bindings = saved_catch_bindings;
|
|
2343
|
+
|
|
1558
2344
|
result = create_jsx_element(
|
|
1559
2345
|
'TsrxErrorBoundary',
|
|
1560
2346
|
[
|
|
@@ -1826,6 +2612,54 @@ function to_jsx_expression_container(expression, source_node = expression) {
|
|
|
1826
2612
|
});
|
|
1827
2613
|
}
|
|
1828
2614
|
|
|
2615
|
+
/**
|
|
2616
|
+
* Ripple's `{text expr}` always renders text, even for booleans and objects.
|
|
2617
|
+
* React's normal `{expr}` child semantics would drop booleans and render
|
|
2618
|
+
* elements as elements, so we coerce to a text value explicitly.
|
|
2619
|
+
* @param {AST.Expression} expression
|
|
2620
|
+
* @param {any} [source_node]
|
|
2621
|
+
* @returns {AST.Expression}
|
|
2622
|
+
*/
|
|
2623
|
+
function to_text_expression(expression, source_node = expression) {
|
|
2624
|
+
return set_loc(
|
|
2625
|
+
/** @type {AST.Expression} */ ({
|
|
2626
|
+
type: 'ConditionalExpression',
|
|
2627
|
+
test: {
|
|
2628
|
+
type: 'BinaryExpression',
|
|
2629
|
+
operator: '==',
|
|
2630
|
+
left: clone_expression_node(expression),
|
|
2631
|
+
right: {
|
|
2632
|
+
type: 'Literal',
|
|
2633
|
+
value: null,
|
|
2634
|
+
raw: 'null',
|
|
2635
|
+
metadata: { path: [] },
|
|
2636
|
+
},
|
|
2637
|
+
metadata: { path: [] },
|
|
2638
|
+
},
|
|
2639
|
+
consequent: {
|
|
2640
|
+
type: 'Literal',
|
|
2641
|
+
value: '',
|
|
2642
|
+
raw: "''",
|
|
2643
|
+
metadata: { path: [] },
|
|
2644
|
+
},
|
|
2645
|
+
alternate: {
|
|
2646
|
+
type: 'BinaryExpression',
|
|
2647
|
+
operator: '+',
|
|
2648
|
+
left: clone_expression_node(expression),
|
|
2649
|
+
right: {
|
|
2650
|
+
type: 'Literal',
|
|
2651
|
+
value: '',
|
|
2652
|
+
raw: "''",
|
|
2653
|
+
metadata: { path: [] },
|
|
2654
|
+
},
|
|
2655
|
+
metadata: { path: [] },
|
|
2656
|
+
},
|
|
2657
|
+
metadata: { path: [] },
|
|
2658
|
+
}),
|
|
2659
|
+
source_node,
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
1829
2663
|
/**
|
|
1830
2664
|
* @param {any} attr
|
|
1831
2665
|
* @returns {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute}
|
package/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Program } from 'estree';
|
|
2
|
-
import type { VolarMappingsResult } from '@tsrx/core/types';
|
|
2
|
+
import type { ParseOptions, VolarMappingsResult } from '@tsrx/core/types';
|
|
3
3
|
|
|
4
|
-
export function parse(source: string, filename?: string): Program;
|
|
4
|
+
export function parse(source: string, filename?: string, options?: ParseOptions): Program;
|
|
5
5
|
|
|
6
6
|
export function compile(
|
|
7
7
|
source: string,
|
|
@@ -12,4 +12,8 @@ export function compile(
|
|
|
12
12
|
css: { code: string; hash: string } | null;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export function compile_to_volar_mappings(
|
|
15
|
+
export function compile_to_volar_mappings(
|
|
16
|
+
source: string,
|
|
17
|
+
filename?: string,
|
|
18
|
+
options?: ParseOptions,
|
|
19
|
+
): VolarMappingsResult;
|