ripple 0.3.13 → 0.3.15

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 (70) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +5 -30
  3. package/src/runtime/array.js +38 -38
  4. package/src/runtime/create-subscriber.js +2 -2
  5. package/src/runtime/internal/client/bindings.js +4 -6
  6. package/src/runtime/internal/client/events.js +8 -3
  7. package/src/runtime/internal/client/hmr.js +5 -17
  8. package/src/runtime/internal/client/runtime.js +1 -0
  9. package/src/runtime/internal/server/blocks.js +7 -9
  10. package/src/runtime/internal/server/index.js +14 -22
  11. package/src/runtime/media-query.js +34 -33
  12. package/src/runtime/object.js +7 -10
  13. package/src/runtime/proxy.js +2 -3
  14. package/src/runtime/reactive-value.js +23 -21
  15. package/src/utils/ast.js +1 -1
  16. package/src/utils/attributes.js +43 -0
  17. package/src/utils/builders.js +2 -2
  18. package/tests/client/basic/basic.components.test.rsrx +103 -1
  19. package/tests/client/basic/basic.errors.test.rsrx +1 -1
  20. package/tests/client/basic/basic.styling.test.rsrx +1 -1
  21. package/tests/client/compiler/compiler.assignments.test.rsrx +1 -1
  22. package/tests/client/compiler/compiler.attributes.test.rsrx +1 -1
  23. package/tests/client/compiler/compiler.basic.test.rsrx +51 -14
  24. package/tests/client/compiler/compiler.tracked-access.test.rsrx +1 -1
  25. package/tests/client/compiler/compiler.try-in-function.test.rsrx +1 -1
  26. package/tests/client/compiler/compiler.typescript.test.rsrx +1 -1
  27. package/tests/client/css/global-additional-cases.test.rsrx +1 -1
  28. package/tests/client/css/global-advanced-selectors.test.rsrx +1 -1
  29. package/tests/client/css/global-at-rules.test.rsrx +1 -1
  30. package/tests/client/css/global-basic.test.rsrx +1 -1
  31. package/tests/client/css/global-classes-ids.test.rsrx +1 -1
  32. package/tests/client/css/global-combinators.test.rsrx +1 -1
  33. package/tests/client/css/global-complex-nesting.test.rsrx +1 -1
  34. package/tests/client/css/global-edge-cases.test.rsrx +1 -1
  35. package/tests/client/css/global-keyframes.test.rsrx +1 -1
  36. package/tests/client/css/global-nested.test.rsrx +1 -1
  37. package/tests/client/css/global-pseudo.test.rsrx +1 -1
  38. package/tests/client/css/global-scoping.test.rsrx +1 -1
  39. package/tests/client/css/style-identifier.test.rsrx +1 -1
  40. package/tests/client/return.test.rsrx +1 -1
  41. package/tests/hydration/build-components.js +1 -1
  42. package/tests/server/basic.components.test.rsrx +114 -0
  43. package/tests/server/compiler.test.rsrx +38 -1
  44. package/tests/server/style-identifier.test.rsrx +1 -1
  45. package/tests/setup-server.js +1 -1
  46. package/tests/utils/compiler-compat-config.test.js +1 -1
  47. package/types/index.d.ts +1 -1
  48. package/src/compiler/comment-utils.js +0 -91
  49. package/src/compiler/errors.js +0 -77
  50. package/src/compiler/identifier-utils.js +0 -80
  51. package/src/compiler/index.d.ts +0 -127
  52. package/src/compiler/index.js +0 -89
  53. package/src/compiler/phases/1-parse/index.js +0 -3007
  54. package/src/compiler/phases/1-parse/style.js +0 -704
  55. package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
  56. package/src/compiler/phases/2-analyze/index.js +0 -2208
  57. package/src/compiler/phases/2-analyze/prune.js +0 -1131
  58. package/src/compiler/phases/2-analyze/validation.js +0 -168
  59. package/src/compiler/phases/3-transform/client/index.js +0 -5264
  60. package/src/compiler/phases/3-transform/segments.js +0 -2125
  61. package/src/compiler/phases/3-transform/server/index.js +0 -1749
  62. package/src/compiler/phases/3-transform/stylesheet.js +0 -545
  63. package/src/compiler/scope.js +0 -476
  64. package/src/compiler/source-map-utils.js +0 -358
  65. package/src/compiler/types/acorn.d.ts +0 -11
  66. package/src/compiler/types/estree-jsx.d.ts +0 -11
  67. package/src/compiler/types/estree.d.ts +0 -11
  68. package/src/compiler/types/index.d.ts +0 -1411
  69. package/src/compiler/types/parse.d.ts +0 -1723
  70. package/src/compiler/utils.js +0 -1258
@@ -1,1258 +0,0 @@
1
- /**
2
- @import * as AST from 'estree';
3
- @import { CommonContext, NameSpace, ScopeInterface, Binding } from '#compiler';
4
- */
5
-
6
- import { build_assignment_value, extract_paths } from '../utils/ast.js';
7
- import * as b from '../utils/builders.js';
8
- import { is_capture_event, is_non_delegated, normalize_event_name } from '../utils/events.js';
9
-
10
- export { hash } from '../utils/hashing.js';
11
-
12
- const VOID_ELEMENT_NAMES = [
13
- 'area',
14
- 'base',
15
- 'br',
16
- 'col',
17
- 'command',
18
- 'embed',
19
- 'hr',
20
- 'img',
21
- 'input',
22
- 'keygen',
23
- 'link',
24
- 'meta',
25
- 'param',
26
- 'source',
27
- 'track',
28
- 'wbr',
29
- ];
30
-
31
- /**
32
- * Returns `true` if `name` is of a void element
33
- * @param {string} name
34
- */
35
- /**
36
- * Returns true if name is a void element
37
- * @param {string} name
38
- * @returns {boolean}
39
- */
40
- export function is_void_element(name) {
41
- return VOID_ELEMENT_NAMES.includes(name) || name.toLowerCase() === '!doctype';
42
- }
43
-
44
- const RESERVED_WORDS = [
45
- 'arguments',
46
- 'await',
47
- 'break',
48
- 'case',
49
- 'catch',
50
- 'class',
51
- 'const',
52
- 'continue',
53
- 'debugger',
54
- 'default',
55
- 'delete',
56
- 'do',
57
- 'else',
58
- 'enum',
59
- 'eval',
60
- 'export',
61
- 'extends',
62
- 'false',
63
- 'finally',
64
- 'for',
65
- 'function',
66
- 'if',
67
- 'implements',
68
- 'import',
69
- 'in',
70
- 'instanceof',
71
- 'interface',
72
- 'let',
73
- 'new',
74
- 'null',
75
- 'package',
76
- 'private',
77
- 'protected',
78
- 'public',
79
- 'return',
80
- 'static',
81
- 'super',
82
- 'switch',
83
- 'this',
84
- 'throw',
85
- 'true',
86
- 'try',
87
- 'typeof',
88
- 'var',
89
- 'void',
90
- 'while',
91
- 'with',
92
- 'yield',
93
- ];
94
-
95
- /**
96
- * Returns true if word is a reserved JS keyword
97
- * @param {string} word
98
- * @returns {boolean}
99
- */
100
- export function is_reserved(word) {
101
- return RESERVED_WORDS.includes(word);
102
- }
103
-
104
- /**
105
- * Attributes that are boolean, i.e. they are present or not present.
106
- */
107
- const DOM_BOOLEAN_ATTRIBUTES = [
108
- 'allowfullscreen',
109
- 'async',
110
- 'autofocus',
111
- 'autoplay',
112
- 'checked',
113
- 'controls',
114
- 'default',
115
- 'disabled',
116
- 'formnovalidate',
117
- 'hidden',
118
- 'indeterminate',
119
- 'inert',
120
- 'ismap',
121
- 'loop',
122
- 'multiple',
123
- 'muted',
124
- 'nomodule',
125
- 'novalidate',
126
- 'open',
127
- 'playsinline',
128
- 'readonly',
129
- 'required',
130
- 'reversed',
131
- 'seamless',
132
- 'selected',
133
- 'webkitdirectory',
134
- 'defer',
135
- 'disablepictureinpicture',
136
- 'disableremoteplayback',
137
- ];
138
-
139
- /**
140
- * Returns true if name is a boolean DOM attribute
141
- * @param {string} name
142
- * @returns {boolean}
143
- */
144
- export function is_boolean_attribute(name) {
145
- return DOM_BOOLEAN_ATTRIBUTES.includes(name);
146
- }
147
-
148
- const DOM_PROPERTIES = [
149
- ...DOM_BOOLEAN_ATTRIBUTES,
150
- 'formNoValidate',
151
- 'isMap',
152
- 'noModule',
153
- 'playsInline',
154
- 'readOnly',
155
- 'value',
156
- 'volume',
157
- 'defaultValue',
158
- 'defaultChecked',
159
- 'srcObject',
160
- 'noValidate',
161
- 'allowFullscreen',
162
- 'disablePictureInPicture',
163
- 'disableRemotePlayback',
164
- ];
165
-
166
- // Omits track, trackSplit and trackAsync are they're handled separately
167
- /** @type {Record<string, {name: string, requiresBlock?: boolean}>} */
168
- const RIPPLE_IMPORT_CALL_NAME = {
169
- RippleArray: { name: 'ripple_array', requiresBlock: true },
170
- RippleObject: { name: 'ripple_object', requiresBlock: true },
171
- RippleURL: { name: 'ripple_url', requiresBlock: true },
172
- RippleURLSearchParams: { name: 'ripple_url_search_params', requiresBlock: true },
173
- RippleDate: { name: 'ripple_date', requiresBlock: true },
174
- RippleMap: { name: 'ripple_map', requiresBlock: true },
175
- RippleSet: { name: 'ripple_set', requiresBlock: true },
176
- MediaQuery: { name: 'media_query', requiresBlock: true },
177
- Context: { name: 'context' },
178
- effect: { name: 'effect' },
179
- untrack: { name: 'untrack' },
180
- trackPending: { name: 'is_tracked_pending' },
181
- peek: { name: 'peek_tracked' },
182
- };
183
-
184
- /**
185
- * Returns true if name is a DOM property
186
- * @param {string} name
187
- * @returns {boolean}
188
- */
189
- export function is_dom_property(name) {
190
- return DOM_PROPERTIES.includes(name);
191
- }
192
-
193
- /**
194
- * Determines if an event handler can be delegated
195
- * @param {string} event_name
196
- * @param {AST.Node} handler
197
- * @param {CommonContext} context
198
- * @returns {boolean}
199
- */
200
- export function is_delegated_event(event_name, handler, context) {
201
- // Handle delegated event handlers. Bail out if not a delegated event.
202
- if (
203
- !handler ||
204
- is_capture_event(event_name) ||
205
- is_non_delegated(normalize_event_name(event_name)) ||
206
- (handler.type !== 'FunctionExpression' &&
207
- handler.type !== 'ArrowFunctionExpression' &&
208
- !is_declared_function_within_component(/** @type {AST.Identifier}*/ (handler), context))
209
- ) {
210
- return false;
211
- }
212
- return true;
213
- }
214
-
215
- /**
216
- * Returns true if context is inside a Component node
217
- * @param {CommonContext} context
218
- * @param {boolean} [includes_functions=false]
219
- * @returns {AST.Component | undefined}
220
- */
221
- export function is_inside_component(context, includes_functions = false) {
222
- for (let i = context.path.length - 1; i >= 0; i -= 1) {
223
- const context_node = context.path[i];
224
- const type = context_node.type;
225
-
226
- if (
227
- !includes_functions &&
228
- (type === 'FunctionExpression' ||
229
- type === 'ArrowFunctionExpression' ||
230
- type === 'FunctionDeclaration')
231
- ) {
232
- return;
233
- }
234
- if (context_node.type === 'Component') {
235
- return context_node;
236
- }
237
- }
238
- return;
239
- }
240
-
241
- /**
242
- * Returns true if context is inside a component-level function
243
- * @param {CommonContext} context
244
- * @returns {boolean}
245
- */
246
- export function is_component_level_function(context) {
247
- for (let i = context.path.length - 1; i >= 0; i -= 1) {
248
- const context_node = context.path[i];
249
- const type = context_node.type;
250
-
251
- if (
252
- type === 'BlockStatement' &&
253
- context_node.body.find((n) => /** @type {AST.Node} */ (n).type === 'Component')
254
- ) {
255
- return true;
256
- }
257
-
258
- if (
259
- type === 'FunctionExpression' ||
260
- type === 'ArrowFunctionExpression' ||
261
- type === 'FunctionDeclaration'
262
- ) {
263
- return false;
264
- }
265
- }
266
- return true;
267
- }
268
-
269
- /**
270
- * Returns the matched Ripple tracking call name
271
- * @param {AST.Expression | AST.Super} callee
272
- * @param {CommonContext} context
273
- * @returns {'track' | 'trackAsync' | null}
274
- */
275
- export function is_ripple_track_call(callee, context) {
276
- // Super expressions cannot be Ripple track calls
277
- if (callee.type === 'Super') return null;
278
-
279
- if (callee.type === 'Identifier' && (callee.name === 'track' || callee.name === 'trackAsync')) {
280
- return is_ripple_import(callee, context) ? callee.name : null;
281
- }
282
-
283
- if (
284
- callee.type === 'MemberExpression' &&
285
- callee.object.type === 'Identifier' &&
286
- callee.property.type === 'Identifier' &&
287
- (callee.property.name === 'track' || callee.property.name === 'trackAsync') &&
288
- !callee.computed &&
289
- is_ripple_import(callee, context)
290
- ) {
291
- return callee.property.name;
292
- }
293
-
294
- return null;
295
- }
296
-
297
- /**
298
- * Returns true if context is inside a call expression
299
- * @param {CommonContext} context
300
- * @returns {boolean}
301
- */
302
- export function is_inside_call_expression(context) {
303
- for (let i = context.path.length - 1; i >= 0; i -= 1) {
304
- const context_node = context.path[i];
305
- const type = context_node.type;
306
-
307
- if (
308
- type === 'FunctionExpression' ||
309
- type === 'ArrowFunctionExpression' ||
310
- type === 'FunctionDeclaration'
311
- ) {
312
- return false;
313
- }
314
- if (type === 'CallExpression') {
315
- const callee = context_node.callee;
316
- if (is_ripple_track_call(callee, context)) {
317
- return false;
318
- }
319
- return true;
320
- }
321
- }
322
- return false;
323
- }
324
-
325
- /**
326
- * Returns true if node is a static value (Literal, ArrayExpression, etc)
327
- * @param {AST.Node} node
328
- * @returns {boolean}
329
- */
330
- export function is_value_static(node) {
331
- if (node.type === 'Literal') {
332
- return true;
333
- }
334
- if (node.type === 'ArrayExpression') {
335
- return true;
336
- }
337
- if (node.type === 'NewExpression') {
338
- if (node.callee.type === 'Identifier' && node.callee.name === 'Array') {
339
- return true;
340
- }
341
- return false;
342
- }
343
-
344
- return false;
345
- }
346
-
347
- /**
348
- * Returns true if callee is a Ripple import
349
- * @param {AST.Expression} callee
350
- * @param {CommonContext} context
351
- * @returns {boolean}
352
- */
353
- export function is_ripple_import(callee, context) {
354
- if (callee.type === 'Identifier') {
355
- const binding = context.state.scope.get(callee.name);
356
-
357
- return (
358
- binding?.declaration_kind === 'import' &&
359
- binding.initial !== null &&
360
- binding.initial.type === 'ImportDeclaration' &&
361
- binding.initial.source.type === 'Literal' &&
362
- binding.initial.source.value === 'ripple'
363
- );
364
- } else if (
365
- callee.type === 'MemberExpression' &&
366
- callee.object.type === 'Identifier' &&
367
- !callee.computed
368
- ) {
369
- const binding = context.state.scope.get(callee.object.name);
370
-
371
- return (
372
- binding?.declaration_kind === 'import' &&
373
- binding.initial !== null &&
374
- binding.initial.type === 'ImportDeclaration' &&
375
- binding.initial.source.type === 'Literal' &&
376
- binding.initial.source.value === 'ripple'
377
- );
378
- }
379
-
380
- return false;
381
- }
382
-
383
- /**
384
- * Returns true if node is a function declared within a component
385
- * @param {AST.Node} node
386
- * @param {CommonContext} context
387
- * @returns {boolean}
388
- */
389
- export function is_declared_function_within_component(node, context) {
390
- const component = context.path?.find((n) => n.type === 'Component');
391
-
392
- if (node.type === 'Identifier' && component) {
393
- const binding = context.state.scope.get(node.name);
394
- const component_scope = context.state.scopes.get(component);
395
-
396
- if (binding !== null && component_scope !== undefined) {
397
- if (
398
- binding.declaration_kind !== 'function' &&
399
- binding.initial?.type !== 'FunctionDeclaration' &&
400
- binding.initial?.type !== 'ArrowFunctionExpression' &&
401
- binding.initial?.type !== 'FunctionExpression'
402
- ) {
403
- return false;
404
- }
405
- /** @type {ScopeInterface | null} */
406
- let scope = binding.scope;
407
-
408
- while (scope !== null) {
409
- if (scope === component_scope) {
410
- return true;
411
- }
412
- scope = scope.parent;
413
- }
414
- }
415
- }
416
-
417
- return false;
418
- }
419
- /**
420
- * Visits and transforms an assignment expression
421
- * @param {AST.AssignmentExpression} node
422
- * @param {CommonContext} context
423
- * @param {Function} build_assignment
424
- * @returns {AST.Expression | AST.AssignmentExpression | null}
425
- */
426
- export function visit_assignment_expression(node, context, build_assignment) {
427
- if (
428
- node.left.type === 'ArrayPattern' ||
429
- node.left.type === 'ObjectPattern' ||
430
- node.left.type === 'RestElement'
431
- ) {
432
- const value = /** @type {AST.Expression} */ (context.visit(node.right));
433
- const should_cache = value.type !== 'Identifier';
434
- const rhs = should_cache ? b.id('$$value') : value;
435
-
436
- let changed = false;
437
-
438
- const assignments = extract_paths(node.left).map((path) => {
439
- const value = path.expression?.(rhs);
440
-
441
- let assignment = build_assignment('=', path.node, value, context);
442
- if (assignment !== null) changed = true;
443
-
444
- return (
445
- assignment ??
446
- b.assignment(
447
- '=',
448
- /** @type {AST.Pattern} */ (context.visit(path.node)),
449
- /** @type {AST.Expression} */ (context.visit(value)),
450
- )
451
- );
452
- });
453
-
454
- if (!changed) {
455
- // No change to output -> nothing to transform -> we can keep the original assignment
456
- return null;
457
- }
458
-
459
- const is_standalone = context.path.at(-1)?.type.endsWith('Statement');
460
- const sequence = b.sequence(assignments);
461
-
462
- if (!is_standalone) {
463
- // this is part of an expression, we need the sequence to end with the value
464
- sequence.expressions.push(rhs);
465
- }
466
-
467
- if (should_cache) {
468
- // the right hand side is a complex expression, wrap in an IIFE to cache it
469
- const iife = b.arrow([rhs], sequence);
470
-
471
- return b.call(iife, value);
472
- }
473
-
474
- return sequence;
475
- }
476
-
477
- if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
478
- throw new Error(`Unexpected assignment type ${node.left.type}`);
479
- }
480
-
481
- const transformed = build_assignment(node.operator, node.left, node.right, context);
482
-
483
- if (transformed === node.left) {
484
- return node;
485
- }
486
-
487
- return transformed;
488
- }
489
-
490
- /**
491
- * Builds an assignment node, possibly transforming for reactivity
492
- * @param {AST.AssignmentOperator} operator
493
- * @param {AST.Pattern} left
494
- * @param {AST.Expression} right
495
- * @param {CommonContext} context
496
- * @returns {AST.Expression | null}
497
- */
498
- export function build_assignment(operator, left, right, context) {
499
- let object = left;
500
-
501
- while (object.type === 'MemberExpression') {
502
- // @ts-expect-error
503
- object = object.object;
504
- }
505
-
506
- if (object.type !== 'Identifier') {
507
- return null;
508
- }
509
-
510
- const binding = context.state.scope.get(object.name);
511
- if (!binding) return null;
512
-
513
- const transform = binding.transform;
514
-
515
- // reassignment
516
- if (object === left || (left.type === 'MemberExpression' && left.computed && operator === '=')) {
517
- const assign_fn = transform?.assign;
518
- if (assign_fn) {
519
- let value = /** @type {AST.Expression} */ (
520
- context.visit(build_assignment_value(operator, left, right))
521
- );
522
-
523
- return assign_fn(object, value);
524
- }
525
- }
526
-
527
- return null;
528
- }
529
-
530
- const ATTR_REGEX = /[&"<]/g;
531
- const CONTENT_REGEX = /[&<]/g;
532
-
533
- /**
534
- * Escapes HTML special characters in a string
535
- * @param {string | number | bigint | boolean | RegExp | null | undefined} value
536
- * @param {boolean} [is_attr=false]
537
- * @returns {string}
538
- */
539
- export function escape_html(value, is_attr = false) {
540
- const str = String(value ?? '');
541
-
542
- const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
543
- pattern.lastIndex = 0;
544
-
545
- let escaped = '';
546
- let last = 0;
547
-
548
- while (pattern.test(str)) {
549
- const i = pattern.lastIndex - 1;
550
- const ch = str[i];
551
- escaped += str.substring(last, i) + (ch === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
552
- last = i + 1;
553
- }
554
-
555
- return escaped + str.substring(last);
556
- }
557
-
558
- /**
559
- * Returns true if node is a DOM element (not a component)
560
- * @param {AST.Node} node
561
- * @returns {boolean}
562
- */
563
- export function is_element_dom_element(node) {
564
- const id = /** @type {AST.Element} */ (node).id;
565
- return (
566
- id.type === 'Identifier' &&
567
- id.name[0].toLowerCase() === id.name[0] &&
568
- id.name !== 'children' &&
569
- !id.tracked
570
- );
571
- }
572
-
573
- /**
574
- * Returns true if element is a dynamic element
575
- * @param {AST.Element} node
576
- * @returns {boolean}
577
- */
578
- export function is_element_dynamic(node) {
579
- return is_id_dynamic(node.id);
580
- }
581
-
582
- /**
583
- * @param {AST.Identifier | AST.MemberExpression | AST.Literal} node
584
- * @returns {boolean}
585
- */
586
- function is_id_dynamic(node) {
587
- if (node.type === 'Identifier') {
588
- return !!node.tracked;
589
- }
590
-
591
- return false;
592
- }
593
-
594
- /**
595
- * Normalizes children nodes (merges adjacent text, removes empty)
596
- * @param {AST.Node[]} children
597
- * @param {CommonContext} context
598
- * @returns {AST.Node[]}
599
- */
600
- export function normalize_children(children, context) {
601
- /** @type {AST.Node[]} */
602
- const normalized = [];
603
-
604
- for (const node of children) {
605
- normalize_child(node, normalized, context);
606
- }
607
-
608
- for (let i = normalized.length - 1; i >= 0; i--) {
609
- const child = normalized[i];
610
- const prev_child = normalized[i - 1];
611
-
612
- if (
613
- (child.type === 'RippleExpression' || child.type === 'Text') &&
614
- (prev_child?.type === 'RippleExpression' || prev_child?.type === 'Text')
615
- ) {
616
- if (
617
- (child.type === 'RippleExpression' &&
618
- is_children_template_expression(child.expression, context.state.scope)) ||
619
- (prev_child.type === 'RippleExpression' &&
620
- is_children_template_expression(prev_child.expression, context.state.scope))
621
- ) {
622
- continue;
623
- }
624
-
625
- if (prev_child.type === 'Text' || child.type === 'Text') {
626
- prev_child.type = 'Text';
627
- }
628
- if (child.expression.type === 'Literal' && prev_child.expression.type === 'Literal') {
629
- prev_child.expression = b.literal(
630
- prev_child.expression.value + String(child.expression.value),
631
- );
632
- } else {
633
- prev_child.expression = b.binary(
634
- '+',
635
- prev_child.expression,
636
- b.call('String', child.expression),
637
- );
638
- }
639
- normalized.splice(i, 1);
640
- }
641
- }
642
-
643
- return normalized;
644
- }
645
-
646
- /**
647
- * @param {AST.Expression} expression
648
- * @returns {AST.Expression}
649
- */
650
- export function unwrap_template_expression(expression) {
651
- /** @type {AST.Expression} */
652
- let node = expression;
653
-
654
- while (true) {
655
- if (
656
- node.type === 'ParenthesizedExpression' ||
657
- node.type === 'TSAsExpression' ||
658
- node.type === 'TSSatisfiesExpression' ||
659
- node.type === 'TSNonNullExpression' ||
660
- node.type === 'TSInstantiationExpression'
661
- ) {
662
- node = /** @type {AST.Expression} */ (node.expression);
663
- continue;
664
- }
665
-
666
- if (node.type === 'ChainExpression') {
667
- node = /** @type {AST.Expression} */ (node.expression);
668
- continue;
669
- }
670
-
671
- break;
672
- }
673
-
674
- return node;
675
- }
676
-
677
- /**
678
- * @param {AST.Expression} expression
679
- * @param {ScopeInterface | null | undefined} scope
680
- * @param {ScopeInterface | null} [component_scope]
681
- * @returns {boolean}
682
- */
683
- export function is_children_template_expression(expression, scope, component_scope = null) {
684
- if (scope == null) {
685
- return false;
686
- }
687
-
688
- const unwrapped = unwrap_template_expression(expression);
689
-
690
- if (unwrapped.type === 'MemberExpression') {
691
- let property_name = null;
692
-
693
- if (!unwrapped.computed && unwrapped.property.type === 'Identifier') {
694
- property_name = unwrapped.property.name;
695
- } else if (
696
- unwrapped.computed &&
697
- unwrapped.property.type === 'Literal' &&
698
- typeof unwrapped.property.value === 'string'
699
- ) {
700
- property_name = unwrapped.property.value;
701
- }
702
-
703
- if (property_name === 'children') {
704
- const target = unwrap_template_expression(/** @type {AST.Expression} */ (unwrapped.object));
705
-
706
- if (target.type === 'Identifier') {
707
- const binding = scope.get(target.name);
708
- return (
709
- binding?.declaration_kind === 'param' &&
710
- (component_scope === null || binding.scope === component_scope)
711
- );
712
- }
713
- }
714
- }
715
-
716
- if (unwrapped.type !== 'Identifier' || unwrapped.name !== 'children') {
717
- return false;
718
- }
719
-
720
- const binding = scope.get(unwrapped.name);
721
- return (
722
- (binding?.declaration_kind === 'param' ||
723
- binding?.kind === 'prop' ||
724
- binding?.kind === 'prop_fallback' ||
725
- binding?.kind === 'lazy' ||
726
- binding?.kind === 'lazy_fallback') &&
727
- (component_scope === null || binding.scope === component_scope)
728
- );
729
- }
730
-
731
- /**
732
- * @param {AST.Node} node
733
- * @param {AST.Node[]} normalized
734
- * @param {CommonContext} context
735
- */
736
- function normalize_child(node, normalized, context) {
737
- if (node.type === 'EmptyStatement') {
738
- return;
739
- } else if (
740
- node.type === 'Element' &&
741
- node.id.type === 'Identifier' &&
742
- ((node.id.name === 'style' &&
743
- !context.state.inside_head &&
744
- !context.state.keep_component_style) ||
745
- node.id.name === 'head' ||
746
- (node.id.name === 'title' && context.state.inside_head))
747
- ) {
748
- return;
749
- } else {
750
- normalized.push(node);
751
- }
752
- }
753
-
754
- /**
755
- * Replaces any lazy subpatterns in a parameter pattern with their generated identifiers.
756
- * This is used by client and server transforms so nested lazy destructuring can coexist
757
- * with otherwise normal object/array params.
758
- * @param {AST.Pattern} pattern
759
- * @returns {AST.Pattern}
760
- */
761
- export function replace_lazy_param_pattern(pattern) {
762
- switch (pattern.type) {
763
- case 'AssignmentPattern':
764
- return { ...pattern, left: replace_lazy_param_pattern(pattern.left) };
765
-
766
- case 'ObjectPattern':
767
- if (pattern.lazy && pattern.metadata?.lazy_id) {
768
- return /** @type {AST.Pattern} */ (b.id(pattern.metadata.lazy_id));
769
- }
770
-
771
- return {
772
- ...pattern,
773
- properties: pattern.properties.map((property) =>
774
- property.type === 'RestElement'
775
- ? { ...property, argument: replace_lazy_param_pattern(property.argument) }
776
- : { ...property, value: replace_lazy_param_pattern(property.value) },
777
- ),
778
- };
779
-
780
- case 'ArrayPattern':
781
- if (pattern.lazy && pattern.metadata?.lazy_id) {
782
- return /** @type {AST.Pattern} */ (b.id(pattern.metadata.lazy_id));
783
- }
784
-
785
- return {
786
- ...pattern,
787
- elements: pattern.elements.map((element) =>
788
- element === null ? null : replace_lazy_param_pattern(element),
789
- ),
790
- };
791
-
792
- case 'RestElement':
793
- return { ...pattern, argument: replace_lazy_param_pattern(pattern.argument) };
794
-
795
- default:
796
- return pattern;
797
- }
798
- }
799
-
800
- /**
801
- * @param {CommonContext} context
802
- */
803
- export function get_parent_block_node(context) {
804
- const path = context.path;
805
-
806
- for (let i = path.length - 1; i >= 0; i -= 1) {
807
- const context_node = path[i];
808
- if (
809
- context_node.type === 'IfStatement' ||
810
- context_node.type === 'ForOfStatement' ||
811
- context_node.type === 'SwitchStatement' ||
812
- context_node.type === 'TryStatement' ||
813
- context_node.type === 'Component'
814
- ) {
815
- return context_node;
816
- }
817
- if (
818
- context_node.type === 'FunctionExpression' ||
819
- context_node.type === 'ArrowFunctionExpression' ||
820
- context_node.type === 'FunctionDeclaration'
821
- ) {
822
- return null;
823
- }
824
- }
825
- return null;
826
- }
827
-
828
- /**
829
- * Builds a getter for a tracked identifier
830
- * @param {AST.Identifier} node
831
- * @param {CommonContext} context
832
- * @returns {AST.Expression | AST.Identifier}
833
- */
834
- export function build_getter(node, context) {
835
- const state = context.state;
836
-
837
- if (!context.path) return node;
838
-
839
- for (let i = context.path.length - 1; i >= 0; i -= 1) {
840
- const binding = state.scope.get(node.name);
841
- const transform = binding?.transform;
842
-
843
- // don't transform the declaration itself
844
- if (node !== binding?.node) {
845
- const read_fn = transform?.read;
846
-
847
- if (read_fn) {
848
- return read_fn(node);
849
- }
850
- }
851
- }
852
-
853
- return node;
854
- }
855
-
856
- /**
857
- * Determines the namespace for child elements
858
- * @param {string} element_name
859
- * @param {NameSpace} current_namespace
860
- * @returns {NameSpace}
861
- */
862
- export function determine_namespace_for_children(element_name, current_namespace) {
863
- if (element_name === 'foreignObject') {
864
- return 'html';
865
- }
866
-
867
- if (element_name === 'svg') {
868
- return 'svg';
869
- }
870
-
871
- if (element_name === 'math') {
872
- return 'mathml';
873
- }
874
-
875
- return current_namespace;
876
- }
877
-
878
- /**
879
- * Converts and index to a key string, where the starting character is a
880
- * letter.
881
- * @param {number} index
882
- */
883
- export function index_to_key(index) {
884
- const letters = 'abcdefghijklmnopqrstuvwxyz';
885
- let key = '';
886
-
887
- do {
888
- key = letters[index % 26] + key;
889
- index = Math.floor(index / 26) - 1;
890
- } while (index >= 0);
891
-
892
- return key;
893
- }
894
-
895
- /**
896
- * Check if a binding ultimately refers to a function, following reference chains
897
- * @param {Binding} binding
898
- * @param {ScopeInterface} scope
899
- * @param {Set<Binding>} visited
900
- * @returns {boolean}
901
- */
902
- export function is_binding_function(binding, scope, visited = new Set()) {
903
- if (!binding || visited.has(binding)) {
904
- return false;
905
- }
906
- visited.add(binding);
907
-
908
- const initial = binding.initial;
909
- if (!initial) {
910
- return false;
911
- }
912
-
913
- // Direct function
914
- if (
915
- initial.type === 'FunctionDeclaration' ||
916
- initial.type === 'FunctionExpression' ||
917
- initial.type === 'ArrowFunctionExpression'
918
- ) {
919
- return true;
920
- }
921
-
922
- // Follow identifier references (e.g., const alias = myFunc)
923
- if (initial.type === 'Identifier') {
924
- const next_binding = scope.get(initial.name);
925
- if (next_binding) {
926
- return is_binding_function(next_binding, scope, visited);
927
- }
928
- }
929
-
930
- return false;
931
- }
932
-
933
- /**
934
- * @param {AST.TryStatement} try_parent_stmt
935
- * @param {CommonContext} context
936
- * @returns {boolean}
937
- */
938
- export function is_inside_try_block(try_parent_stmt, context) {
939
- /** @type {AST.BlockStatement | null} */
940
- let block_node = null;
941
- for (let i = context.path.length - 1; i >= 0; i -= 1) {
942
- const context_node = context.path[i];
943
-
944
- if (context_node.type === 'BlockStatement') {
945
- block_node = /** @type {AST.BlockStatement} */ (context_node);
946
- }
947
-
948
- if (context_node === try_parent_stmt) {
949
- break;
950
- }
951
- }
952
-
953
- return block_node !== null && try_parent_stmt.block === block_node;
954
- }
955
-
956
- /**
957
- * Checks if a node is used as the left side of an assignment or update expression.
958
- * @param {AST.Node} node
959
- * @returns {boolean}
960
- */
961
- export function is_inside_left_side_assignment(node) {
962
- const path = node.metadata?.path;
963
- if (!path || path.length === 0) {
964
- return false;
965
- }
966
-
967
- /** @type {AST.Node} */
968
- let current = node;
969
-
970
- for (let i = path.length - 1; i >= 0; i--) {
971
- const parent = path[i];
972
-
973
- switch (parent.type) {
974
- case 'AssignmentExpression':
975
- case 'AssignmentPattern':
976
- if (parent.right === current) {
977
- return false;
978
- }
979
-
980
- if (parent.left === current) {
981
- return true;
982
- }
983
- current = parent;
984
- continue;
985
- case 'UpdateExpression':
986
- return true;
987
- case 'MemberExpression':
988
- // In obj[computeKey()] = 10, computeKey() is evaluated to determine
989
- // which property to assign to, but is not itself an assignment target
990
- if (parent.computed && parent.property === current) {
991
- return false;
992
- }
993
- current = parent;
994
- continue;
995
- case 'Property':
996
- // exit here to stop promoting current to parent
997
- // and thus reaching VariableDeclarator, causing an erroneous truthy result
998
- // e.g. const { [computeKey()]: value } = obj; where node = computeKey:
999
- if (parent.key === current) {
1000
- return false;
1001
- }
1002
- current = parent;
1003
- continue;
1004
- case 'VariableDeclarator':
1005
- return parent.id === current;
1006
- case 'ForInStatement':
1007
- case 'ForOfStatement':
1008
- return parent.left === current;
1009
-
1010
- case 'Program':
1011
- case 'FunctionDeclaration':
1012
- case 'FunctionExpression':
1013
- case 'ArrowFunctionExpression':
1014
- case 'ClassDeclaration':
1015
- case 'ClassExpression':
1016
- case 'MethodDefinition':
1017
- case 'PropertyDefinition':
1018
- case 'StaticBlock':
1019
- case 'Component':
1020
- case 'Element':
1021
- return false;
1022
-
1023
- default:
1024
- current = parent;
1025
- continue;
1026
- }
1027
- }
1028
-
1029
- return false;
1030
- }
1031
-
1032
- /**
1033
- * Flattens top-level BlockStatements in switch case consequents so that
1034
- * BreakStatements and elements inside block-scoped cases are properly handled.
1035
- * e.g. `case 1: { <div /> break; }` → `[Element, BreakStatement]`
1036
- * @param {AST.Node[]} consequent
1037
- * @returns {AST.Node[]}
1038
- */
1039
- export function flatten_switch_consequent(consequent) {
1040
- /** @type {AST.Node[]} */
1041
- const result = [];
1042
- for (const node of consequent) {
1043
- if (node.type === 'BlockStatement') {
1044
- result.push(.../** @type {AST.BlockStatement} */ (node).body);
1045
- } else {
1046
- result.push(node);
1047
- }
1048
- }
1049
- return result;
1050
- }
1051
-
1052
- /**
1053
- * @param {string | null | undefined} name
1054
- * @returns {string | null}
1055
- */
1056
- export function get_ripple_namespace_call_name(name) {
1057
- return name == null ? null : (RIPPLE_IMPORT_CALL_NAME[name]?.name ?? null);
1058
- }
1059
-
1060
- /**
1061
- * Returns true if the given import name requires a __block parameter
1062
- * @param {string} name
1063
- * @returns {boolean}
1064
- */
1065
- export function ripple_import_requires_block(name) {
1066
- return name == null ? false : (RIPPLE_IMPORT_CALL_NAME[name]?.requiresBlock ?? false);
1067
- }
1068
-
1069
- /**
1070
- * @param {AST.ClassDeclaration | AST.ClassExpression} node
1071
- * @param {CommonContext} context
1072
- * @returns {void}
1073
- */
1074
- export function strip_class_typescript_syntax(node, context) {
1075
- delete node.typeParameters;
1076
- delete node.superTypeParameters;
1077
- delete node.implements;
1078
-
1079
- if (node.superClass?.type === 'TSInstantiationExpression') {
1080
- node.superClass = /** @type {AST.Expression} */ (context.visit(node.superClass.expression));
1081
- } else if (node.superClass && 'typeArguments' in node.superClass) {
1082
- delete node.superClass.typeArguments;
1083
- }
1084
- }
1085
-
1086
- /**
1087
- * Converts a JSXMemberExpression to an AST MemberExpression.
1088
- * e.g., <Foo.Bar.Baz> → MemberExpression(MemberExpression(Foo, Bar), Baz)
1089
- * @param {import('estree-jsx').JSXMemberExpression} jsx_member
1090
- * @returns {AST.MemberExpression}
1091
- */
1092
- function jsx_member_expression_to_member_expression(jsx_member) {
1093
- /** @type {AST.Expression} */
1094
- let object;
1095
-
1096
- if (jsx_member.object.type === 'JSXMemberExpression') {
1097
- // Recursively convert nested member expressions
1098
- object = jsx_member_expression_to_member_expression(jsx_member.object);
1099
- } else {
1100
- // Base case: JSXIdentifier
1101
- object = /** @type {AST.Identifier} */ ({
1102
- type: 'Identifier',
1103
- name: jsx_member.object.name,
1104
- start: jsx_member.object.start,
1105
- end: jsx_member.object.end,
1106
- });
1107
- }
1108
-
1109
- return /** @type {AST.MemberExpression} */ ({
1110
- type: 'MemberExpression',
1111
- object,
1112
- property: /** @type {AST.Identifier} */ ({
1113
- type: 'Identifier',
1114
- name: jsx_member.property.name,
1115
- start: jsx_member.property.start,
1116
- end: jsx_member.property.end,
1117
- }),
1118
- computed: false,
1119
- optional: false,
1120
- start: jsx_member.start,
1121
- end: jsx_member.end,
1122
- });
1123
- }
1124
-
1125
- /**
1126
- * Converts a JSX AST node (JSXElement, JSXText, etc.) to a Ripple AST node
1127
- * (Element, Text, RippleExpression) for processing inside `<tsx>` blocks.
1128
- * @param {AST.Node} node
1129
- * @returns {AST.Node | AST.Node[] | null}
1130
- */
1131
- export function jsx_to_ripple_node(node) {
1132
- if (node.type === 'JSXElement') {
1133
- const opening = node.openingElement;
1134
- const name = opening.name;
1135
-
1136
- /** @type {AST.Identifier | AST.MemberExpression} */
1137
- let id;
1138
-
1139
- if (name.type === 'JSXIdentifier') {
1140
- id = /** @type {AST.Identifier} */ ({
1141
- type: 'Identifier',
1142
- name: name.name,
1143
- start: name.start,
1144
- end: name.end,
1145
- });
1146
- } else if (name.type === 'JSXMemberExpression') {
1147
- // Convert JSXMemberExpression to MemberExpression
1148
- // e.g., <Foo.Bar.Baz> → MemberExpression(MemberExpression(Foo, Bar), Baz)
1149
- id = jsx_member_expression_to_member_expression(name);
1150
- } else if (name.type === 'JSXNamespacedName') {
1151
- // For JSXNamespacedName like <namespace:element>, create an identifier with the full name
1152
- id = /** @type {AST.Identifier} */ ({
1153
- type: 'Identifier',
1154
- name: name.namespace.name + ':' + name.name.name,
1155
- start: name.start,
1156
- end: name.end,
1157
- });
1158
- } else {
1159
- // Fallback - should not reach here
1160
- id = /** @type {AST.Identifier} */ ({
1161
- type: 'Identifier',
1162
- name: 'unknown',
1163
- start: /** @type {any} */ (name).start,
1164
- end: /** @type {any} */ (name).end,
1165
- });
1166
- }
1167
-
1168
- const attributes = opening.attributes
1169
- .map((attr) => {
1170
- if (attr.type === 'JSXAttribute') {
1171
- const is_dynamic = attr.value && attr.value.type === 'JSXExpressionContainer';
1172
- return /** @type {AST.Node} */ ({
1173
- type: 'Attribute',
1174
- name: {
1175
- type: 'Identifier',
1176
- name:
1177
- attr.name.type === 'JSXIdentifier'
1178
- ? attr.name.name
1179
- : attr.name.namespace.name + ':' + attr.name.name.name,
1180
- tracked: is_dynamic,
1181
- start: attr.name.start,
1182
- end: attr.name.end,
1183
- },
1184
- value: attr.value
1185
- ? attr.value.type === 'JSXExpressionContainer'
1186
- ? attr.value.expression
1187
- : attr.value
1188
- : null,
1189
- shorthand: false,
1190
- start: attr.start,
1191
- end: attr.end,
1192
- });
1193
- } else if (attr.type === 'JSXSpreadAttribute') {
1194
- return /** @type {AST.Node} */ ({
1195
- type: 'SpreadAttribute',
1196
- argument: attr.argument,
1197
- start: attr.start,
1198
- end: attr.end,
1199
- });
1200
- }
1201
- return null;
1202
- })
1203
- .filter(Boolean);
1204
-
1205
- const children = /** @type {AST.Node[]} */ (
1206
- /** @type {AST.Node[]} */ (node.children).map(jsx_to_ripple_node).flat().filter(Boolean)
1207
- );
1208
-
1209
- return /** @type {AST.Element} */ (
1210
- /** @type {unknown} */ ({
1211
- type: 'Element',
1212
- id,
1213
- attributes,
1214
- children,
1215
- selfClosing: opening.selfClosing,
1216
- metadata: { scoped: false, path: /** @type {string[]} */ ([]) },
1217
- start: node.start,
1218
- end: node.end,
1219
- })
1220
- );
1221
- }
1222
-
1223
- if (node.type === 'JSXText') {
1224
- if (node.value.trim() === '') return null;
1225
- return /** @type {AST.Node} */ ({
1226
- type: 'Text',
1227
- expression: {
1228
- type: 'Literal',
1229
- value: node.value,
1230
- raw: JSON.stringify(node.value),
1231
- start: node.start,
1232
- end: node.end,
1233
- },
1234
- metadata: {},
1235
- start: node.start,
1236
- end: node.end,
1237
- });
1238
- }
1239
-
1240
- if (node.type === 'JSXExpressionContainer') {
1241
- if (node.expression.type === 'JSXEmptyExpression') return null;
1242
- return /** @type {AST.Node} */ ({
1243
- type: 'RippleExpression',
1244
- expression: node.expression,
1245
- metadata: {},
1246
- start: node.start,
1247
- end: node.end,
1248
- });
1249
- }
1250
-
1251
- if (node.type === 'JSXFragment') {
1252
- return /** @type {AST.Node[]} */ (
1253
- /** @type {AST.Node[]} */ (node.children).map(jsx_to_ripple_node).flat().filter(Boolean)
1254
- );
1255
- }
1256
-
1257
- return node;
1258
- }