ripple 0.2.46 → 0.2.48

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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/compiler/phases/1-parse/index.js +52 -2
  3. package/src/compiler/phases/2-analyze/index.js +640 -667
  4. package/src/compiler/phases/3-transform/index.js +1878 -1879
  5. package/src/compiler/phases/3-transform/segments.js +2 -2
  6. package/src/compiler/utils.js +598 -550
  7. package/src/jsx-runtime.js +12 -12
  8. package/src/runtime/array.js +611 -609
  9. package/src/runtime/index.js +29 -17
  10. package/src/runtime/internal/client/array.js +121 -121
  11. package/src/runtime/internal/client/blocks.js +206 -206
  12. package/src/runtime/internal/client/constants.js +2 -2
  13. package/src/runtime/internal/client/context.js +40 -40
  14. package/src/runtime/internal/client/events.js +191 -191
  15. package/src/runtime/internal/client/for.js +355 -355
  16. package/src/runtime/internal/client/if.js +25 -25
  17. package/src/runtime/internal/client/index.js +57 -56
  18. package/src/runtime/internal/client/operations.js +32 -32
  19. package/src/runtime/internal/client/portal.js +19 -19
  20. package/src/runtime/internal/client/render.js +132 -132
  21. package/src/runtime/internal/client/runtime.js +839 -835
  22. package/src/runtime/internal/client/template.js +36 -36
  23. package/src/runtime/internal/client/try.js +113 -113
  24. package/src/runtime/internal/client/types.d.ts +12 -11
  25. package/src/runtime/internal/client/utils.js +5 -5
  26. package/src/runtime/map.js +139 -139
  27. package/src/runtime/set.js +130 -130
  28. package/src/utils/ast.js +189 -189
  29. package/src/utils/builders.js +244 -244
  30. package/src/utils/sanitize_template_string.js +1 -1
  31. package/tests/__snapshots__/composite.test.ripple.snap +1 -1
  32. package/tests/accessors-props.test.ripple +9 -9
  33. package/tests/basic.test.ripple +4 -4
  34. package/tests/boundaries.test.ripple +17 -17
  35. package/tests/compiler.test.ripple +14 -14
  36. package/tests/composite.test.ripple +43 -72
  37. package/tests/context.test.ripple +35 -12
  38. package/types/index.d.ts +38 -34
@@ -2,13 +2,14 @@ import * as b from '../../../utils/builders.js';
2
2
  import { walk } from 'zimmerframe';
3
3
  import { create_scopes, ScopeRoot } from '../../scope.js';
4
4
  import {
5
- get_delegated_event,
6
- is_event_attribute,
7
- is_inside_component,
8
- is_ripple_import,
9
- is_tracked_computed_property,
10
- is_tracked_name,
11
- is_void_element,
5
+ get_delegated_event,
6
+ is_element_dom_element,
7
+ is_event_attribute,
8
+ is_inside_component,
9
+ is_ripple_import,
10
+ is_tracked_computed_property,
11
+ is_tracked_name,
12
+ is_void_element,
12
13
  } from '../../utils.js';
13
14
  import { extract_paths } from '../../../utils/ast.js';
14
15
  import is_reference from 'is-reference';
@@ -16,676 +17,648 @@ import { prune_css } from './prune.js';
16
17
  import { error } from '../../errors.js';
17
18
 
18
19
  function mark_for_loop_has_template(path) {
19
- for (let i = path.length - 1; i >= 0; i -= 1) {
20
- const node = path[i];
21
-
22
- if (
23
- node.type === 'Component' ||
24
- node.type === 'FunctionExpression' ||
25
- node.type === 'ArrowFunctionExpression' ||
26
- node.type === 'FunctionDeclaration'
27
- ) {
28
- break;
29
- }
30
- if (
31
- node.type === 'ForStatement' ||
32
- node.type === 'ForInStatement' ||
33
- node.type === 'ForOfStatement'
34
- ) {
35
- node.metadata.has_template = true;
36
- break;
37
- }
38
- }
20
+ for (let i = path.length - 1; i >= 0; i -= 1) {
21
+ const node = path[i];
22
+
23
+ if (
24
+ node.type === 'Component' ||
25
+ node.type === 'FunctionExpression' ||
26
+ node.type === 'ArrowFunctionExpression' ||
27
+ node.type === 'FunctionDeclaration'
28
+ ) {
29
+ break;
30
+ }
31
+ if (
32
+ node.type === 'ForStatement' ||
33
+ node.type === 'ForInStatement' ||
34
+ node.type === 'ForOfStatement'
35
+ ) {
36
+ node.metadata.has_template = true;
37
+ break;
38
+ }
39
+ }
39
40
  }
40
41
 
41
42
  function visit_function(node, context) {
42
- node.metadata = {
43
- hoisted: false,
44
- hoisted_params: [],
45
- scope: context.state.scope,
46
- tracked: false,
47
- };
48
-
49
- if (node.params.length > 0) {
50
- for (let i = 0; i < node.params.length; i += 1) {
51
- const param = node.params[i];
52
-
53
- if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
54
- const paths = extract_paths(param);
55
- const id = context.state.scope.generate('__arg');
56
- const arg_id = b.id(id);
57
-
58
- for (const path of paths) {
59
- const name = path.node.name;
60
-
61
- const expression = path.expression(arg_id);
62
- const binding = context.state.scope.get(name);
63
-
64
- if (binding !== null && is_tracked_name(name)) {
65
- node.params[i] = b.id(id);
66
- binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
67
-
68
- binding.transform = {
69
- read: (_, __, visit) => visit(expression),
70
- };
71
- }
72
- }
73
- }
74
- }
75
- }
76
-
77
- context.next({
78
- ...context.state,
79
- function_depth: context.state.function_depth + 1,
80
- expression: null,
81
- });
82
-
83
- if (node.metadata.tracked) {
84
- mark_as_tracked(context.path);
85
- }
43
+ node.metadata = {
44
+ hoisted: false,
45
+ hoisted_params: [],
46
+ scope: context.state.scope,
47
+ tracked: false,
48
+ };
49
+
50
+ if (node.params.length > 0) {
51
+ for (let i = 0; i < node.params.length; i += 1) {
52
+ const param = node.params[i];
53
+
54
+ if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
55
+ const paths = extract_paths(param);
56
+ const id = context.state.scope.generate('__arg');
57
+ const arg_id = b.id(id);
58
+
59
+ for (const path of paths) {
60
+ const name = path.node.name;
61
+
62
+ const expression = path.expression(arg_id);
63
+ const binding = context.state.scope.get(name);
64
+
65
+ if (binding !== null && is_tracked_name(name)) {
66
+ node.params[i] = b.id(id);
67
+ binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
68
+
69
+ binding.transform = {
70
+ read: (_, __, visit) => visit(expression),
71
+ };
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ context.next({
79
+ ...context.state,
80
+ function_depth: context.state.function_depth + 1,
81
+ expression: null,
82
+ });
83
+
84
+ if (node.metadata.tracked) {
85
+ mark_as_tracked(context.path);
86
+ }
86
87
  }
87
88
 
88
89
  function mark_as_tracked(path) {
89
- for (let i = path.length - 1; i >= 0; i -= 1) {
90
- const node = path[i];
91
-
92
- if (node.type === 'Component') {
93
- break;
94
- }
95
- if (
96
- node.type === 'FunctionExpression' ||
97
- node.type === 'ArrowFunctionExpression' ||
98
- node.type === 'FunctionDeclaration'
99
- ) {
100
- node.metadata.tracked = true;
101
- break;
102
- }
103
- }
90
+ for (let i = path.length - 1; i >= 0; i -= 1) {
91
+ const node = path[i];
92
+
93
+ if (node.type === 'Component') {
94
+ break;
95
+ }
96
+ if (
97
+ node.type === 'FunctionExpression' ||
98
+ node.type === 'ArrowFunctionExpression' ||
99
+ node.type === 'FunctionDeclaration'
100
+ ) {
101
+ node.metadata.tracked = true;
102
+ break;
103
+ }
104
+ }
104
105
  }
105
106
 
106
107
  const visitors = {
107
- _(node, { state, next, path }) {
108
- // Set up metadata.path for each node (needed for CSS pruning)
109
- if (!node.metadata) {
110
- node.metadata = {};
111
- }
112
- node.metadata.path = [...path];
113
-
114
- const scope = state.scopes.get(node);
115
- next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
116
- },
117
-
118
- Identifier(node, context) {
119
- const binding = context.state.scope.get(node.name);
120
- const parent = context.path.at(-1);
121
-
122
- if (
123
- is_reference(node, /** @type {Node} */ (parent)) &&
124
- (is_tracked_name(node) || node.tracked) &&
125
- binding?.node !== node
126
- ) {
127
- mark_as_tracked(context.path);
128
- if (context.state.metadata?.tracking === false) {
129
- context.state.metadata.tracking = true;
130
- }
131
- }
132
-
133
- if (
134
- is_reference(node, /** @type {Node} */ (parent)) &&
135
- node.tracked &&
136
- binding?.node !== node
137
- ) {
138
- if (context.state.metadata?.tracking === false) {
139
- context.state.metadata.tracking = true;
140
- }
141
- binding.transform = {
142
- read_tracked: (node) => b.call('$.get_tracked', node),
143
- assign_tracked: (node, value) => b.call('$.set', node, value, b.id('__block')),
144
- update_tracked: (node) => {
145
- return b.call(
146
- node.prefix ? '$.update_pre' : '$.update',
147
- node.argument,
148
- b.id('__block'),
149
- node.operator === '--' && b.literal(-1),
150
- );
151
- },
152
- };
153
- }
154
-
155
- context.next();
156
- },
157
-
158
- MemberExpression(node, context) {
159
- const parent = context.path.at(-1);
160
-
161
- if (context.state.metadata?.tracking === false && parent.type !== 'AssignmentExpression') {
162
- if (
163
- node.property.type === 'Identifier' &&
164
- !node.computed &&
165
- is_tracked_name(node.property.name)
166
- ) {
167
- context.state.metadata.tracking = true;
168
- } else if (
169
- node.computed &&
170
- is_tracked_computed_property(node.object, node.property, context)
171
- ) {
172
- context.state.metadata.tracking = true;
173
- }
174
- }
175
-
176
- context.next();
177
- },
178
-
179
- CallExpression(node, context) {
180
- if (context.state.metadata?.tracking === false) {
181
- context.state.metadata.tracking = true;
182
- }
183
-
184
- context.next();
185
- },
186
-
187
- ObjectExpression(node, context) {
188
- for (const property of node.properties) {
189
- if (
190
- property.type === 'Property' &&
191
- !property.computed &&
192
- property.key.type === 'Identifier' &&
193
- property.kind === 'init' &&
194
- is_tracked_name(property.key.name)
195
- ) {
196
- mark_as_tracked(context.path);
197
- }
198
- }
199
-
200
- context.next();
201
- },
202
-
203
- ArrayExpression(node, context) {
204
- for (const element of node.elements) {
205
- if (element !== null && element.type === 'Identifier' && is_tracked_name(element.name)) {
206
- mark_as_tracked(context.path);
207
- }
208
- }
209
-
210
- context.next();
211
- },
212
-
213
- VariableDeclaration(node, context) {
214
- const { state, visit, path } = context;
215
-
216
- for (const declarator of node.declarations) {
217
- if (is_inside_component(context) && node.kind === 'var') {
218
- error(
219
- 'var declarations are not allowed in components, use let or const instead',
220
- state.analysis.module.filename,
221
- declarator,
222
- );
223
- }
224
- const metadata = { tracking: false, await: false };
225
- const parent = path.at(-1);
226
- const init_is_untracked =
227
- declarator.init !== null &&
228
- declarator.init.type === 'CallExpression' &&
229
- is_ripple_import(declarator.init.callee, context) &&
230
- declarator.init.callee.type === 'Identifier' &&
231
- (declarator.init.callee.name === 'untrack' || declarator.init.callee.name === 'deferred');
232
-
233
- if (declarator.id.type === 'Identifier') {
234
- const binding = state.scope.get(declarator.id.name);
235
-
236
- if (binding !== null && parent?.type !== 'ForOfStatement') {
237
- if (is_tracked_name(declarator.id.name)) {
238
- binding.kind = 'tracked';
239
-
240
- mark_as_tracked(path);
241
-
242
- visit(declarator, { ...state, metadata });
243
-
244
- if (init_is_untracked && metadata.tracking) {
245
- metadata.tracking = false;
246
- }
247
-
248
- binding.transform = {
249
- read: (node) => {
250
- return metadata.tracking && !metadata.await
251
- ? b.call('$.get_computed', node)
252
- : b.call('$.get_tracked', node);
253
- },
254
- assign: (node, value) => {
255
- return b.call('$.set', node, value, b.id('__block'));
256
- },
257
- update: (node) => {
258
- return b.call(
259
- node.prefix ? '$.update_pre' : '$.update',
260
- node.argument,
261
- b.id('__block'),
262
- node.operator === '--' && b.literal(-1),
263
- );
264
- },
265
- };
266
- } else if (binding.initial?.type !== 'Literal') {
267
- for (const ref of binding.references) {
268
- const path = ref.path;
269
- const parent_node = path?.at(-1);
270
-
271
- // We're reading a computed property, which might mean it's a reactive property
272
- if (parent_node?.type === 'MemberExpression' && parent_node.computed) {
273
- binding.transform = {
274
- assign: (node, value, computed) => {
275
- if (!computed) {
276
- return node;
277
- }
278
- return b.call('$.old_set_property', node, computed, value, b.id('__block'));
279
- },
280
- };
281
- break;
282
- }
283
- }
284
- }
285
-
286
- visit(declarator, state);
287
- } else {
288
- visit(declarator, state);
289
- }
290
- } else {
291
- const paths = extract_paths(declarator.id);
292
- const has_tracked = paths.some(
293
- (path) => path.node.type === 'Identifier' && is_tracked_name(path.node.name),
294
- );
295
- for (const path of paths) {
296
- if (path.node.tracked) {
297
- error(
298
- 'Variables cannot be reactively referenced using @',
299
- state.analysis.module.filename,
300
- path.node,
301
- );
302
- }
303
- }
304
-
305
- if (has_tracked) {
306
- const tmp = state.scope.generate('tmp');
307
- declarator.transformed = b.id(tmp);
308
-
309
- if (declarator.init !== null) {
310
- visit(declarator.init, { ...state, metadata });
311
- }
312
-
313
- if (init_is_untracked && metadata.tracking) {
314
- metadata.tracking = false;
315
- }
316
-
317
- for (const path of paths) {
318
- const binding = state.scope.get(path.node.name);
319
-
320
- binding.transform = {
321
- read: (node) => {
322
- const value = path.expression?.(b.id(tmp));
323
-
324
- if (metadata.tracking && metadata.await) {
325
- // TODO
326
- debugger;
327
- } else if (metadata.tracking && !metadata.await) {
328
- if (is_tracked_name(path.node.name) && value.type === 'MemberExpression') {
329
- return b.call(
330
- '$.old_get_property',
331
- b.call('$.get_computed', value.object),
332
- value.property.type === 'Identifier'
333
- ? b.literal(value.property.name)
334
- : value.property,
335
- );
336
- }
337
-
338
- const key =
339
- value.property.type === 'Identifier'
340
- ? b.key(value.property.name)
341
- : value.property;
342
-
343
- return b.member(
344
- b.call('$.get_computed', value.object),
345
- key,
346
- key.type === 'Literal',
347
- );
348
- }
349
-
350
- if (is_tracked_name(path.node.name) && value.type === 'MemberExpression') {
351
- return b.call(
352
- '$.old_get_property',
353
- value.object,
354
- value.property.type === 'Identifier'
355
- ? b.literal(value.property.name)
356
- : value.property,
357
- );
358
- }
359
-
360
- return value;
361
- },
362
- };
363
- }
364
- } else {
365
- visit(declarator, state);
366
- }
367
- }
368
-
369
- declarator.metadata = metadata;
370
- }
371
- },
372
-
373
- ArrowFunctionExpression(node, context) {
374
- visit_function(node, context);
375
- },
376
- FunctionExpression(node, context) {
377
- visit_function(node, context);
378
- },
379
- FunctionDeclaration(node, context) {
380
- visit_function(node, context);
381
- },
382
-
383
- Component(node, context) {
384
- context.state.component = node;
385
-
386
- if (node.params.length > 0) {
387
- const props = node.params[0];
388
-
389
- if (props.type === 'ObjectPattern') {
390
- const paths = extract_paths(props);
391
-
392
- for (const path of paths) {
393
- const name = path.node.name;
394
- const binding = context.state.scope.get(name);
395
-
396
- if (binding !== null && is_tracked_name(name)) {
397
- binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
398
-
399
- if (path.has_default_value) {
400
- binding.transform = {
401
- read: (_) => b.call('$.get_computed', path.node),
402
- assign: (node, value) => {
403
- return b.call('$.set', path.node, value, b.id('__block'));
404
- },
405
- update: (_) =>
406
- b.call(
407
- node.prefix ? '$.update_pre' : '$.update',
408
- path.node,
409
- b.id('__block'),
410
- node.operator === '--' && b.literal(-1),
411
- ),
412
- };
413
- } else {
414
- binding.transform = {
415
- read: (_) => b.call('$.old_get_property', b.id('__props'), b.literal(name)),
416
- assign: (node, value) => {
417
- return b.call(
418
- '$.old_set_property',
419
- b.id('__props'),
420
- b.literal(name),
421
- value,
422
- b.id('__block'),
423
- );
424
- },
425
- update: (_) =>
426
- b.call(
427
- node.prefix ? '$.old_update_property_pre' : '$.old_update_property',
428
- b.id('__props'),
429
- b.literal(name),
430
- b.id('__block'),
431
- node.operator === '--' && b.literal(-1),
432
- ),
433
- };
434
- }
435
- }
436
- }
437
- } else if (props.type === 'AssignmentPattern') {
438
- error(
439
- 'Props are always an object, use destructured props with default values instead',
440
- context.state.analysis.module.filename,
441
- props,
442
- );
443
- }
444
- }
445
- const elements = [];
446
-
447
- context.next({ ...context.state, elements });
448
-
449
- const css = node.css;
450
-
451
- if (css !== null) {
452
- for (const node of elements) {
453
- prune_css(css, node);
454
- }
455
- }
456
- },
457
-
458
- ForStatement(node, context) {
459
- if (is_inside_component(context)) {
460
- error(
461
- 'For loops are not supported in components. Use for...of instead.',
462
- context.state.analysis.module.filename,
463
- node,
464
- );
465
- }
466
-
467
- context.next();
468
- },
469
-
470
- ForOfStatement(node, context) {
471
- if (!is_inside_component(context)) {
472
- return context.next();
473
- }
474
-
475
- node.metadata = {
476
- has_template: false,
477
- };
478
- context.next();
479
- if (!node.metadata.has_template) {
480
- error(
481
- 'For...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
482
- context.state.analysis.module.filename,
483
- node,
484
- );
485
- }
486
- },
487
-
488
- ForInStatement(node, context) {
489
- if (is_inside_component(context)) {
490
- error(
491
- 'For...in loops are not supported in components. Use for...of instead.',
492
- context.state.analysis.module.filename,
493
- node,
494
- );
495
- }
496
-
497
- context.next();
498
- },
499
-
500
- JSXElement(_, context) {
501
- {
502
- error(
503
- 'Elements cannot be used as generic expressions, only as statements within a component',
504
- context.state.analysis.module.filename,
505
- node,
506
- );
507
- }
508
- },
509
-
510
- Element(node, { state, visit, path }) {
511
- const is_dom_element =
512
- node.id.type === 'Identifier' &&
513
- node.id.name[0].toLowerCase() === node.id.name[0] &&
514
- node.id.name[0] !== '$';
515
- const attribute_names = new Set();
516
-
517
- mark_for_loop_has_template(path);
518
-
519
- if (is_dom_element) {
520
- const is_void = is_void_element(node.id.name);
521
-
522
- if (state.elements) {
523
- state.elements.push(node);
524
- }
525
-
526
- for (const attr of node.attributes) {
527
- if (attr.type === 'Attribute') {
528
- if (attr.name.type === 'Identifier') {
529
- attribute_names.add(attr.name);
530
-
531
- if (is_event_attribute(attr.name.name)) {
532
- const event_name = attr.name.name.slice(2).toLowerCase();
533
- const handler = visit(attr.value, state);
534
- const delegated_event = get_delegated_event(event_name, handler, state);
535
-
536
- if (delegated_event !== null) {
537
- if (delegated_event.hoisted) {
538
- delegated_event.function.metadata.hoisted = true;
539
- delegated_event.hoisted = true;
540
- }
541
-
542
- if (attr.metadata === undefined) {
543
- attr.metadata = {};
544
- }
545
-
546
- attr.metadata.delegated = delegated_event;
547
- }
548
- } else {
549
- visit(attr.value, state);
550
- }
551
- }
552
- } else if (attr.type === 'AccessorAttribute') {
553
- error(
554
- 'Accessor props are not supported on DOM elements',
555
- state.analysis.module.filename,
556
- attr,
557
- );
558
- }
559
- }
560
-
561
- if (is_void && node.children.length > 0) {
562
- error(
563
- `The <${node.id.name}> element is a void element and cannot have children`,
564
- state.analysis.module.filename,
565
- node,
566
- );
567
- }
568
- } else {
569
- for (const attr of node.attributes) {
570
- if (attr.type === 'Attribute') {
571
- if (attr.name.type === 'Identifier') {
572
- attribute_names.add(attr.name);
573
- }
574
- } else if (attr.type === 'AccessorAttribute') {
575
- if (!attr.name.name.startsWith('$')) {
576
- error(
577
- 'Accessor props must always be $ prefixed as they are reactive',
578
- state.analysis.module.filename,
579
- attr,
580
- );
581
- }
582
- attribute_names.add(attr.name);
583
- }
584
- }
585
-
586
- let implicit_children = false;
587
- let explicit_children = false;
588
-
589
- for (const child of node.children) {
590
- if (child.type === 'Component') {
591
- if (child.id.name === '$children') {
592
- explicit_children = true;
593
- if (implicit_children) {
594
- error(
595
- 'Cannot have both implicit and explicit children',
596
- state.analysis.module.filename,
597
- node,
598
- );
599
- }
600
- }
601
- } else if (child.type !== 'EmptyStatement') {
602
- implicit_children = true;
603
- if (explicit_children) {
604
- error(
605
- 'Cannot have both implicit and explicit children',
606
- state.analysis.module.filename,
607
- node,
608
- );
609
- }
610
- }
611
- }
612
- }
613
-
614
- // Validation
615
- for (const attribute of attribute_names) {
616
- const name = attribute.name;
617
- if (name === 'children') {
618
- if (is_dom_element) {
619
- error(
620
- 'Cannot have a `children` prop on an element',
621
- state.analysis.module.filename,
622
- attribute,
623
- );
624
- } else {
625
- error(
626
- 'Cannot have a `children` prop on a component, did you mean `$children`?',
627
- state.analysis.module.filename,
628
- attribute,
629
- );
630
- }
631
- }
632
-
633
- if (is_tracked_name(name)) {
634
- attribute_names.forEach((n) => {
635
- if (n.name.slice(1) === name) {
636
- error(
637
- `Cannot have both ${name} and ${name.slice(1)} on the same element`,
638
- state.analysis.module.filename,
639
- n,
640
- );
641
- }
642
- });
643
- }
644
- }
645
-
646
- return {
647
- ...node,
648
- children: node.children.map((child) => visit(child)),
649
- };
650
- },
651
-
652
- Text(node, context) {
653
- mark_for_loop_has_template(context.path);
654
- context.next();
655
- },
656
-
657
- AwaitExpression(node, context) {
658
- if (is_inside_component(context)) {
659
- if (context.state.metadata?.await === false) {
660
- context.state.metadata.await = true;
661
- }
662
- }
663
-
664
- context.next();
665
- },
108
+ _(node, { state, next, path }) {
109
+ // Set up metadata.path for each node (needed for CSS pruning)
110
+ if (!node.metadata) {
111
+ node.metadata = {};
112
+ }
113
+ node.metadata.path = [...path];
114
+
115
+ const scope = state.scopes.get(node);
116
+ next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
117
+ },
118
+
119
+ Identifier(node, context) {
120
+ const binding = context.state.scope.get(node.name);
121
+ const parent = context.path.at(-1);
122
+
123
+ if (binding?.kind === 'prop' || binding?.kind === 'prop_fallback') {
124
+ mark_as_tracked(context.path);
125
+ if (context.state.metadata?.tracking === false) {
126
+ context.state.metadata.tracking = true;
127
+ }
128
+ }
129
+
130
+ if (
131
+ is_reference(node, /** @type {Node} */ (parent)) &&
132
+ (is_tracked_name(node) || node.tracked) &&
133
+ binding?.node !== node
134
+ ) {
135
+ mark_as_tracked(context.path);
136
+ if (context.state.metadata?.tracking === false) {
137
+ context.state.metadata.tracking = true;
138
+ }
139
+ }
140
+
141
+ if (
142
+ is_reference(node, /** @type {Node} */ (parent)) &&
143
+ node.tracked &&
144
+ binding?.node !== node
145
+ ) {
146
+ if (context.state.metadata?.tracking === false) {
147
+ context.state.metadata.tracking = true;
148
+ }
149
+ }
150
+
151
+ context.next();
152
+ },
153
+
154
+ MemberExpression(node, context) {
155
+ const parent = context.path.at(-1);
156
+
157
+ if (context.state.metadata?.tracking === false && parent.type !== 'AssignmentExpression') {
158
+ if (
159
+ node.property.type === 'Identifier' &&
160
+ !node.computed &&
161
+ is_tracked_name(node.property.name)
162
+ ) {
163
+ context.state.metadata.tracking = true;
164
+ } else if (
165
+ node.computed &&
166
+ is_tracked_computed_property(node.object, node.property, context)
167
+ ) {
168
+ context.state.metadata.tracking = true;
169
+ }
170
+ }
171
+
172
+ context.next();
173
+ },
174
+
175
+ CallExpression(node, context) {
176
+ if (context.state.metadata?.tracking === false) {
177
+ context.state.metadata.tracking = true;
178
+ }
179
+
180
+ if (!is_inside_component(context, true)) {
181
+ mark_as_tracked(context.path);
182
+ }
183
+
184
+ context.next();
185
+ },
186
+
187
+ ObjectExpression(node, context) {
188
+ for (const property of node.properties) {
189
+ if (
190
+ property.type === 'Property' &&
191
+ !property.computed &&
192
+ property.key.type === 'Identifier' &&
193
+ property.kind === 'init' &&
194
+ is_tracked_name(property.key.name)
195
+ ) {
196
+ mark_as_tracked(context.path);
197
+ }
198
+ }
199
+
200
+ context.next();
201
+ },
202
+
203
+ ArrayExpression(node, context) {
204
+ for (const element of node.elements) {
205
+ if (element !== null && element.type === 'Identifier' && is_tracked_name(element.name)) {
206
+ mark_as_tracked(context.path);
207
+ }
208
+ }
209
+
210
+ context.next();
211
+ },
212
+
213
+ VariableDeclaration(node, context) {
214
+ const { state, visit, path } = context;
215
+
216
+ for (const declarator of node.declarations) {
217
+ if (is_inside_component(context) && node.kind === 'var') {
218
+ error(
219
+ 'var declarations are not allowed in components, use let or const instead',
220
+ state.analysis.module.filename,
221
+ declarator,
222
+ );
223
+ }
224
+ const metadata = { tracking: false, await: false };
225
+ const parent = path.at(-1);
226
+ const init_is_untracked =
227
+ declarator.init !== null &&
228
+ declarator.init.type === 'CallExpression' &&
229
+ is_ripple_import(declarator.init.callee, context) &&
230
+ declarator.init.callee.type === 'Identifier' &&
231
+ (declarator.init.callee.name === 'untrack' || declarator.init.callee.name === 'deferred');
232
+
233
+ if (declarator.id.type === 'Identifier') {
234
+ const binding = state.scope.get(declarator.id.name);
235
+
236
+ if (binding !== null && parent?.type !== 'ForOfStatement') {
237
+ if (is_tracked_name(declarator.id.name)) {
238
+ binding.kind = 'tracked';
239
+
240
+ mark_as_tracked(path);
241
+
242
+ visit(declarator, { ...state, metadata });
243
+
244
+ if (init_is_untracked && metadata.tracking) {
245
+ metadata.tracking = false;
246
+ }
247
+
248
+ binding.transform = {
249
+ read: (node) => {
250
+ return metadata.tracking && !metadata.await
251
+ ? b.call('$.get_derived', node)
252
+ : b.call('$.get_tracked', node);
253
+ },
254
+ assign: (node, value) => {
255
+ return b.call('$.set', node, value, b.id('__block'));
256
+ },
257
+ update: (node) => {
258
+ return b.call(
259
+ node.prefix ? '$.update_pre' : '$.update',
260
+ node.argument,
261
+ b.id('__block'),
262
+ node.operator === '--' && b.literal(-1),
263
+ );
264
+ },
265
+ };
266
+ } else if (binding.initial?.type !== 'Literal') {
267
+ for (const ref of binding.references) {
268
+ const path = ref.path;
269
+ const parent_node = path?.at(-1);
270
+
271
+ // We're reading a computed property, which might mean it's a reactive property
272
+ if (!ref.node.tracked && parent_node?.type === 'MemberExpression' && parent_node.computed) {
273
+ binding.transform = {
274
+ assign: (node, value, computed) => {
275
+ if (!computed) {
276
+ return node;
277
+ }
278
+ return b.call('$.old_set_property', node, computed, value, b.id('__block'));
279
+ },
280
+ };
281
+ break;
282
+ }
283
+ }
284
+ }
285
+
286
+ visit(declarator, state);
287
+ } else {
288
+ visit(declarator, state);
289
+ }
290
+ } else {
291
+ const paths = extract_paths(declarator.id);
292
+ const has_tracked = paths.some(
293
+ (path) => path.node.type === 'Identifier' && is_tracked_name(path.node.name),
294
+ );
295
+ for (const path of paths) {
296
+ if (path.node.tracked) {
297
+ error(
298
+ 'Variables cannot be reactively referenced using @',
299
+ state.analysis.module.filename,
300
+ path.node,
301
+ );
302
+ }
303
+ }
304
+
305
+ if (has_tracked) {
306
+ const tmp = state.scope.generate('tmp');
307
+ declarator.transformed = b.id(tmp);
308
+
309
+ if (declarator.init !== null) {
310
+ visit(declarator.init, { ...state, metadata });
311
+ }
312
+
313
+ if (init_is_untracked && metadata.tracking) {
314
+ metadata.tracking = false;
315
+ }
316
+
317
+ for (const path of paths) {
318
+ const binding = state.scope.get(path.node.name);
319
+
320
+ binding.transform = {
321
+ read: (node) => {
322
+ const value = path.expression?.(b.id(tmp));
323
+
324
+ if (metadata.tracking && metadata.await) {
325
+ // TODO
326
+ debugger;
327
+ } else if (metadata.tracking && !metadata.await) {
328
+ if (is_tracked_name(path.node.name) && value.type === 'MemberExpression') {
329
+ return b.call(
330
+ '$.old_get_property',
331
+ b.call('$.get_derived', value.object),
332
+ value.property.type === 'Identifier'
333
+ ? b.literal(value.property.name)
334
+ : value.property,
335
+ );
336
+ }
337
+
338
+ const key =
339
+ value.property.type === 'Identifier'
340
+ ? b.key(value.property.name)
341
+ : value.property;
342
+
343
+ return b.member(
344
+ b.call('$.get_derived', value.object),
345
+ key,
346
+ key.type === 'Literal',
347
+ );
348
+ }
349
+
350
+ if (is_tracked_name(path.node.name) && value.type === 'MemberExpression') {
351
+ return b.call(
352
+ '$.old_get_property',
353
+ value.object,
354
+ value.property.type === 'Identifier'
355
+ ? b.literal(value.property.name)
356
+ : value.property,
357
+ );
358
+ }
359
+
360
+ return value;
361
+ },
362
+ };
363
+ }
364
+ } else {
365
+ visit(declarator, state);
366
+ }
367
+ }
368
+
369
+ declarator.metadata = metadata;
370
+ }
371
+ },
372
+
373
+ ArrowFunctionExpression(node, context) {
374
+ visit_function(node, context);
375
+ },
376
+ FunctionExpression(node, context) {
377
+ visit_function(node, context);
378
+ },
379
+ FunctionDeclaration(node, context) {
380
+ visit_function(node, context);
381
+ },
382
+
383
+ Component(node, context) {
384
+ context.state.component = node;
385
+
386
+ if (node.params.length > 0) {
387
+ const props = node.params[0];
388
+
389
+ if (props.type === 'ObjectPattern') {
390
+ const paths = extract_paths(props);
391
+
392
+ for (const path of paths) {
393
+ const name = path.node.name;
394
+ const binding = context.state.scope.get(name);
395
+
396
+ if (binding !== null) {
397
+ binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
398
+
399
+ binding.transform = {
400
+ read: (_) => {
401
+ return path.expression(b.id('__props'))
402
+ },
403
+ assign: (node, value) => {
404
+ return b.assignment('=', path.expression(b.id('__props')), value);
405
+ },
406
+ update: (node) =>
407
+ b.update(node.operator, path.expression(b.id('__props')), node.prefix),
408
+ };
409
+ }
410
+ }
411
+ } else if (props.type === 'AssignmentPattern') {
412
+ error(
413
+ 'Props are always an object, use destructured props with default values instead',
414
+ context.state.analysis.module.filename,
415
+ props,
416
+ );
417
+ }
418
+ }
419
+ const elements = [];
420
+
421
+ context.next({ ...context.state, elements });
422
+
423
+ const css = node.css;
424
+
425
+ if (css !== null) {
426
+ for (const node of elements) {
427
+ prune_css(css, node);
428
+ }
429
+ }
430
+ },
431
+
432
+ ForStatement(node, context) {
433
+ if (is_inside_component(context)) {
434
+ error(
435
+ 'For loops are not supported in components. Use for...of instead.',
436
+ context.state.analysis.module.filename,
437
+ node,
438
+ );
439
+ }
440
+
441
+ context.next();
442
+ },
443
+
444
+ ForOfStatement(node, context) {
445
+ if (!is_inside_component(context)) {
446
+ return context.next();
447
+ }
448
+
449
+ node.metadata = {
450
+ has_template: false,
451
+ };
452
+ context.next();
453
+ if (!node.metadata.has_template) {
454
+ error(
455
+ 'For...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
456
+ context.state.analysis.module.filename,
457
+ node,
458
+ );
459
+ }
460
+ },
461
+
462
+ ForInStatement(node, context) {
463
+ if (is_inside_component(context)) {
464
+ error(
465
+ 'For...in loops are not supported in components. Use for...of instead.',
466
+ context.state.analysis.module.filename,
467
+ node,
468
+ );
469
+ }
470
+
471
+ context.next();
472
+ },
473
+
474
+ JSXElement(_, context) {
475
+ {
476
+ error(
477
+ 'Elements cannot be used as generic expressions, only as statements within a component',
478
+ context.state.analysis.module.filename,
479
+ node,
480
+ );
481
+ }
482
+ },
483
+
484
+ Element(node, context) {
485
+ const { state, visit, path } = context;
486
+ const is_dom_element = is_element_dom_element(node, context);
487
+ const attribute_names = new Set();
488
+
489
+ mark_for_loop_has_template(path);
490
+
491
+ if (is_dom_element) {
492
+ const is_void = is_void_element(node.id.name);
493
+
494
+ if (state.elements) {
495
+ state.elements.push(node);
496
+ }
497
+
498
+ for (const attr of node.attributes) {
499
+ if (attr.type === 'Attribute') {
500
+ if (attr.name.type === 'Identifier') {
501
+ attribute_names.add(attr.name);
502
+
503
+ if (is_event_attribute(attr.name.name)) {
504
+ const event_name = attr.name.name.slice(2).toLowerCase();
505
+ const handler = visit(attr.value, state);
506
+ const delegated_event = get_delegated_event(event_name, handler, state);
507
+
508
+ if (delegated_event !== null) {
509
+ if (delegated_event.hoisted) {
510
+ delegated_event.function.metadata.hoisted = true;
511
+ delegated_event.hoisted = true;
512
+ }
513
+
514
+ if (attr.metadata === undefined) {
515
+ attr.metadata = {};
516
+ }
517
+
518
+ attr.metadata.delegated = delegated_event;
519
+ }
520
+ } else {
521
+ visit(attr.value, state);
522
+ }
523
+ }
524
+ } else if (attr.type === 'AccessorAttribute') {
525
+ error(
526
+ 'Accessor props are not supported on DOM elements',
527
+ state.analysis.module.filename,
528
+ attr,
529
+ );
530
+ }
531
+ }
532
+
533
+ if (is_void && node.children.length > 0) {
534
+ error(
535
+ `The <${node.id.name}> element is a void element and cannot have children`,
536
+ state.analysis.module.filename,
537
+ node,
538
+ );
539
+ }
540
+ } else {
541
+ for (const attr of node.attributes) {
542
+ if (attr.type === 'Attribute') {
543
+ if (attr.name.type === 'Identifier') {
544
+ attribute_names.add(attr.name);
545
+ }
546
+ visit(attr.value, state);
547
+ } else if (attr.type === 'AccessorAttribute') {
548
+ attribute_names.add(attr.name);
549
+ visit(attr.get, state);
550
+ if (attr.set) {
551
+ visit(attr.set, state);
552
+ }
553
+ } else if (attr.type === 'SpreadAttribute') {
554
+ visit(attr.argument, state);
555
+ } else if (attr.type === 'RefAttribute') {
556
+ visit(attr.argument, state);
557
+ }
558
+ }
559
+ let implicit_children = false;
560
+ let explicit_children = false;
561
+
562
+ for (const child of node.children) {
563
+ if (child.type === 'Component') {
564
+ if (child.id.name === 'children') {
565
+ explicit_children = true;
566
+ if (implicit_children) {
567
+ error(
568
+ 'Cannot have both implicit and explicit children',
569
+ state.analysis.module.filename,
570
+ node,
571
+ );
572
+ }
573
+ }
574
+ } else if (child.type !== 'EmptyStatement') {
575
+ implicit_children = true;
576
+ if (explicit_children) {
577
+ error(
578
+ 'Cannot have both implicit and explicit children',
579
+ state.analysis.module.filename,
580
+ node,
581
+ );
582
+ }
583
+ }
584
+ }
585
+ }
586
+
587
+ // Validation
588
+ for (const attribute of attribute_names) {
589
+ const name = attribute.name;
590
+ if (name === 'children') {
591
+ if (is_dom_element) {
592
+ error(
593
+ 'Cannot have a `children` prop on an element',
594
+ state.analysis.module.filename,
595
+ attribute,
596
+ );
597
+ } else {
598
+ error(
599
+ 'Cannot have a `children` prop on a component, did you mean `$children`?',
600
+ state.analysis.module.filename,
601
+ attribute,
602
+ );
603
+ }
604
+ }
605
+
606
+ if (is_tracked_name(name)) {
607
+ attribute_names.forEach((n) => {
608
+ if (n.name.slice(1) === name) {
609
+ error(
610
+ `Cannot have both ${name} and ${name.slice(1)} on the same element`,
611
+ state.analysis.module.filename,
612
+ n,
613
+ );
614
+ }
615
+ });
616
+ }
617
+ }
618
+
619
+ return {
620
+ ...node,
621
+ children: node.children.map((child) => visit(child)),
622
+ };
623
+ },
624
+
625
+ Text(node, context) {
626
+ mark_for_loop_has_template(context.path);
627
+ context.next();
628
+ },
629
+
630
+ AwaitExpression(node, context) {
631
+ if (is_inside_component(context)) {
632
+ if (context.state.metadata?.await === false) {
633
+ context.state.metadata.await = true;
634
+ }
635
+ }
636
+
637
+ context.next();
638
+ },
666
639
  };
667
640
 
668
641
  export function analyze(ast, filename) {
669
- const scope_root = new ScopeRoot();
670
-
671
- const { scope, scopes } = create_scopes(ast, scope_root, null);
672
-
673
- const analysis = {
674
- module: { ast, scope, scopes, filename },
675
- ast,
676
- scope,
677
- scopes,
678
- };
679
-
680
- walk(
681
- ast,
682
- {
683
- scope,
684
- scopes,
685
- analysis,
686
- },
687
- visitors,
688
- );
689
-
690
- return analysis;
642
+ const scope_root = new ScopeRoot();
643
+
644
+ const { scope, scopes } = create_scopes(ast, scope_root, null);
645
+
646
+ const analysis = {
647
+ module: { ast, scope, scopes, filename },
648
+ ast,
649
+ scope,
650
+ scopes,
651
+ };
652
+
653
+ walk(
654
+ ast,
655
+ {
656
+ scope,
657
+ scopes,
658
+ analysis,
659
+ },
660
+ visitors,
661
+ );
662
+
663
+ return analysis;
691
664
  }