@tsrx/preact 0.0.2 → 0.0.4

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