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,2208 +0,0 @@
1
- /** @import {AnalyzeOptions} from 'ripple/compiler' */
2
- /**
3
- @import {
4
- AnalysisResult,
5
- AnalysisState,
6
- AnalysisContext,
7
- Context,
8
- ScopeInterface,
9
- Visitors,
10
- TopScopedClasses,
11
- StyleClasses,
12
- } from '#compiler';
13
- */
14
- /**
15
- @import * as AST from 'estree';
16
- @import * as ESTreeJSX from 'estree-jsx';
17
- */
18
-
19
- import * as b from '../../../utils/builders.js';
20
- import { walk } from 'zimmerframe';
21
- import { create_scopes, ScopeRoot } from '../../scope.js';
22
- import {
23
- is_delegated_event,
24
- get_parent_block_node,
25
- is_element_dom_element,
26
- is_inside_component,
27
- is_ripple_track_call,
28
- is_void_element,
29
- is_children_template_expression as is_children_template_expression_in_scope,
30
- normalize_children,
31
- is_binding_function,
32
- is_inside_try_block,
33
- } from '../../utils.js';
34
- import { extract_paths } from '../../../utils/ast.js';
35
- import is_reference from 'is-reference';
36
- import { prune_css } from './prune.js';
37
- import { analyze_css } from './css-analyze.js';
38
- import { error } from '../../errors.js';
39
- import { is_event_attribute } from '../../../utils/events.js';
40
- import { validate_nesting } from './validation.js';
41
-
42
- const valid_in_head = new Set(['title', 'base', 'link', 'meta', 'style', 'script', 'noscript']);
43
-
44
- const mutating_method_names = new Set([
45
- 'add',
46
- 'append',
47
- 'clear',
48
- 'copyWithin',
49
- 'delete',
50
- 'fill',
51
- 'pop',
52
- 'push',
53
- 'reverse',
54
- 'set',
55
- 'shift',
56
- 'sort',
57
- 'splice',
58
- 'unshift',
59
- ]);
60
-
61
- /**
62
- * @param {AST.MemberExpression} node
63
- * @returns {string | null}
64
- */
65
- function get_member_name(node) {
66
- if (!node.computed && node.property.type === 'Identifier') {
67
- return node.property.name;
68
- }
69
-
70
- if (node.computed && node.property.type === 'Literal') {
71
- return typeof node.property.value === 'string' ? node.property.value : null;
72
- }
73
-
74
- return null;
75
- }
76
-
77
- /**
78
- * @param {AST.CallExpression} node
79
- * @returns {boolean}
80
- */
81
- function is_mutating_call_expression(node) {
82
- return (
83
- node.callee.type === 'MemberExpression' &&
84
- mutating_method_names.has(get_member_name(node.callee) ?? '')
85
- );
86
- }
87
-
88
- /**
89
- * Check if an expression contains side effects or other impure operations.
90
- * Template expressions should be pure reads.
91
- * @param {AST.Expression | AST.SpreadElement | AST.Super | AST.Pattern} node
92
- * @returns {boolean}
93
- */
94
- function expression_has_side_effects(node) {
95
- switch (node.type) {
96
- case 'AssignmentExpression':
97
- case 'UpdateExpression':
98
- return true;
99
- case 'SequenceExpression':
100
- return node.expressions.some(expression_has_side_effects);
101
- case 'ConditionalExpression':
102
- return (
103
- expression_has_side_effects(node.test) ||
104
- expression_has_side_effects(node.consequent) ||
105
- expression_has_side_effects(node.alternate)
106
- );
107
- case 'LogicalExpression':
108
- case 'BinaryExpression':
109
- return (
110
- expression_has_side_effects(/** @type {AST.Expression} */ (node.left)) ||
111
- expression_has_side_effects(node.right)
112
- );
113
- case 'UnaryExpression':
114
- // delete operator has side effects (removes object properties)
115
- if (node.operator === 'delete') return true;
116
- return expression_has_side_effects(node.argument);
117
- case 'AwaitExpression':
118
- return expression_has_side_effects(node.argument);
119
- case 'ChainExpression':
120
- return expression_has_side_effects(node.expression);
121
- case 'MemberExpression':
122
- return (
123
- expression_has_side_effects(node.object) ||
124
- (node.computed &&
125
- expression_has_side_effects(/** @type {AST.Expression} */ (node.property)))
126
- );
127
- case 'CallExpression':
128
- return (
129
- is_mutating_call_expression(node) ||
130
- expression_has_side_effects(node.callee) ||
131
- node.arguments.some(expression_has_side_effects)
132
- );
133
- case 'NewExpression':
134
- return (
135
- expression_has_side_effects(node.callee) || node.arguments.some(expression_has_side_effects)
136
- );
137
- case 'TemplateLiteral':
138
- return node.expressions.some(expression_has_side_effects);
139
- case 'TaggedTemplateExpression':
140
- return (
141
- expression_has_side_effects(node.tag) ||
142
- node.quasi.expressions.some(expression_has_side_effects)
143
- );
144
- case 'ArrayExpression':
145
- return node.elements.some((el) => el !== null && expression_has_side_effects(el));
146
- case 'ObjectExpression':
147
- return node.properties.some((prop) =>
148
- prop.type === 'SpreadElement'
149
- ? expression_has_side_effects(prop.argument)
150
- : expression_has_side_effects(prop.value) ||
151
- (prop.computed &&
152
- expression_has_side_effects(/** @type {AST.Expression} */ (prop.key))),
153
- );
154
- case 'SpreadElement':
155
- return expression_has_side_effects(node.argument);
156
- default:
157
- return false;
158
- }
159
- }
160
-
161
- /**
162
- * @param {AnalysisContext['path']} path
163
- */
164
- function mark_control_flow_has_template(path) {
165
- for (let i = path.length - 1; i >= 0; i -= 1) {
166
- const node = path[i];
167
-
168
- if (
169
- node.type === 'Component' ||
170
- node.type === 'FunctionExpression' ||
171
- node.type === 'ArrowFunctionExpression' ||
172
- node.type === 'FunctionDeclaration'
173
- ) {
174
- break;
175
- }
176
- if (
177
- node.type === 'ForStatement' ||
178
- node.type === 'ForInStatement' ||
179
- node.type === 'ForOfStatement' ||
180
- node.type === 'TryStatement' ||
181
- node.type === 'IfStatement' ||
182
- node.type === 'SwitchStatement' ||
183
- node.type === 'Tsx' ||
184
- node.type === 'TsxCompat'
185
- ) {
186
- node.metadata.has_template = true;
187
- }
188
- }
189
- }
190
-
191
- /**
192
- * Set up lazy destructuring transforms for bindings extracted from a lazy pattern.
193
- * Converts each destructured identifier into a binding that lazily accesses properties
194
- * on the source identifier (e.g., `a` → `source.a` for object, `a` → `source[0]` for array).
195
- * @param {AST.ObjectPattern | AST.ArrayPattern} pattern - The destructuring pattern with lazy: true
196
- * @param {AST.Identifier} source_id - The identifier to access properties on
197
- * @param {AnalysisState} state - The analysis state
198
- * @param {boolean} writable - Whether assignments/updates should be supported (let vs const)
199
- * @param {boolean} is_track_call - Whether the RHS is a Ripple track() call
200
- */
201
- function setup_lazy_transforms(pattern, source_id, state, writable, is_track_call) {
202
- // For ArrayPattern from track() calls, use direct get/set calls as a fast path
203
- // instead of going through prototype getters source[0]/source[1]
204
- if (pattern.type === 'ArrayPattern' && is_track_call) {
205
- setup_lazy_array_transforms(pattern, source_id, state, writable);
206
- return;
207
- }
208
-
209
- const paths = extract_paths(pattern);
210
-
211
- for (const path of paths) {
212
- const name = /** @type {AST.Identifier} */ (path.node).name;
213
- const binding = state.scope.get(name);
214
-
215
- if (binding !== null) {
216
- const has_fallback = path.has_default_value;
217
- binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
218
-
219
- binding.transform = {
220
- read: (_) => {
221
- return path.expression(source_id);
222
- },
223
- };
224
-
225
- if (writable) {
226
- binding.transform.assign = (node, value) => {
227
- return b.assignment(
228
- '=',
229
- /** @type {AST.MemberExpression} */ (path.update_expression(source_id)),
230
- value,
231
- );
232
- };
233
-
234
- if (has_fallback) {
235
- // For bindings with default values, generate proper fallback-aware update
236
- // e.g., count++ with default 0 becomes:
237
- // (() => { var _v = _$_.fallback(obj.count, 0); obj.count = _v + 1; return _v; })() for postfix
238
- // (obj.count = _$_.fallback(obj.count, 0) + 1) for prefix
239
- binding.transform.update = (node) => {
240
- const member = path.update_expression(source_id);
241
- const fallback_read = path.expression(source_id);
242
- const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
243
-
244
- if (node.prefix) {
245
- // ++count: return new value
246
- return b.assignment(
247
- '=',
248
- /** @type {AST.Pattern} */ (member),
249
- b.binary('+', fallback_read, delta),
250
- );
251
- } else {
252
- // count++: return old value, write new value
253
- // Use IIFE to declare temp variable
254
- const temp = b.id('_v');
255
- return b.call(
256
- b.arrow(
257
- [],
258
- b.block([
259
- b.var(temp, fallback_read),
260
- b.stmt(
261
- b.assignment(
262
- '=',
263
- /** @type {AST.Pattern} */ (member),
264
- b.binary('+', temp, delta),
265
- ),
266
- ),
267
- b.return(temp),
268
- ]),
269
- ),
270
- );
271
- }
272
- };
273
- } else {
274
- binding.transform.update = (node) =>
275
- b.update(node.operator, path.update_expression(source_id), node.prefix);
276
- }
277
- }
278
- }
279
- }
280
- }
281
-
282
- /**
283
- * Set up fast-path transforms for lazy array destructuring of tracked values.
284
- * For index 0 (the value): uses _$_.get/set/update directly instead of source[0] getters.
285
- * For index 1 (the tracked ref): returns source directly instead of source[1].
286
- * @param {AST.ArrayPattern} pattern - The array destructuring pattern
287
- * @param {AST.Identifier} source_id - The identifier for the tracked value
288
- * @param {AnalysisState} state - The analysis state
289
- * @param {boolean} writable - Whether assignments/updates should be supported
290
- */
291
- function setup_lazy_array_transforms(pattern, source_id, state, writable) {
292
- for (let i = 0; i < pattern.elements.length; i++) {
293
- const element = pattern.elements[i];
294
- if (!element) continue;
295
-
296
- // Rest elements — fall back to generic source.slice(i)
297
- if (element.type === 'RestElement') {
298
- const rest_paths = extract_paths(pattern);
299
- for (const path of rest_paths) {
300
- if (!path.is_rest) continue;
301
- const name = /** @type {AST.Identifier} */ (path.node).name;
302
- const binding = state.scope.get(name);
303
- if (binding !== null) {
304
- binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
305
- binding.transform = {
306
- read: (_) => path.expression(source_id),
307
- };
308
- }
309
- }
310
- continue;
311
- }
312
-
313
- const actual = element.type === 'AssignmentPattern' ? element.left : element;
314
- const has_fallback = element.type === 'AssignmentPattern';
315
- /** @type {AST.Expression | null} */
316
- const fallback_value = has_fallback
317
- ? /** @type {AST.AssignmentPattern} */ (element).right
318
- : null;
319
-
320
- if (actual.type === 'Identifier' && i <= 1) {
321
- const name = actual.name;
322
- const binding = state.scope.get(name);
323
- if (binding === null) continue;
324
-
325
- binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
326
-
327
- if (i === 0) {
328
- // Fast path for index 0: use _$_.get(source) instead of source[0]
329
- const read_expr = has_fallback
330
- ? () =>
331
- b.call(
332
- '_$_.fallback',
333
- b.call('_$_.get', source_id),
334
- /** @type {AST.Expression} */ (fallback_value),
335
- )
336
- : () => b.call('_$_.get', source_id);
337
-
338
- // Signal that read already produces an unwrapped value (calls _$_.get internally)
339
- binding.read_unwraps = true;
340
-
341
- binding.transform = {
342
- read: (_) => read_expr(),
343
- };
344
-
345
- if (writable) {
346
- binding.transform.assign = (_, value) => {
347
- return b.call('_$_.set', source_id, value);
348
- };
349
-
350
- if (has_fallback) {
351
- binding.transform.update = (node) => {
352
- const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
353
- const temp = b.id('_v');
354
-
355
- if (node.prefix) {
356
- // ++count: compute new value and set it, return new value
357
- return b.call(
358
- b.arrow(
359
- [],
360
- b.block([
361
- b.var(temp, b.binary('+', read_expr(), delta)),
362
- b.stmt(b.call('_$_.set', source_id, temp)),
363
- b.return(temp),
364
- ]),
365
- ),
366
- );
367
- } else {
368
- // count++: read old value, set new value, return old value
369
- return b.call(
370
- b.arrow(
371
- [],
372
- b.block([
373
- b.var(temp, read_expr()),
374
- b.stmt(b.call('_$_.set', source_id, b.binary('+', temp, delta))),
375
- b.return(temp),
376
- ]),
377
- ),
378
- );
379
- }
380
- };
381
- } else {
382
- binding.transform.update = (node) => {
383
- const fn_name = node.prefix ? '_$_.update_pre' : '_$_.update';
384
- /** @type {AST.Expression[]} */
385
- const args = [source_id];
386
- if (node.operator === '--') {
387
- args.push(b.literal(-1));
388
- }
389
- return b.call(fn_name, ...args);
390
- };
391
- }
392
- }
393
- } else {
394
- // Fast path for index 1: source itself is the tracked ref
395
- binding.transform = {
396
- read: (_) => source_id,
397
- };
398
- }
399
- } else {
400
- // Nested patterns or indices > 1: fall back to generic source[i] access via extract_paths
401
- /** @type {(object: AST.Expression) => AST.Expression} */
402
- const base_expression =
403
- i === 0
404
- ? (object) => b.call('_$_.get', object)
405
- : i === 1
406
- ? (object) => object
407
- : (object) => b.member(object, b.literal(i), true);
408
-
409
- const inner_paths = extract_paths(element);
410
- for (const path of inner_paths) {
411
- const name = /** @type {AST.Identifier} */ (path.node).name;
412
- const binding = state.scope.get(name);
413
- if (binding === null) continue;
414
-
415
- binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
416
-
417
- binding.transform = {
418
- read: (_) =>
419
- path.expression(
420
- /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
421
- ),
422
- };
423
-
424
- if (writable) {
425
- binding.transform.assign = (node, value) => {
426
- return b.assignment(
427
- '=',
428
- /** @type {AST.MemberExpression} */ (
429
- path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id)))
430
- ),
431
- value,
432
- );
433
- };
434
-
435
- if (path.has_default_value) {
436
- binding.transform.update = (node) => {
437
- const member = path.update_expression(
438
- /** @type {AST.Identifier} */ (base_expression(source_id)),
439
- );
440
- const fallback_read = path.expression(
441
- /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
442
- );
443
- const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
444
-
445
- if (node.prefix) {
446
- return b.assignment(
447
- '=',
448
- /** @type {AST.Pattern} */ (member),
449
- b.binary('+', fallback_read, delta),
450
- );
451
- } else {
452
- const temp = b.id('_v');
453
- return b.call(
454
- b.arrow(
455
- [],
456
- b.block([
457
- b.var(temp, fallback_read),
458
- b.stmt(
459
- b.assignment(
460
- '=',
461
- /** @type {AST.Pattern} */ (member),
462
- b.binary('+', temp, delta),
463
- ),
464
- ),
465
- b.return(temp),
466
- ]),
467
- ),
468
- );
469
- }
470
- };
471
- } else {
472
- binding.transform.update = (node) =>
473
- b.update(
474
- node.operator,
475
- path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id))),
476
- node.prefix,
477
- );
478
- }
479
- }
480
- }
481
- }
482
- }
483
- }
484
-
485
- /**
486
- * @param {AST.Pattern} pattern
487
- * @returns {AST.TypeNode | undefined}
488
- */
489
- function get_pattern_type_annotation(pattern) {
490
- return pattern.typeAnnotation?.typeAnnotation;
491
- }
492
-
493
- /**
494
- * @param {AST.TypeNode | undefined} type_annotation
495
- * @returns {AST.TypeNode | undefined}
496
- */
497
- function unwrap_type_annotation(type_annotation) {
498
- /** @type {AST.TypeNode | undefined} */
499
- let annotation = type_annotation;
500
-
501
- while (annotation) {
502
- if (annotation.type === 'TSParenthesizedType') {
503
- annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
504
- continue;
505
- }
506
- if (annotation.type === 'TSOptionalType') {
507
- annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
508
- continue;
509
- }
510
- break;
511
- }
512
-
513
- return annotation;
514
- }
515
-
516
- /**
517
- * @param {AST.TypeNode} type_annotation
518
- * @returns {AST.TypeNode}
519
- */
520
- function normalize_tuple_element_type(type_annotation) {
521
- /** @type {AST.TypeNode} */
522
- let annotation = type_annotation;
523
-
524
- while (true) {
525
- if (annotation.type === 'TSNamedTupleMember') {
526
- annotation = annotation.elementType;
527
- continue;
528
- }
529
- if (annotation.type === 'TSParenthesizedType') {
530
- annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
531
- continue;
532
- }
533
- if (annotation.type === 'TSOptionalType') {
534
- annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
535
- continue;
536
- }
537
- break;
538
- }
539
-
540
- return annotation;
541
- }
542
-
543
- /**
544
- * @param {AST.Expression} key
545
- * @returns {string | null}
546
- */
547
- function get_object_pattern_key_name(key) {
548
- if (key.type === 'Identifier') {
549
- return key.name;
550
- }
551
- if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
552
- return String(key.value);
553
- }
554
- return null;
555
- }
556
-
557
- /**
558
- * @param {AST.PropertyNameNonComputed} key
559
- * @returns {string | null}
560
- */
561
- function get_type_property_key_name(key) {
562
- if (key.type === 'Identifier') {
563
- return key.name;
564
- }
565
- if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
566
- return String(key.value);
567
- }
568
- return null;
569
- }
570
-
571
- /**
572
- * @param {AST.TypeNode | undefined} type_annotation
573
- * @param {AST.Property | AST.RestElement} property
574
- * @returns {AST.TypeNode | undefined}
575
- */
576
- function get_object_property_type_annotation(type_annotation, property) {
577
- if (property.type === 'RestElement' || property.computed) {
578
- return undefined;
579
- }
580
-
581
- const object_type_annotation = unwrap_type_annotation(type_annotation);
582
- if (object_type_annotation?.type !== 'TSTypeLiteral') {
583
- return undefined;
584
- }
585
-
586
- const key_name = get_object_pattern_key_name(/** @type {AST.Expression} */ (property.key));
587
- if (key_name === null) {
588
- return undefined;
589
- }
590
-
591
- for (const member of object_type_annotation.members) {
592
- if (member.type !== 'TSPropertySignature' || member.computed) {
593
- continue;
594
- }
595
- const member_key_name = get_type_property_key_name(member.key);
596
- if (member_key_name === key_name) {
597
- return member.typeAnnotation?.typeAnnotation;
598
- }
599
- }
600
-
601
- return undefined;
602
- }
603
-
604
- /**
605
- * @param {AST.TypeNode | undefined} type_annotation
606
- * @param {number} index
607
- * @param {boolean} is_rest
608
- * @returns {AST.TypeNode | undefined}
609
- */
610
- function get_array_element_type_annotation(type_annotation, index, is_rest) {
611
- const array_type_annotation = unwrap_type_annotation(type_annotation);
612
-
613
- if (array_type_annotation?.type === 'TSArrayType') {
614
- return array_type_annotation.elementType;
615
- }
616
- if (array_type_annotation?.type !== 'TSTupleType') {
617
- return undefined;
618
- }
619
-
620
- if (is_rest) {
621
- for (let i = array_type_annotation.elementTypes.length - 1; i >= 0; i -= 1) {
622
- const element_type = normalize_tuple_element_type(array_type_annotation.elementTypes[i]);
623
- if (element_type.type === 'TSRestType') {
624
- return element_type.typeAnnotation;
625
- }
626
- }
627
- return undefined;
628
- }
629
-
630
- if (index < array_type_annotation.elementTypes.length) {
631
- const element_type = normalize_tuple_element_type(array_type_annotation.elementTypes[index]);
632
- if (element_type.type === 'TSRestType') {
633
- const rest_type_annotation = unwrap_type_annotation(element_type.typeAnnotation);
634
- return rest_type_annotation?.type === 'TSArrayType'
635
- ? rest_type_annotation.elementType
636
- : element_type.typeAnnotation;
637
- }
638
- return element_type;
639
- }
640
-
641
- const last_element = array_type_annotation.elementTypes.at(-1);
642
- if (!last_element) {
643
- return undefined;
644
- }
645
- const normalized_last_element = normalize_tuple_element_type(last_element);
646
- if (normalized_last_element.type === 'TSRestType') {
647
- const rest_type_annotation = unwrap_type_annotation(normalized_last_element.typeAnnotation);
648
- return rest_type_annotation?.type === 'TSArrayType'
649
- ? rest_type_annotation.elementType
650
- : normalized_last_element.typeAnnotation;
651
- }
652
-
653
- return undefined;
654
- }
655
-
656
- /**
657
- * Checks if a parameter source has a Tracked<T> type annotation imported from ripple.
658
- * This is used to determine if lazy array destructuring should use the track tuple fast path.
659
- * @param {AST.TypeNode | undefined} type_annotation - The source type annotation
660
- * @param {AnalysisContext} context - The analysis context
661
- * @returns {boolean}
662
- */
663
- function is_param_tracked_type(type_annotation, context) {
664
- const annotation = unwrap_type_annotation(type_annotation);
665
-
666
- if (
667
- annotation?.type === 'TSTypeReference' &&
668
- annotation.typeName?.type === 'Identifier' &&
669
- annotation.typeName.name === 'Tracked'
670
- ) {
671
- const binding = context.state.scope.get('Tracked');
672
-
673
- return (
674
- binding?.declaration_kind === 'import' &&
675
- binding.initial !== null &&
676
- binding.initial.type === 'ImportDeclaration' &&
677
- binding.initial.source.type === 'Literal' &&
678
- binding.initial.source.value === 'ripple'
679
- );
680
- }
681
-
682
- return false;
683
- }
684
-
685
- /**
686
- * Sets up lazy transforms for any lazy subpatterns nested inside a function or component param.
687
- * @param {AST.Pattern} pattern
688
- * @param {AnalysisContext} context
689
- * @param {AST.TypeNode | undefined} [type_annotation]
690
- */
691
- function setup_nested_lazy_param_transforms(pattern, context, type_annotation = undefined) {
692
- const pattern_type_annotation = get_pattern_type_annotation(pattern) ?? type_annotation;
693
-
694
- switch (pattern.type) {
695
- case 'AssignmentPattern':
696
- setup_nested_lazy_param_transforms(pattern.left, context, pattern_type_annotation);
697
- return;
698
-
699
- case 'RestElement':
700
- setup_nested_lazy_param_transforms(pattern.argument, context, pattern_type_annotation);
701
- return;
702
-
703
- case 'ObjectPattern':
704
- case 'ArrayPattern': {
705
- if (pattern.lazy) {
706
- const param_id = b.id(context.state.scope.generate('lazy'));
707
- const is_tracked_type =
708
- pattern.type === 'ArrayPattern' &&
709
- is_param_tracked_type(pattern_type_annotation, context);
710
-
711
- setup_lazy_transforms(pattern, param_id, context.state, true, is_tracked_type);
712
- pattern.metadata = { ...pattern.metadata, lazy_id: param_id.name };
713
- return;
714
- }
715
-
716
- if (pattern.type === 'ObjectPattern') {
717
- for (const property of pattern.properties) {
718
- const property_type_annotation = get_object_property_type_annotation(
719
- pattern_type_annotation,
720
- property,
721
- );
722
- if (property.type === 'RestElement') {
723
- setup_nested_lazy_param_transforms(
724
- property.argument,
725
- context,
726
- property_type_annotation,
727
- );
728
- } else {
729
- setup_nested_lazy_param_transforms(property.value, context, property_type_annotation);
730
- }
731
- }
732
- } else {
733
- for (let i = 0; i < pattern.elements.length; i += 1) {
734
- const element = pattern.elements[i];
735
- if (element !== null) {
736
- setup_nested_lazy_param_transforms(
737
- element,
738
- context,
739
- get_array_element_type_annotation(
740
- pattern_type_annotation,
741
- i,
742
- element.type === 'RestElement',
743
- ),
744
- );
745
- }
746
- }
747
- }
748
-
749
- return;
750
- }
751
- }
752
- }
753
-
754
- /**
755
- * @param {AST.Function} node
756
- * @param {AnalysisContext} context
757
- */
758
- function visit_function(node, context) {
759
- node.metadata = {
760
- tracked: false,
761
- path: [...context.path],
762
- };
763
-
764
- // Set up lazy transforms for any lazy destructured parameters
765
- for (let i = 0; i < node.params.length; i++) {
766
- const param_node = node.params[i];
767
- const param = param_node.type === 'AssignmentPattern' ? param_node.left : param_node;
768
- const param_type_annotation =
769
- get_pattern_type_annotation(param) ?? param_node.typeAnnotation?.typeAnnotation;
770
-
771
- if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
772
- setup_nested_lazy_param_transforms(param, context, param_type_annotation);
773
- }
774
- }
775
-
776
- context.next({
777
- ...context.state,
778
- function_depth: (context.state.function_depth ?? 0) + 1,
779
- });
780
-
781
- if (node.metadata.tracked) {
782
- mark_as_tracked(context.path);
783
- }
784
- }
785
-
786
- /**
787
- * @param {AnalysisContext['path']} path
788
- */
789
- function mark_as_tracked(path) {
790
- for (let i = path.length - 1; i >= 0; i -= 1) {
791
- const node = path[i];
792
-
793
- if (node.type === 'Component') {
794
- break;
795
- }
796
- if (
797
- node.type === 'FunctionExpression' ||
798
- node.type === 'ArrowFunctionExpression' ||
799
- node.type === 'FunctionDeclaration'
800
- ) {
801
- node.metadata.tracked = true;
802
- break;
803
- }
804
- }
805
- }
806
-
807
- /**
808
- * @param {AST.ReturnStatement} node
809
- * @returns {AST.ReturnStatement}
810
- */
811
- function get_return_keyword_node(node) {
812
- const return_keyword_length = 'return'.length;
813
- return /** @type {AST.ReturnStatement} */ ({
814
- ...node,
815
- end: /** @type {AST.NodeWithLocation} */ (node).start + return_keyword_length,
816
- loc: {
817
- start: /** @type {AST.NodeWithLocation} */ (node).loc.start,
818
- end: {
819
- line: /** @type {AST.NodeWithLocation} */ (node).loc.start.line,
820
- column: /** @type {AST.NodeWithLocation} */ (node).loc.start.column + return_keyword_length,
821
- },
822
- },
823
- });
824
- }
825
-
826
- /**
827
- * @param {AST.ReturnStatement} node
828
- * @param {AnalysisContext} context
829
- * @param {string} message
830
- */
831
- function error_return_keyword(node, context, message) {
832
- const return_keyword_node = get_return_keyword_node(node);
833
-
834
- error(
835
- message,
836
- context.state.analysis.module.filename,
837
- return_keyword_node,
838
- context.state.loose ? context.state.analysis.errors : undefined,
839
- context.state.analysis.comments,
840
- );
841
- }
842
-
843
- /**
844
- * @param {AST.Expression} expression
845
- * @param {Context<AST.Node, AnalysisState>} context
846
- * @returns {boolean}
847
- */
848
- function is_children_template_expression(expression, context) {
849
- const component = context.path.findLast((node) => node.type === 'Component');
850
- const component_scope = component ? context.state.scopes.get(component) : null;
851
- return is_children_template_expression_in_scope(expression, context.state.scope, component_scope);
852
- }
853
-
854
- /** @type {Visitors<AST.Node, AnalysisState>} */
855
- const visitors = {
856
- _(node, { state, next, path }) {
857
- // Set up metadata.path for each node (needed for CSS pruning)
858
- if (!node.metadata) {
859
- node.metadata = { path: [...path] };
860
- } else {
861
- node.metadata.path = [...path];
862
- }
863
-
864
- const scope = state.scopes.get(node);
865
- next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
866
- },
867
-
868
- Program(_, context) {
869
- return context.next({ ...context.state, function_depth: 0 });
870
- },
871
-
872
- ServerBlock(node, context) {
873
- if (context.path.at(-1)?.type !== 'Program') {
874
- // fatal since we don't have a transformation defined for this case
875
- error(
876
- '`#server` block can only be declared at the module level.',
877
- context.state.analysis.module.filename,
878
- node,
879
- );
880
- }
881
- node.metadata = {
882
- ...node.metadata,
883
- exports: new Set(),
884
- };
885
- context.visit(node.body, {
886
- ...context.state,
887
- ancestor_server_block: node,
888
- });
889
- },
890
-
891
- Identifier(node, context) {
892
- const binding = context.state.scope.get(node.name);
893
- const parent = context.path.at(-1);
894
-
895
- if (
896
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
897
- binding &&
898
- context.state.ancestor_server_block &&
899
- binding.node !== node // Don't check the declaration itself
900
- ) {
901
- /** @type {ScopeInterface | null} */
902
- let current_scope = binding.scope;
903
- let found_server_block = false;
904
-
905
- while (current_scope !== null) {
906
- if (current_scope.server_block) {
907
- found_server_block = true;
908
- break;
909
- }
910
- current_scope = current_scope.parent;
911
- }
912
-
913
- if (!found_server_block) {
914
- error(
915
- `Cannot reference client-side "${node.name}" from a server block. Server blocks can only access variables and imports declared inside them.`,
916
- context.state.analysis.module.filename,
917
- node,
918
- context.state.loose ? context.state.analysis.errors : undefined,
919
- context.state.analysis.comments,
920
- );
921
- }
922
- }
923
-
924
- if (node.tracked && binding) {
925
- if (
926
- binding.kind === 'prop' ||
927
- binding.kind === 'prop_fallback' ||
928
- binding.kind === 'lazy' ||
929
- binding.kind === 'lazy_fallback' ||
930
- binding.kind === 'for_pattern' ||
931
- (is_reference(node, /** @type {AST.Node} */ (parent)) &&
932
- node.tracked &&
933
- binding.node !== node)
934
- ) {
935
- mark_as_tracked(context.path);
936
- if (context.state.metadata?.tracking === false) {
937
- context.state.metadata.tracking = true;
938
- }
939
- }
940
- }
941
-
942
- // Lazy bindings from track() calls (read_unwraps) are inherently reactive —
943
- // propagate tracking so that control flow (if/for/switch)
944
- // and early returns create reactive blocks
945
- if (
946
- !node.tracked &&
947
- binding?.read_unwraps &&
948
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
949
- binding.node !== node
950
- ) {
951
- mark_as_tracked(context.path);
952
- if (context.state.metadata?.tracking === false) {
953
- context.state.metadata.tracking = true;
954
- }
955
- }
956
-
957
- context.next();
958
- },
959
-
960
- MemberExpression(node, context) {
961
- const parent = context.path.at(-1);
962
-
963
- // Track #style.className or #style['className'] references
964
- if (node.object.type === 'StyleIdentifier') {
965
- const component = is_inside_component(context, true);
966
-
967
- if (!component) {
968
- error(
969
- '`#style` can only be used within a component',
970
- context.state.analysis.module.filename,
971
- node,
972
- context.state.loose ? context.state.analysis.errors : undefined,
973
- context.state.analysis.comments,
974
- );
975
- } else {
976
- component.metadata.styleIdentifierPresent = true;
977
- }
978
-
979
- /** @type {string | null} */
980
- let className = null;
981
-
982
- if (!node.computed && node.property.type === 'Identifier') {
983
- // #style.test
984
- className = node.property.name;
985
- } else if (
986
- node.computed &&
987
- node.property.type === 'Literal' &&
988
- typeof node.property.value === 'string'
989
- ) {
990
- // #style['test']
991
- className = node.property.value;
992
- } else {
993
- // #style[expression] - dynamic, not allowed
994
- error(
995
- '`#style` property access must use a dot property or static string for css class name, not a dynamic expression',
996
- context.state.analysis.module.filename,
997
- node.property,
998
- context.state.loose ? context.state.analysis.errors : undefined,
999
- context.state.analysis.comments,
1000
- );
1001
- }
1002
-
1003
- if (className !== null) {
1004
- context.state.metadata.styleClasses?.set(className, node.property);
1005
- }
1006
-
1007
- return context.next();
1008
- } else if (node.object.type === 'ServerIdentifier') {
1009
- context.state.analysis.metadata.serverIdentifierPresent = true;
1010
- }
1011
-
1012
- if (node.object.type === 'Identifier' && !node.object.tracked) {
1013
- const binding = context.state.scope.get(node.object.name);
1014
-
1015
- if (binding && binding.metadata?.is_ripple_object) {
1016
- const internalProperties = new Set(['__v', 'a', 'b', 'c', 'f']);
1017
-
1018
- let propertyName = null;
1019
- if (node.property.type === 'Identifier' && !node.computed) {
1020
- propertyName = node.property.name;
1021
- } else if (node.property.type === 'Literal' && typeof node.property.value === 'string') {
1022
- propertyName = node.property.value;
1023
- }
1024
-
1025
- if (propertyName && internalProperties.has(propertyName)) {
1026
- error(
1027
- `Directly accessing internal property "${propertyName}" of a tracked object is not allowed. Use \`${node.object.name}.value\` or \`&[]\` lazy destructuring instead.`,
1028
- context.state.analysis.module.filename,
1029
- node.property,
1030
- context.state.loose ? context.state.analysis.errors : undefined,
1031
- context.state.analysis.comments,
1032
- );
1033
- }
1034
- }
1035
-
1036
- if (
1037
- binding !== null &&
1038
- binding.kind !== 'lazy' &&
1039
- binding.kind !== 'lazy_fallback' &&
1040
- binding.initial?.type === 'CallExpression' &&
1041
- is_ripple_track_call(binding.initial.callee, context)
1042
- ) {
1043
- const is_allowed_tracked_access =
1044
- // Allow [0] and [1] indexed access on tracked objects.
1045
- (node.computed &&
1046
- node.property.type === 'Literal' &&
1047
- (node.property.value === 0 || node.property.value === 1)) ||
1048
- // Allow .value and .length property access on tracked objects.
1049
- (!node.computed &&
1050
- node.property.type === 'Identifier' &&
1051
- (node.property.name === 'value' || node.property.name === 'length'));
1052
-
1053
- if (is_allowed_tracked_access) {
1054
- // pass through
1055
- } else {
1056
- error(
1057
- `Accessing a tracked object directly is not allowed, use \`.value\` or \`&[]\` lazy destructuring to read the value inside a tracked object - for example \`${node.object.name}.value\``,
1058
- context.state.analysis.module.filename,
1059
- node.object,
1060
- context.state.loose ? context.state.analysis.errors : undefined,
1061
- context.state.analysis.comments,
1062
- );
1063
- }
1064
- }
1065
- }
1066
-
1067
- context.next();
1068
- },
1069
-
1070
- CallExpression(node, context) {
1071
- // bug in our acorn [parser]: it uses typeParameters instead of typeArguments
1072
- // @ts-expect-error
1073
- if (node.typeParameters) {
1074
- // @ts-expect-error
1075
- node.typeArguments = node.typeParameters;
1076
- // @ts-expect-error
1077
- delete node.typeParameters;
1078
- }
1079
-
1080
- const callee = node.callee;
1081
-
1082
- if (
1083
- !context.path.some(
1084
- (path_node) => path_node.type === 'TsxCompat' || path_node.type === 'Tsx',
1085
- ) &&
1086
- is_children_template_expression(/** @type {AST.Expression} */ (callee), context)
1087
- ) {
1088
- error(
1089
- '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
1090
- context.state.analysis.module.filename,
1091
- callee,
1092
- context.state.loose ? context.state.analysis.errors : undefined,
1093
- context.state.analysis.comments,
1094
- );
1095
- }
1096
-
1097
- if (context.state.function_depth === 0 && is_ripple_track_call(callee, context)) {
1098
- error(
1099
- '`track` can only be used within a reactive context, such as a component, function or class that is used or created from a component',
1100
- context.state.analysis.module.filename,
1101
- node.callee,
1102
- context.state.loose ? context.state.analysis.errors : undefined,
1103
- context.state.analysis.comments,
1104
- );
1105
- }
1106
-
1107
- if (!is_inside_component(context, true)) {
1108
- mark_as_tracked(context.path);
1109
- }
1110
-
1111
- context.next();
1112
- },
1113
-
1114
- NewExpression(node, context) {
1115
- context.next();
1116
- },
1117
-
1118
- VariableDeclaration(node, context) {
1119
- const { state, visit } = context;
1120
-
1121
- for (const declarator of node.declarations) {
1122
- if (is_inside_component(context) && node.kind === 'var') {
1123
- error(
1124
- '`var` declarations are not allowed in components, use let or const instead',
1125
- state.analysis.module.filename,
1126
- declarator.id,
1127
- context.state.loose ? context.state.analysis.errors : undefined,
1128
- context.state.analysis.comments,
1129
- );
1130
- }
1131
- const metadata = { tracking: false };
1132
-
1133
- if (declarator.id.type === 'Identifier') {
1134
- const binding = state.scope.get(declarator.id.name);
1135
- if (binding && declarator.init && declarator.init.type === 'CallExpression') {
1136
- const callee = declarator.init.callee;
1137
- // Check if it's a call to `track` or `tracked`
1138
- if (
1139
- (callee.type === 'Identifier' &&
1140
- (callee.name === 'track' ||
1141
- callee.name === 'trackAsync' ||
1142
- callee.name === 'tracked')) ||
1143
- (callee.type === 'MemberExpression' &&
1144
- callee.property.type === 'Identifier' &&
1145
- (callee.property.name === 'track' ||
1146
- callee.property.name === 'trackAsync' ||
1147
- callee.property.name === 'tracked'))
1148
- ) {
1149
- binding.metadata = { ...binding.metadata, is_ripple_object: true };
1150
- }
1151
- }
1152
- visit(declarator, state);
1153
- } else {
1154
- // Handle lazy destructuring patterns
1155
- if (
1156
- (declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') &&
1157
- declarator.id.lazy
1158
- ) {
1159
- const lazy_id = b.id(state.scope.generate('lazy'));
1160
- const writable = node.kind !== 'const';
1161
- const call_name =
1162
- declarator.init?.type === 'CallExpression' &&
1163
- is_ripple_track_call(declarator.init.callee, context);
1164
- const init_is_track = call_name === 'track' || call_name === 'trackAsync';
1165
- setup_lazy_transforms(declarator.id, lazy_id, state, writable, !!init_is_track);
1166
- // Store the generated identifier name on the pattern for the transform phase
1167
- declarator.id.metadata = { ...declarator.id.metadata, lazy_id: lazy_id.name };
1168
- }
1169
-
1170
- visit(declarator, state);
1171
- }
1172
-
1173
- declarator.metadata = { ...metadata, path: [...context.path] };
1174
- }
1175
- },
1176
-
1177
- ExpressionStatement(node, context) {
1178
- const { state, visit } = context;
1179
-
1180
- // Handle standalone lazy destructuring assignment: &[data] = track(0);
1181
- if (
1182
- node.expression.type === 'AssignmentExpression' &&
1183
- node.expression.operator === '=' &&
1184
- (node.expression.left.type === 'ObjectPattern' ||
1185
- node.expression.left.type === 'ArrayPattern') &&
1186
- node.expression.left.lazy
1187
- ) {
1188
- const pattern = /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (node.expression.left);
1189
- const lazy_id = b.id(state.scope.generate('lazy'));
1190
- const init = /** @type {AST.Expression} */ (node.expression.right);
1191
- const init_is_track =
1192
- init?.type === 'CallExpression' && is_ripple_track_call(init.callee, context) === 'track';
1193
- setup_lazy_transforms(pattern, lazy_id, state, true, !!init_is_track);
1194
- // Store the generated identifier name on the pattern for the transform phase
1195
- pattern.metadata = { ...pattern.metadata, lazy_id: lazy_id.name };
1196
- }
1197
-
1198
- context.next();
1199
- },
1200
-
1201
- StyleIdentifier(node, context) {
1202
- const component = is_inside_component(context, true);
1203
- const parent = context.path.at(-1);
1204
-
1205
- if (component) {
1206
- component.metadata.styleIdentifierPresent = true;
1207
- }
1208
-
1209
- // #style must only be used for property access (e.g., #style.className)
1210
- if (!parent || parent.type !== 'MemberExpression' || parent.object !== node) {
1211
- error(
1212
- '`#style` can only be used for property access, e.g., `#style.className`.',
1213
- context.state.analysis.module.filename,
1214
- node,
1215
- context.state.loose ? context.state.analysis.errors : undefined,
1216
- context.state.analysis.comments,
1217
- );
1218
- }
1219
- context.next();
1220
- },
1221
-
1222
- ServerIdentifier(node, context) {
1223
- const parent = context.path.at(-1);
1224
-
1225
- // #server must only be used for member access (e.g., #server.functionName(...))
1226
- if (!parent || parent.type !== 'MemberExpression' || parent.object !== node) {
1227
- error(
1228
- '`#server` can only be used for member access, e.g., `#server.functionName(...)`.',
1229
- context.state.analysis.module.filename,
1230
- node,
1231
- context.state.loose ? context.state.analysis.errors : undefined,
1232
- context.state.analysis.comments,
1233
- );
1234
- }
1235
- context.next();
1236
- },
1237
-
1238
- ArrowFunctionExpression(node, context) {
1239
- visit_function(node, context);
1240
- },
1241
- FunctionExpression(node, context) {
1242
- visit_function(node, context);
1243
- },
1244
- FunctionDeclaration(node, context) {
1245
- visit_function(node, context);
1246
- },
1247
-
1248
- Component(node, context) {
1249
- context.state.component = node;
1250
-
1251
- if (node.params.length > 0) {
1252
- const props = node.params[0];
1253
-
1254
- if (props.type === 'ObjectPattern' || props.type === 'ArrayPattern') {
1255
- // Lazy destructuring: &{...} or &[...] — set up lazy transforms
1256
- if (props.lazy) {
1257
- setup_lazy_transforms(props, b.id('__props'), context.state, true, false);
1258
- } else {
1259
- setup_nested_lazy_param_transforms(props, context, get_pattern_type_annotation(props));
1260
- }
1261
- } else if (props.type === 'AssignmentPattern') {
1262
- error(
1263
- 'Props are always an object, use destructured props with default values instead',
1264
- context.state.analysis.module.filename,
1265
- props,
1266
- context.state.loose ? context.state.analysis.errors : undefined,
1267
- context.state.analysis.comments,
1268
- );
1269
- }
1270
- }
1271
- /** @type {AST.Element[]} */
1272
- const elements = [];
1273
-
1274
- // Track metadata for this component
1275
- const metadata = {
1276
- styleClasses: /** @type {StyleClasses} */ (new Map()),
1277
- };
1278
-
1279
- /** @type {TopScopedClasses} */
1280
- const topScopedClasses = new Map();
1281
-
1282
- context.next({
1283
- ...context.state,
1284
- elements,
1285
- function_depth: (context.state.function_depth ?? 0) + 1,
1286
- metadata,
1287
- });
1288
-
1289
- const css = node.css;
1290
-
1291
- if (css !== null) {
1292
- // Analyze CSS to set global selector metadata
1293
- analyze_css(css);
1294
-
1295
- for (const node of elements) {
1296
- prune_css(css, node, metadata.styleClasses, topScopedClasses);
1297
- }
1298
-
1299
- if (topScopedClasses.size > 0) {
1300
- node.metadata.topScopedClasses = topScopedClasses;
1301
- }
1302
- }
1303
-
1304
- if (metadata.styleClasses.size > 0) {
1305
- node.metadata.styleClasses = metadata.styleClasses;
1306
-
1307
- for (const [className, property] of metadata.styleClasses) {
1308
- if (!topScopedClasses?.has(className)) {
1309
- error(
1310
- `CSS class ".${className}" does not exist as a stand-alone class in ${node.id?.name ? node.id.name : "this component's"} <style> block`,
1311
- context.state.analysis.module.filename,
1312
- property,
1313
- context.state.loose ? context.state.analysis.errors : undefined,
1314
- context.state.analysis.comments,
1315
- );
1316
- }
1317
- }
1318
- }
1319
-
1320
- // Store component metadata in analysis
1321
- // Only add metadata if component has a name (not anonymous)
1322
- if (node.id) {
1323
- context.state.analysis.component_metadata.push({
1324
- id: node.id.name,
1325
- });
1326
- }
1327
- },
1328
-
1329
- ForStatement(node, context) {
1330
- if (is_inside_component(context)) {
1331
- // TODO: it's a fatal error for now but
1332
- // we could implement the for loop for the ts mode only
1333
- error(
1334
- 'For loops are not supported in components. Use for...of instead.',
1335
- context.state.analysis.module.filename,
1336
- node,
1337
- );
1338
- }
1339
-
1340
- context.next();
1341
- },
1342
-
1343
- SwitchStatement(node, context) {
1344
- if (!is_inside_component(context)) {
1345
- return context.next();
1346
- }
1347
-
1348
- context.visit(node.discriminant, context.state);
1349
-
1350
- for (const switch_case of node.cases) {
1351
- // Skip empty cases
1352
- if (switch_case.consequent.length === 0) {
1353
- continue;
1354
- }
1355
-
1356
- node.metadata = {
1357
- ...node.metadata,
1358
- has_template: false,
1359
- };
1360
-
1361
- context.visit(switch_case, context.state);
1362
-
1363
- if (!node.metadata.has_template) {
1364
- error(
1365
- 'Component switch statements must contain a template in each of their cases. Move the switch statement into an effect if it does not render anything.',
1366
- context.state.analysis.module.filename,
1367
- switch_case,
1368
- context.state.loose ? context.state.analysis.errors : undefined,
1369
- context.state.analysis.comments,
1370
- );
1371
- }
1372
- }
1373
- },
1374
-
1375
- ForOfStatement(node, context) {
1376
- if (!is_inside_component(context)) {
1377
- return context.next();
1378
- }
1379
-
1380
- if (node.index) {
1381
- const state = context.state;
1382
- const scope = /** @type {ScopeInterface} */ (state.scopes.get(node));
1383
- const binding = scope.get(/** @type {AST.Identifier} */ (node.index).name);
1384
-
1385
- if (binding !== null) {
1386
- binding.kind = 'index';
1387
- binding.transform = {
1388
- read: (node) => {
1389
- return b.call('_$_.get', node);
1390
- },
1391
- };
1392
- }
1393
- }
1394
-
1395
- if (node.key) {
1396
- const state = context.state;
1397
- const pattern = /** @type {AST.VariableDeclaration} */ (node.left).declarations[0].id;
1398
- const paths = extract_paths(pattern);
1399
- const scope = /** @type {ScopeInterface} */ (state.scopes.get(node));
1400
- /** @type {AST.Identifier | AST.Pattern} */
1401
- let pattern_id;
1402
- if (state.to_ts || state.mode === 'server') {
1403
- pattern_id = pattern;
1404
- } else {
1405
- pattern_id = b.id(scope.generate('pattern'));
1406
- /** @type {AST.VariableDeclaration} */ (node.left).declarations[0].id = pattern_id;
1407
- }
1408
-
1409
- for (const path of paths) {
1410
- const name = /** @type {AST.Identifier} */ (path.node).name;
1411
- const binding = context.state.scope.get(name);
1412
-
1413
- if (binding !== null) {
1414
- binding.kind = 'for_pattern';
1415
- if (!binding.metadata) {
1416
- binding.metadata = {
1417
- pattern: /** @type {AST.Identifier} */ (pattern_id),
1418
- };
1419
- }
1420
-
1421
- binding.transform = {
1422
- read: () => {
1423
- return path.expression(b.call('_$_.get', /** @type {AST.Identifier} */ (pattern_id)));
1424
- },
1425
- };
1426
- }
1427
- }
1428
- }
1429
-
1430
- node.metadata = {
1431
- ...node.metadata,
1432
- has_template: false,
1433
- };
1434
- context.next();
1435
-
1436
- if (!node.metadata.has_template) {
1437
- error(
1438
- 'Component for...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
1439
- context.state.analysis.module.filename,
1440
- node.body,
1441
- context.state.loose ? context.state.analysis.errors : undefined,
1442
- context.state.analysis.comments,
1443
- );
1444
- }
1445
- },
1446
-
1447
- ExportNamedDeclaration(node, context) {
1448
- const server_block = context.state.ancestor_server_block;
1449
-
1450
- if (!server_block) {
1451
- return context.next();
1452
- }
1453
-
1454
- const exports = server_block.metadata.exports;
1455
- const declaration = /** @type {AST.RippleExportNamedDeclaration} */ (node).declaration;
1456
-
1457
- if (declaration && declaration.type === 'FunctionDeclaration') {
1458
- exports.add(declaration.id.name);
1459
- } else if (declaration && declaration.type === 'Component') {
1460
- error(
1461
- 'Not implemented: Exported component declaration not supported in server blocks.',
1462
- context.state.analysis.module.filename,
1463
- /** @type {AST.Identifier} */ (declaration.id),
1464
- context.state.loose ? context.state.analysis.errors : undefined,
1465
- context.state.analysis.comments,
1466
- );
1467
- // TODO: the client and server rendering doesn't currently support components
1468
- // If we're going to support this, we need to account also for anonymous object declaration
1469
- // and specifiers
1470
- // exports.add(/** @type {AST.Identifier} */ (declaration.id).name);
1471
- } else if (declaration && declaration.type === 'VariableDeclaration') {
1472
- for (const decl of declaration.declarations) {
1473
- if (decl.init !== undefined && decl.init !== null) {
1474
- if (decl.id.type === 'Identifier') {
1475
- if (
1476
- decl.init.type === 'FunctionExpression' ||
1477
- decl.init.type === 'ArrowFunctionExpression'
1478
- ) {
1479
- exports.add(decl.id.name);
1480
- continue;
1481
- } else if (decl.init.type === 'Identifier') {
1482
- const name = decl.init.name;
1483
- const binding = context.state.scope.get(name);
1484
- if (binding && is_binding_function(binding, context.state.scope)) {
1485
- exports.add(decl.id.name);
1486
- continue;
1487
- }
1488
- } else if (decl.init.type === 'MemberExpression') {
1489
- error(
1490
- 'Not implemented: Exported member expressions are not supported in server blocks.',
1491
- context.state.analysis.module.filename,
1492
- decl.init,
1493
- context.state.loose ? context.state.analysis.errors : undefined,
1494
- context.state.analysis.comments,
1495
- );
1496
- continue;
1497
- }
1498
- } else if (decl.id.type === 'ObjectPattern' || decl.id.type === 'ArrayPattern') {
1499
- const paths = extract_paths(decl.id);
1500
- for (const path of paths) {
1501
- error(
1502
- 'Not implemented: Exported object or array patterns are not supported in server blocks.',
1503
- context.state.analysis.module.filename,
1504
- path.node,
1505
- context.state.loose ? context.state.analysis.errors : undefined,
1506
- context.state.analysis.comments,
1507
- );
1508
- }
1509
- }
1510
- }
1511
- // TODO: allow exporting consts when hydration is supported
1512
- error(
1513
- `Not implemented: Exported '${decl.id.type}' type is not supported in server blocks.`,
1514
- context.state.analysis.module.filename,
1515
- decl,
1516
- context.state.loose ? context.state.analysis.errors : undefined,
1517
- context.state.analysis.comments,
1518
- );
1519
- }
1520
- } else if (node.specifiers) {
1521
- for (const specifier of node.specifiers) {
1522
- const name = /** @type {AST.Identifier} */ (specifier.local).name;
1523
- const binding = context.state.scope.get(name);
1524
- const is_function = binding && is_binding_function(binding, context.state.scope);
1525
-
1526
- if (is_function) {
1527
- exports.add(name);
1528
- continue;
1529
- }
1530
-
1531
- error(
1532
- `Not implemented: Exported specifier type not supported in server blocks.`,
1533
- context.state.analysis.module.filename,
1534
- specifier,
1535
- context.state.loose ? context.state.analysis.errors : undefined,
1536
- context.state.analysis.comments,
1537
- );
1538
- }
1539
- } else {
1540
- error(
1541
- 'Not implemented: Exported declaration type not supported in server blocks.',
1542
- context.state.analysis.module.filename,
1543
- node,
1544
- context.state.loose ? context.state.analysis.errors : undefined,
1545
- context.state.analysis.comments,
1546
- );
1547
- }
1548
-
1549
- return context.next();
1550
- },
1551
-
1552
- TSTypeReference(node, context) {
1553
- // bug in our acorn parser: it uses typeParameters instead of typeArguments
1554
- // @ts-expect-error
1555
- if (node.typeParameters) {
1556
- // @ts-expect-error
1557
- node.typeArguments = node.typeParameters;
1558
- // @ts-expect-error
1559
- delete node.typeParameters;
1560
- }
1561
- context.next();
1562
- },
1563
-
1564
- IfStatement(node, context) {
1565
- if (!is_inside_component(context)) {
1566
- return context.next();
1567
- }
1568
-
1569
- node.metadata = {
1570
- ...node.metadata,
1571
- has_template: false,
1572
- has_throw: false,
1573
- };
1574
-
1575
- const test_metadata = { tracking: false };
1576
- context.visit(node.test, { ...context.state, metadata: test_metadata });
1577
- if (test_metadata.tracking) {
1578
- /** @type {AST.TrackedNode} */ (node.test).tracked = true;
1579
- }
1580
-
1581
- context.visit(node.consequent, context.state);
1582
-
1583
- const consequent_body =
1584
- node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
1585
-
1586
- if (
1587
- consequent_body.length === 1 &&
1588
- consequent_body[0].type === 'ReturnStatement' &&
1589
- !node.alternate
1590
- ) {
1591
- node.metadata.lone_return = true;
1592
- }
1593
-
1594
- if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1595
- error(
1596
- 'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
1597
- context.state.analysis.module.filename,
1598
- node.consequent,
1599
- context.state.loose ? context.state.analysis.errors : undefined,
1600
- context.state.analysis.comments,
1601
- );
1602
- }
1603
-
1604
- if (node.alternate) {
1605
- const saved_has_return = node.metadata.has_return;
1606
- const saved_returns = node.metadata.returns;
1607
- node.metadata.has_template = false;
1608
- node.metadata.has_throw = false;
1609
- context.visit(node.alternate, context.state);
1610
-
1611
- if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1612
- error(
1613
- 'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
1614
- context.state.analysis.module.filename,
1615
- node.alternate,
1616
- context.state.loose ? context.state.analysis.errors : undefined,
1617
- context.state.analysis.comments,
1618
- );
1619
- }
1620
-
1621
- if (saved_has_return) {
1622
- node.metadata.has_return = true;
1623
- if (saved_returns) {
1624
- node.metadata.returns = [...saved_returns, ...(node.metadata.returns || [])];
1625
- }
1626
- }
1627
- }
1628
- },
1629
-
1630
- ReturnStatement(node, context) {
1631
- const parent = context.path.at(-1);
1632
-
1633
- if (!is_inside_component(context)) {
1634
- if (parent?.type === 'Program') {
1635
- error_return_keyword(
1636
- node,
1637
- context,
1638
- 'Return statements are not allowed at the top level of a module.',
1639
- );
1640
- }
1641
-
1642
- return context.next();
1643
- }
1644
-
1645
- if (node.argument !== null) {
1646
- error_return_keyword(
1647
- node,
1648
- context,
1649
- 'Return statements inside components cannot have a return value.',
1650
- );
1651
- }
1652
-
1653
- for (let i = context.path.length - 1; i >= 0; i--) {
1654
- const ancestor = context.path[i];
1655
-
1656
- if (
1657
- ancestor.type === 'Component' ||
1658
- ancestor.type === 'FunctionExpression' ||
1659
- ancestor.type === 'ArrowFunctionExpression' ||
1660
- ancestor.type === 'FunctionDeclaration'
1661
- ) {
1662
- break;
1663
- }
1664
-
1665
- if (
1666
- ancestor.type === 'IfStatement' &&
1667
- /** @type {AST.TrackedNode} */ (ancestor.test).tracked
1668
- ) {
1669
- node.metadata.is_reactive = true;
1670
- }
1671
-
1672
- if (!ancestor.metadata.returns) {
1673
- ancestor.metadata.returns = [];
1674
- }
1675
- ancestor.metadata.returns.push(node);
1676
- ancestor.metadata.has_return = true;
1677
- }
1678
- },
1679
-
1680
- ThrowStatement(node, context) {
1681
- if (!is_inside_component(context)) {
1682
- return context.next();
1683
- }
1684
-
1685
- for (let i = context.path.length - 1; i >= 0; i--) {
1686
- const ancestor = context.path[i];
1687
-
1688
- if (
1689
- ancestor.type === 'Component' ||
1690
- ancestor.type === 'FunctionExpression' ||
1691
- ancestor.type === 'ArrowFunctionExpression' ||
1692
- ancestor.type === 'FunctionDeclaration'
1693
- ) {
1694
- break;
1695
- }
1696
-
1697
- if (ancestor.type === 'IfStatement') {
1698
- if (!ancestor.metadata.has_throw) {
1699
- ancestor.metadata.has_throw = true;
1700
- }
1701
- }
1702
- }
1703
-
1704
- context.next();
1705
- },
1706
-
1707
- TryStatement(node, context) {
1708
- const { state } = context;
1709
- if (!is_inside_component(context)) {
1710
- return context.next();
1711
- }
1712
-
1713
- if (node.pending) {
1714
- node.metadata = {
1715
- ...node.metadata,
1716
- has_template: false,
1717
- };
1718
-
1719
- context.visit(node.block, state);
1720
-
1721
- if (!node.metadata.has_template) {
1722
- error(
1723
- 'Component try statements must contain a template in their main body. Move the try statement into an effect if it does not render anything.',
1724
- state.analysis.module.filename,
1725
- node.block,
1726
- context.state.loose ? context.state.analysis.errors : undefined,
1727
- context.state.analysis.comments,
1728
- );
1729
- }
1730
-
1731
- node.metadata = {
1732
- ...node.metadata,
1733
- has_template: false,
1734
- };
1735
-
1736
- context.visit(node.pending, state);
1737
-
1738
- if (!node.metadata.has_template) {
1739
- error(
1740
- 'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
1741
- state.analysis.module.filename,
1742
- node.pending,
1743
- context.state.loose ? context.state.analysis.errors : undefined,
1744
- context.state.analysis.comments,
1745
- );
1746
- }
1747
- } else {
1748
- context.visit(node.block, state);
1749
- }
1750
-
1751
- if (node.handler) {
1752
- context.visit(node.handler, state);
1753
- }
1754
-
1755
- if (node.finalizer) {
1756
- context.visit(node.finalizer, state);
1757
- }
1758
- },
1759
-
1760
- ForInStatement(node, context) {
1761
- if (is_inside_component(context)) {
1762
- // TODO: it's a fatal error for now but
1763
- // we could implement the for in loop for the ts mode only to make it a usage error
1764
- error(
1765
- 'For...in loops are not supported in components. Use for...of instead.',
1766
- context.state.analysis.module.filename,
1767
- node,
1768
- );
1769
- }
1770
-
1771
- context.next();
1772
- },
1773
-
1774
- WhileStatement(node, context) {
1775
- if (is_inside_component(context)) {
1776
- error(
1777
- 'While loops are not supported in components. Move the while loop into a function.',
1778
- context.state.analysis.module.filename,
1779
- node,
1780
- );
1781
- }
1782
-
1783
- context.next();
1784
- },
1785
-
1786
- DoWhileStatement(node, context) {
1787
- if (is_inside_component(context)) {
1788
- error(
1789
- 'Do...while loops are not supported in components. Move the do...while loop into a function.',
1790
- context.state.analysis.module.filename,
1791
- node,
1792
- );
1793
- }
1794
-
1795
- context.next();
1796
- },
1797
-
1798
- JSXElement(node, context) {
1799
- const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1800
-
1801
- if (inside_tsx_compat) {
1802
- return context.next();
1803
- }
1804
- // TODO: could compile it as something to avoid a fatal error
1805
- error(
1806
- 'Elements cannot be used as generic expressions, only as statements within a component',
1807
- context.state.analysis.module.filename,
1808
- node,
1809
- );
1810
- },
1811
-
1812
- Tsx(_, context) {
1813
- mark_control_flow_has_template(context.path);
1814
- return context.next();
1815
- },
1816
-
1817
- TsxCompat(node, context) {
1818
- mark_control_flow_has_template(context.path);
1819
-
1820
- const configured_compat_kinds = context.state.configured_compat_kinds;
1821
- if (configured_compat_kinds !== undefined && !configured_compat_kinds.has(node.kind)) {
1822
- error(
1823
- `<tsx:${node.kind}> requires "${node.kind}" compat to be configured in ripple.config.ts.`,
1824
- context.state.analysis.module.filename,
1825
- node,
1826
- context.state.loose ? context.state.analysis.errors : undefined,
1827
- context.state.analysis.comments,
1828
- );
1829
- }
1830
-
1831
- return context.next();
1832
- },
1833
-
1834
- Element(node, context) {
1835
- if (!is_inside_component(context)) {
1836
- error(
1837
- 'Elements cannot be used outside of components',
1838
- context.state.analysis.module.filename,
1839
- node,
1840
- );
1841
- }
1842
-
1843
- const { state, visit, path } = context;
1844
- const is_dom_element = is_element_dom_element(node);
1845
- /** @type {Set<AST.Identifier>} */
1846
- const attribute_names = new Set();
1847
-
1848
- mark_control_flow_has_template(path);
1849
-
1850
- if (
1851
- !is_dom_element &&
1852
- is_children_template_expression(/** @type {AST.Expression} */ (node.id), context)
1853
- ) {
1854
- error(
1855
- '`children` cannot be rendered as a component. Render it with `{children}` or `{props.children}` instead.',
1856
- state.analysis.module.filename,
1857
- node.id,
1858
- context.state.loose ? context.state.analysis.errors : undefined,
1859
- context.state.analysis.comments,
1860
- );
1861
- }
1862
-
1863
- validate_nesting(node, context);
1864
-
1865
- // Store capitalized name for dynamic components/elements
1866
- // TODO: this is not quite right as the node.id could be a member expression
1867
- // so, we'd need to identify dynamic based on that too
1868
- // However, we're going to get rid of capitalization in favor of jsx()
1869
- // so, this will be need to be redone.
1870
- if (node.id.type === 'Identifier' && node.id.tracked) {
1871
- const source_name = node.id.name;
1872
- const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
1873
- node.metadata.ts_name = capitalized_name;
1874
- node.metadata.source_name = source_name;
1875
-
1876
- // Mark the binding as a dynamic component so we can capitalize it everywhere
1877
- const binding = context.state.scope.get(source_name);
1878
- if (binding) {
1879
- if (!binding.metadata) {
1880
- binding.metadata = {};
1881
- }
1882
- binding.metadata.is_dynamic_component = true;
1883
- }
1884
-
1885
- if (!is_dom_element && state.elements) {
1886
- state.elements.push(node);
1887
- // Mark dynamic elements as scoped by default since we can't match CSS at compile time
1888
- if (state.component?.css) {
1889
- node.metadata.scoped = true;
1890
- }
1891
- }
1892
- }
1893
-
1894
- if (is_dom_element) {
1895
- if (/** @type {AST.Identifier} */ (node.id).name === 'head') {
1896
- // head validation
1897
- if (node.attributes.length > 0) {
1898
- // TODO: could transform attributes as something, e.g. Text Node, and avoid a fatal error
1899
- error('<head> cannot have any attributes', state.analysis.module.filename, node);
1900
- }
1901
- if (node.children.length === 0) {
1902
- // TODO: could transform children as something, e.g. Text Node, and avoid a fatal error
1903
- error('<head> must have children', state.analysis.module.filename, node);
1904
- }
1905
-
1906
- for (const child of node.children) {
1907
- context.visit(child, { ...state, inside_head: true });
1908
- }
1909
-
1910
- return;
1911
- }
1912
- if (state.inside_head) {
1913
- if (/** @type {AST.Identifier} */ (node.id).name === 'title') {
1914
- const children = normalize_children(node.children, context);
1915
-
1916
- if (
1917
- children.length !== 1 ||
1918
- (children[0].type !== 'RippleExpression' && children[0].type !== 'Text')
1919
- ) {
1920
- // TODO: could transform children as something, e.g. Text Node, and avoid a fatal error
1921
- error(
1922
- '<title> must have only contain text nodes',
1923
- state.analysis.module.filename,
1924
- node,
1925
- );
1926
- }
1927
- }
1928
-
1929
- // check for invalid elements in head
1930
- if (!valid_in_head.has(/** @type {AST.Identifier} */ (node.id).name)) {
1931
- // TODO: could transform invalid elements as something, e.g. Text Node, and avoid a fatal error
1932
- error(
1933
- `<${/** @type {AST.Identifier} */ (node.id).name}> cannot be used in <head>`,
1934
- state.analysis.module.filename,
1935
- node,
1936
- );
1937
- }
1938
- } else {
1939
- if (/** @type {AST.Identifier} */ (node.id).name === 'script') {
1940
- const err_msg = '<script> cannot be used outside of <head>.';
1941
- error(
1942
- err_msg,
1943
- state.analysis.module.filename,
1944
- node.openingElement,
1945
- state.loose ? state.analysis.errors : undefined,
1946
- );
1947
-
1948
- if (node.closingElement) {
1949
- error(
1950
- err_msg,
1951
- state.analysis.module.filename,
1952
- node.closingElement,
1953
- state.loose ? state.analysis.errors : undefined,
1954
- );
1955
- }
1956
- }
1957
- }
1958
-
1959
- const is_void = is_void_element(/** @type {AST.Identifier} */ (node.id).name);
1960
-
1961
- if (state.elements) {
1962
- state.elements.push(node);
1963
- }
1964
-
1965
- for (const attr of node.attributes) {
1966
- if (attr.type === 'Attribute') {
1967
- if (attr.value && attr.value.type === 'JSXEmptyExpression') {
1968
- const value = /** @type {ESTreeJSX.JSXEmptyExpression & AST.NodeWithLocation} */ (
1969
- attr.value
1970
- );
1971
- error(
1972
- 'attributes must only be assigned a non-empty expression',
1973
- state.analysis.module.filename,
1974
- {
1975
- ...value,
1976
- start: value.start - 1,
1977
- end: value.end + 1,
1978
- loc: {
1979
- start: {
1980
- line: value.loc.start.line,
1981
- column: value.loc.start.column - 1,
1982
- },
1983
- end: {
1984
- line: value.loc.end.line,
1985
- column: value.loc.end.column + 1,
1986
- },
1987
- },
1988
- },
1989
- context.state.loose ? context.state.analysis.errors : undefined,
1990
- context.state.analysis.comments,
1991
- );
1992
- }
1993
- if (attr.name.type === 'Identifier') {
1994
- attribute_names.add(attr.name);
1995
-
1996
- if (attr.name.name === 'key') {
1997
- error(
1998
- 'The `key` attribute is not a thing in Ripple, and cannot be used on DOM elements. If you are using a for loop, then use the `for (let item of items; key item.id)` syntax.',
1999
- state.analysis.module.filename,
2000
- attr,
2001
- context.state.loose ? context.state.analysis.errors : undefined,
2002
- context.state.analysis.comments,
2003
- );
2004
- }
2005
-
2006
- if (
2007
- attr.value &&
2008
- attr.value.type === 'MemberExpression' &&
2009
- attr.value.object.type === 'StyleIdentifier'
2010
- ) {
2011
- error(
2012
- '`#style` cannot be used directly on DOM elements. Pass the class to a child component instead.',
2013
- state.analysis.module.filename,
2014
- attr.value.object,
2015
- context.state.loose ? context.state.analysis.errors : undefined,
2016
- context.state.analysis.comments,
2017
- );
2018
- }
2019
-
2020
- if (is_event_attribute(attr.name.name)) {
2021
- const handler = visit(/** @type {AST.Expression} */ (attr.value), state);
2022
- const is_delegated = is_delegated_event(attr.name.name, handler, context);
2023
-
2024
- if (is_delegated) {
2025
- if (attr.metadata === undefined) {
2026
- attr.metadata = { path: [...path] };
2027
- }
2028
-
2029
- attr.metadata.delegated = is_delegated;
2030
- }
2031
- } else if (attr.value !== null) {
2032
- visit(attr.value, state);
2033
- }
2034
- }
2035
- }
2036
- }
2037
-
2038
- if (is_void && node.children.length > 0) {
2039
- error(
2040
- `The <${/** @type {AST.Identifier} */ (node.id).name}> element is a void element and cannot have children`,
2041
- state.analysis.module.filename,
2042
- node,
2043
- context.state.loose ? context.state.analysis.errors : undefined,
2044
- context.state.analysis.comments,
2045
- );
2046
- }
2047
- } else {
2048
- for (const attr of node.attributes) {
2049
- if (attr.type === 'Attribute') {
2050
- if (attr.name.type === 'Identifier') {
2051
- attribute_names.add(attr.name);
2052
- }
2053
- if (attr.value !== null) {
2054
- visit(attr.value, state);
2055
- }
2056
- } else if (attr.type === 'SpreadAttribute') {
2057
- visit(attr.argument, state);
2058
- } else if (attr.type === 'RefAttribute') {
2059
- visit(attr.argument, state);
2060
- }
2061
- }
2062
- /** @type {(AST.Node | AST.Expression)[]} */
2063
- let implicit_children = [];
2064
-
2065
- for (const child of node.children) {
2066
- if (child.type === 'Component') {
2067
- error(
2068
- 'Component declarations cannot be used inside composite component children. Pass them as explicit props on the template element instead.',
2069
- state.analysis.module.filename,
2070
- child.id || child,
2071
- context.state.loose ? context.state.analysis.errors : undefined,
2072
- context.state.analysis.comments,
2073
- );
2074
- } else if (child.type !== 'EmptyStatement') {
2075
- implicit_children.push(
2076
- child.type === 'RippleExpression' || child.type === 'Text' || child.type === 'Html'
2077
- ? child.expression
2078
- : child,
2079
- );
2080
- }
2081
- }
2082
- }
2083
-
2084
- // Validation
2085
- for (const attribute of attribute_names) {
2086
- const name = attribute.name;
2087
- if (name === 'children') {
2088
- if (is_dom_element) {
2089
- error(
2090
- 'Cannot have a `children` prop on an element',
2091
- state.analysis.module.filename,
2092
- attribute,
2093
- context.state.loose ? context.state.analysis.errors : undefined,
2094
- context.state.analysis.comments,
2095
- );
2096
- }
2097
- }
2098
- }
2099
-
2100
- return {
2101
- ...node,
2102
- children: node.children.map((child) => visit(child)),
2103
- };
2104
- },
2105
-
2106
- RippleExpression(node, context) {
2107
- mark_control_flow_has_template(context.path);
2108
-
2109
- context.next();
2110
- },
2111
-
2112
- Text(node, context) {
2113
- mark_control_flow_has_template(context.path);
2114
-
2115
- if (is_children_template_expression(/** @type {AST.Expression} */ (node.expression), context)) {
2116
- error(
2117
- '`children` cannot be rendered using explicit text interpolation. Use `{children}` or `{props.children}` instead.',
2118
- context.state.analysis.module.filename,
2119
- node.expression,
2120
- context.state.loose ? context.state.analysis.errors : undefined,
2121
- context.state.analysis.comments,
2122
- );
2123
- }
2124
-
2125
- context.next();
2126
- },
2127
-
2128
- AwaitExpression(node, context) {
2129
- const parent_block = get_parent_block_node(context);
2130
-
2131
- if (is_inside_component(context)) {
2132
- const adjusted_node /** @type {AST.AwaitExpression} */ = {
2133
- ...node,
2134
- end: /** @type {AST.NodeWithLocation} */ (node).start + 'await'.length,
2135
- };
2136
- error(
2137
- '`await` is not allowed inside client components. Use `trackAsync(() => ...)` with an upstream `try { ... } pending { ... }` boundary instead.',
2138
- context.state.analysis.module.filename,
2139
- adjusted_node,
2140
- context.state.loose ? context.state.analysis.errors : undefined,
2141
- context.state.analysis.comments,
2142
- );
2143
- }
2144
-
2145
- if (parent_block) {
2146
- if (!parent_block.metadata) {
2147
- parent_block.metadata = { path: [...context.path] };
2148
- }
2149
- }
2150
-
2151
- context.next();
2152
- },
2153
- };
2154
-
2155
- /**
2156
- *
2157
- * @param {AST.Program} ast
2158
- * @param {string} filename
2159
- * @param {AnalyzeOptions} options
2160
- * @returns {AnalysisResult}
2161
- */
2162
- export function analyze(ast, filename, options = {}) {
2163
- const scope_root = new ScopeRoot();
2164
- const errors = options.errors ?? [];
2165
- const comments = options.comments ?? [];
2166
- const loose = options.loose ?? false;
2167
-
2168
- const { scope, scopes } = create_scopes(ast, scope_root, null, {
2169
- loose,
2170
- errors,
2171
- filename,
2172
- comments,
2173
- });
2174
-
2175
- const analysis = /** @type {AnalysisResult} */ ({
2176
- module: { ast, scope, scopes, filename },
2177
- ast,
2178
- scope,
2179
- scopes,
2180
- component_metadata: [],
2181
- metadata: {
2182
- serverIdentifierPresent: false,
2183
- },
2184
- errors,
2185
- comments,
2186
- });
2187
-
2188
- walk(
2189
- ast,
2190
- /** @type {AnalysisState} */
2191
- {
2192
- scope,
2193
- scopes,
2194
- analysis,
2195
- inside_head: false,
2196
- ancestor_server_block: undefined,
2197
- to_ts: options.to_ts ?? false,
2198
- loose,
2199
- configured_compat_kinds:
2200
- options.compat_kinds === undefined ? undefined : new Set(options.compat_kinds),
2201
- metadata: {},
2202
- mode: options.mode,
2203
- },
2204
- visitors,
2205
- );
2206
-
2207
- return analysis;
2208
- }