ripple 0.2.83 → 0.2.85

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.
@@ -2,11 +2,12 @@ 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_element_dom_element,
7
- is_inside_component,
8
- is_ripple_track_call,
9
- is_void_element,
5
+ get_delegated_event,
6
+ is_element_dom_element,
7
+ is_inside_component,
8
+ is_ripple_track_call,
9
+ is_void_element,
10
+ normalize_children,
10
11
  } from '../../utils.js';
11
12
  import { extract_paths } from '../../../utils/ast.js';
12
13
  import is_reference from 'is-reference';
@@ -14,535 +15,572 @@ import { prune_css } from './prune.js';
14
15
  import { error } from '../../errors.js';
15
16
  import { is_event_attribute } from '../../../utils/events.js';
16
17
 
18
+ const valid_in_head = new Set(['title', 'base', 'link', 'meta', 'style', 'script', 'noscript']);
19
+
17
20
  function mark_control_flow_has_template(path) {
18
- for (let i = path.length - 1; i >= 0; i -= 1) {
19
- const node = path[i];
20
-
21
- if (
22
- node.type === 'Component' ||
23
- node.type === 'FunctionExpression' ||
24
- node.type === 'ArrowFunctionExpression' ||
25
- node.type === 'FunctionDeclaration'
26
- ) {
27
- break;
28
- }
29
- if (
30
- node.type === 'ForStatement' ||
31
- node.type === 'ForInStatement' ||
32
- node.type === 'ForOfStatement' ||
33
- node.type === 'TryStatement' ||
34
- node.type === 'IfStatement'
35
- ) {
36
- node.metadata.has_template = true;
37
- }
38
- }
21
+ for (let i = path.length - 1; i >= 0; i -= 1) {
22
+ const node = path[i];
23
+
24
+ if (
25
+ node.type === 'Component' ||
26
+ node.type === 'FunctionExpression' ||
27
+ node.type === 'ArrowFunctionExpression' ||
28
+ node.type === 'FunctionDeclaration'
29
+ ) {
30
+ break;
31
+ }
32
+ if (
33
+ node.type === 'ForStatement' ||
34
+ node.type === 'ForInStatement' ||
35
+ node.type === 'ForOfStatement' ||
36
+ node.type === 'TryStatement' ||
37
+ node.type === 'IfStatement'
38
+ ) {
39
+ node.metadata.has_template = true;
40
+ }
41
+ }
39
42
  }
40
43
 
41
44
  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
- context.next({
50
- ...context.state,
51
- function_depth: context.state.function_depth + 1,
52
- expression: null,
53
- });
54
-
55
- if (node.metadata.tracked) {
56
- mark_as_tracked(context.path);
57
- }
45
+ node.metadata = {
46
+ hoisted: false,
47
+ hoisted_params: [],
48
+ scope: context.state.scope,
49
+ tracked: false,
50
+ };
51
+
52
+ context.next({
53
+ ...context.state,
54
+ function_depth: context.state.function_depth + 1,
55
+ expression: null,
56
+ });
57
+
58
+ if (node.metadata.tracked) {
59
+ mark_as_tracked(context.path);
60
+ }
58
61
  }
59
62
 
60
63
  function mark_as_tracked(path) {
61
- for (let i = path.length - 1; i >= 0; i -= 1) {
62
- const node = path[i];
63
-
64
- if (node.type === 'Component') {
65
- break;
66
- }
67
- if (
68
- node.type === 'FunctionExpression' ||
69
- node.type === 'ArrowFunctionExpression' ||
70
- node.type === 'FunctionDeclaration'
71
- ) {
72
- node.metadata.tracked = true;
73
- break;
74
- }
75
- }
64
+ for (let i = path.length - 1; i >= 0; i -= 1) {
65
+ const node = path[i];
66
+
67
+ if (node.type === 'Component') {
68
+ break;
69
+ }
70
+ if (
71
+ node.type === 'FunctionExpression' ||
72
+ node.type === 'ArrowFunctionExpression' ||
73
+ node.type === 'FunctionDeclaration'
74
+ ) {
75
+ node.metadata.tracked = true;
76
+ break;
77
+ }
78
+ }
76
79
  }
77
80
 
78
81
  const visitors = {
79
- _(node, { state, next, path }) {
80
- // Set up metadata.path for each node (needed for CSS pruning)
81
- if (!node.metadata) {
82
- node.metadata = {};
83
- }
84
- node.metadata.path = [...path];
85
-
86
- const scope = state.scopes.get(node);
87
- next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
88
- },
89
-
90
- Program(_, context) {
91
- return context.next({ ...context.state, function_depth: 0, expression: null });
92
- },
93
-
94
- Identifier(node, context) {
95
- const binding = context.state.scope.get(node.name);
96
- const parent = context.path.at(-1);
97
-
98
- if (binding?.kind === 'prop' || binding?.kind === 'prop_fallback') {
99
- mark_as_tracked(context.path);
100
- if (context.state.metadata?.tracking === false) {
101
- context.state.metadata.tracking = true;
102
- }
103
- }
104
-
105
- if (
106
- is_reference(node, /** @type {Node} */ (parent)) &&
107
- node.tracked &&
108
- binding?.node !== node
109
- ) {
110
- mark_as_tracked(context.path);
111
- if (context.state.metadata?.tracking === false) {
112
- context.state.metadata.tracking = true;
113
- }
114
- }
115
-
116
- if (
117
- is_reference(node, /** @type {Node} */ (parent)) &&
118
- node.tracked &&
119
- binding?.node !== node
120
- ) {
121
- if (context.state.metadata?.tracking === false) {
122
- context.state.metadata.tracking = true;
123
- }
124
- }
125
-
126
- context.next();
127
- },
128
-
129
- MemberExpression(node, context) {
130
- const parent = context.path.at(-1);
131
-
132
- if (context.state.metadata?.tracking === false && parent.type !== 'AssignmentExpression') {
133
- context.state.metadata.tracking = true;
134
- }
135
-
136
- context.next();
137
- },
138
-
139
- CallExpression(node, context) {
140
- const callee = node.callee;
141
-
142
- if (context.state.function_depth === 0 && is_ripple_track_call(callee, context)) {
143
- error(
144
- '`track` can only be used within a reactive context, such as a component, function or class that is used or created from a component',
145
- context.state.analysis.module.filename,
146
- node,
147
- );
148
- }
149
-
150
- if (context.state.metadata?.tracking === false) {
151
- context.state.metadata.tracking = true;
152
- }
153
-
154
- if (!is_inside_component(context, true)) {
155
- mark_as_tracked(context.path);
156
- }
157
-
158
- context.next();
159
- },
160
-
161
- VariableDeclaration(node, context) {
162
- const { state, visit } = context;
163
-
164
- for (const declarator of node.declarations) {
165
- if (is_inside_component(context) && node.kind === 'var') {
166
- error(
167
- '`var` declarations are not allowed in components, use let or const instead',
168
- state.analysis.module.filename,
169
- declarator,
170
- );
171
- }
172
- const metadata = { tracking: false, await: false };
173
-
174
- if (declarator.id.type === 'Identifier') {
175
- visit(declarator, state);
176
- } else {
177
- const paths = extract_paths(declarator.id);
178
-
179
- for (const path of paths) {
180
- if (path.node.tracked) {
181
- error(
182
- 'Variables cannot be reactively referenced using @',
183
- state.analysis.module.filename,
184
- path.node,
185
- );
186
- }
187
- }
188
-
189
- visit(declarator, state);
190
- }
191
-
192
- declarator.metadata = metadata;
193
- }
194
- },
195
-
196
- ArrowFunctionExpression(node, context) {
197
- visit_function(node, context);
198
- },
199
- FunctionExpression(node, context) {
200
- visit_function(node, context);
201
- },
202
- FunctionDeclaration(node, context) {
203
- visit_function(node, context);
204
- },
205
-
206
- Component(node, context) {
207
- context.state.component = node;
208
-
209
- if (node.params.length > 0) {
210
- const props = node.params[0];
211
-
212
- if (props.type === 'ObjectPattern') {
213
- const paths = extract_paths(props);
214
-
215
- for (const path of paths) {
216
- const name = path.node.name;
217
- const binding = context.state.scope.get(name);
218
-
219
- if (binding !== null) {
220
- binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
221
-
222
- binding.transform = {
223
- read: (_) => {
224
- return path.expression(b.id('__props'));
225
- },
226
- assign: (node, value) => {
227
- return b.assignment('=', path.expression(b.id('__props')), value);
228
- },
229
- update: (node) =>
230
- b.update(node.operator, path.expression(b.id('__props')), node.prefix),
231
- };
232
- }
233
- }
234
- } else if (props.type === 'AssignmentPattern') {
235
- error(
236
- 'Props are always an object, use destructured props with default values instead',
237
- context.state.analysis.module.filename,
238
- props,
239
- );
240
- }
241
- }
242
- const elements = [];
243
-
244
- context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1 });
245
-
246
- const css = node.css;
247
-
248
- if (css !== null) {
249
- for (const node of elements) {
250
- prune_css(css, node);
251
- }
252
- }
253
- },
254
-
255
- ForStatement(node, context) {
256
- if (is_inside_component(context)) {
257
- error(
258
- 'For loops are not supported in components. Use for...of instead.',
259
- context.state.analysis.module.filename,
260
- node,
261
- );
262
- }
263
-
264
- context.next();
265
- },
266
-
267
- ForOfStatement(node, context) {
268
- if (!is_inside_component(context)) {
269
- return context.next();
270
- }
271
-
272
- if (node.index) {
273
- const state = context.state;
274
- const scope = state.scopes.get(node);
275
- const binding = scope.get(node.index.name);
276
- binding.kind = 'index'
277
-
278
- if (binding !== null) {
279
- binding.transform = {
280
- read: (node) => {
281
- return b.call('_$_.get', node)
282
- },
283
- };
284
- }
285
- }
286
-
287
- node.metadata = {
288
- has_template: false,
289
- };
290
- context.next();
291
-
292
- if (!node.metadata.has_template) {
293
- error(
294
- 'Component for...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
295
- context.state.analysis.module.filename,
296
- node,
297
- );
298
- }
299
- },
300
-
301
- IfStatement(node, context) {
302
- if (!is_inside_component(context)) {
303
- return context.next();
304
- }
305
-
306
- node.metadata = {
307
- has_template: false,
308
- };
309
-
310
- context.visit(node.consequent, context.state);
311
-
312
- if (!node.metadata.has_template) {
313
- error(
314
- 'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
315
- context.state.analysis.module.filename,
316
- node,
317
- );
318
- }
319
-
320
- if (node.alternate) {
321
- node.metadata = {
322
- has_template: false,
323
- };
324
- context.visit(node.alternate, context.state);
325
-
326
- if (!node.metadata.has_template) {
327
- error(
328
- 'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
329
- context.state.analysis.module.filename,
330
- node,
331
- );
332
- }
333
- }
334
- },
335
-
336
- TryStatement(node, context) {
337
- if (!is_inside_component(context)) {
338
- return context.next();
339
- }
340
-
341
- if (node.pending) {
342
- node.metadata = {
343
- has_template: false,
344
- };
345
-
346
- context.visit(node.block, context.state);
347
-
348
- if (!node.metadata.has_template) {
349
- error(
350
- 'Component try statements must contain a template in their main body. Move the try statement into an effect if it does not render anything.',
351
- context.state.analysis.module.filename,
352
- node,
353
- );
354
- }
355
-
356
- node.metadata = {
357
- has_template: false,
358
- };
359
-
360
- context.visit(node.pending, context.state);
361
-
362
- if (!node.metadata.has_template) {
363
- error(
364
- 'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
365
- context.state.analysis.module.filename,
366
- node,
367
- );
368
- }
369
- }
370
-
371
- if (node.finalizer) {
372
- context.visit(node.finalizer, context.state);
373
- }
374
- },
375
-
376
- ForInStatement(node, context) {
377
- if (is_inside_component(context)) {
378
- error(
379
- 'For...in loops are not supported in components. Use for...of instead.',
380
- context.state.analysis.module.filename,
381
- node,
382
- );
383
- }
384
-
385
- context.next();
386
- },
387
-
388
- JSXElement(node, context) {
389
- {
390
- error(
391
- 'Elements cannot be used as generic expressions, only as statements within a component',
392
- context.state.analysis.module.filename,
393
- node,
394
- );
395
- }
396
- },
397
-
398
- Element(node, context) {
399
- const { state, visit, path } = context;
400
- const is_dom_element = is_element_dom_element(node);
401
- const attribute_names = new Set();
402
-
403
- mark_control_flow_has_template(path);
404
-
405
- if (is_dom_element) {
406
- const is_void = is_void_element(node.id.name);
407
-
408
- if (state.elements) {
409
- state.elements.push(node);
410
- }
411
-
412
- for (const attr of node.attributes) {
413
- if (attr.type === 'Attribute') {
414
- if (attr.name.type === 'Identifier') {
415
- attribute_names.add(attr.name);
416
-
417
- if (is_event_attribute(attr.name.name)) {
418
- const event_name = attr.name.name.slice(2).toLowerCase();
419
- const handler = visit(attr.value, state);
420
- const delegated_event = get_delegated_event(event_name, handler, state);
421
-
422
- if (delegated_event !== null) {
423
- if (delegated_event.hoisted) {
424
- delegated_event.function.metadata.hoisted = true;
425
- delegated_event.hoisted = true;
426
- }
427
-
428
- if (attr.metadata === undefined) {
429
- attr.metadata = {};
430
- }
431
-
432
- attr.metadata.delegated = delegated_event;
433
- }
434
- } else if (attr.value !== null) {
435
- visit(attr.value, state);
436
- }
437
- }
438
- }
439
- }
440
-
441
- if (is_void && node.children.length > 0) {
442
- error(
443
- `The <${node.id.name}> element is a void element and cannot have children`,
444
- state.analysis.module.filename,
445
- node,
446
- );
447
- }
448
- } else {
449
- for (const attr of node.attributes) {
450
- if (attr.type === 'Attribute') {
451
- if (attr.name.type === 'Identifier') {
452
- attribute_names.add(attr.name);
453
- }
454
- visit(attr.value, state);
455
- } else if (attr.type === 'SpreadAttribute') {
456
- visit(attr.argument, state);
457
- } else if (attr.type === 'RefAttribute') {
458
- visit(attr.argument, state);
459
- }
460
- }
461
- let implicit_children = false;
462
- let explicit_children = false;
463
-
464
- for (const child of node.children) {
465
- if (child.type === 'Component') {
466
- if (child.id.name === 'children') {
467
- explicit_children = true;
468
- if (implicit_children) {
469
- error(
470
- 'Cannot have both implicit and explicit children',
471
- state.analysis.module.filename,
472
- node,
473
- );
474
- }
475
- }
476
- } else if (child.type !== 'EmptyStatement') {
477
- implicit_children = true;
478
- if (explicit_children) {
479
- error(
480
- 'Cannot have both implicit and explicit children',
481
- state.analysis.module.filename,
482
- node,
483
- );
484
- }
485
- }
486
- }
487
- }
488
-
489
- // Validation
490
- for (const attribute of attribute_names) {
491
- const name = attribute.name;
492
- if (name === 'children') {
493
- if (is_dom_element) {
494
- error(
495
- 'Cannot have a `children` prop on an element',
496
- state.analysis.module.filename,
497
- attribute,
498
- );
499
- }
500
- }
501
- }
502
-
503
- return {
504
- ...node,
505
- children: node.children.map((child) => visit(child)),
506
- };
507
- },
508
-
509
- Text(node, context) {
510
- mark_control_flow_has_template(context.path);
511
- context.next();
512
- },
513
-
514
- AwaitExpression(node, context) {
515
- if (is_inside_component(context)) {
516
- if (context.state.metadata?.await === false) {
517
- context.state.metadata.await = true;
518
- }
519
- }
520
-
521
- context.next();
522
- },
82
+ _(node, { state, next, path }) {
83
+ // Set up metadata.path for each node (needed for CSS pruning)
84
+ if (!node.metadata) {
85
+ node.metadata = {};
86
+ }
87
+ node.metadata.path = [...path];
88
+
89
+ const scope = state.scopes.get(node);
90
+ next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
91
+ },
92
+
93
+ Program(_, context) {
94
+ return context.next({ ...context.state, function_depth: 0, expression: null });
95
+ },
96
+
97
+ Identifier(node, context) {
98
+ const binding = context.state.scope.get(node.name);
99
+ const parent = context.path.at(-1);
100
+
101
+ if (binding?.kind === 'prop' || binding?.kind === 'prop_fallback') {
102
+ mark_as_tracked(context.path);
103
+ if (context.state.metadata?.tracking === false) {
104
+ context.state.metadata.tracking = true;
105
+ }
106
+ }
107
+
108
+ if (
109
+ is_reference(node, /** @type {Node} */ (parent)) &&
110
+ node.tracked &&
111
+ binding?.node !== node
112
+ ) {
113
+ mark_as_tracked(context.path);
114
+ if (context.state.metadata?.tracking === false) {
115
+ context.state.metadata.tracking = true;
116
+ }
117
+ }
118
+
119
+ if (
120
+ is_reference(node, /** @type {Node} */ (parent)) &&
121
+ node.tracked &&
122
+ binding?.node !== node
123
+ ) {
124
+ if (context.state.metadata?.tracking === false) {
125
+ context.state.metadata.tracking = true;
126
+ }
127
+ }
128
+
129
+ context.next();
130
+ },
131
+
132
+ MemberExpression(node, context) {
133
+ const parent = context.path.at(-1);
134
+
135
+ if (context.state.metadata?.tracking === false && parent.type !== 'AssignmentExpression') {
136
+ context.state.metadata.tracking = true;
137
+ }
138
+
139
+ context.next();
140
+ },
141
+
142
+ CallExpression(node, context) {
143
+ const callee = node.callee;
144
+
145
+ if (context.state.function_depth === 0 && is_ripple_track_call(callee, context)) {
146
+ error(
147
+ '`track` can only be used within a reactive context, such as a component, function or class that is used or created from a component',
148
+ context.state.analysis.module.filename,
149
+ node,
150
+ );
151
+ }
152
+
153
+ if (context.state.metadata?.tracking === false) {
154
+ context.state.metadata.tracking = true;
155
+ }
156
+
157
+ if (!is_inside_component(context, true)) {
158
+ mark_as_tracked(context.path);
159
+ }
160
+
161
+ context.next();
162
+ },
163
+
164
+ VariableDeclaration(node, context) {
165
+ const { state, visit } = context;
166
+
167
+ for (const declarator of node.declarations) {
168
+ if (is_inside_component(context) && node.kind === 'var') {
169
+ error(
170
+ '`var` declarations are not allowed in components, use let or const instead',
171
+ state.analysis.module.filename,
172
+ declarator,
173
+ );
174
+ }
175
+ const metadata = { tracking: false, await: false };
176
+
177
+ if (declarator.id.type === 'Identifier') {
178
+ visit(declarator, state);
179
+ } else {
180
+ const paths = extract_paths(declarator.id);
181
+
182
+ for (const path of paths) {
183
+ if (path.node.tracked) {
184
+ error(
185
+ 'Variables cannot be reactively referenced using @',
186
+ state.analysis.module.filename,
187
+ path.node,
188
+ );
189
+ }
190
+ }
191
+
192
+ visit(declarator, state);
193
+ }
194
+
195
+ declarator.metadata = metadata;
196
+ }
197
+ },
198
+
199
+ ArrowFunctionExpression(node, context) {
200
+ visit_function(node, context);
201
+ },
202
+ FunctionExpression(node, context) {
203
+ visit_function(node, context);
204
+ },
205
+ FunctionDeclaration(node, context) {
206
+ visit_function(node, context);
207
+ },
208
+
209
+ Component(node, context) {
210
+ context.state.component = node;
211
+
212
+ if (node.params.length > 0) {
213
+ const props = node.params[0];
214
+
215
+ if (props.type === 'ObjectPattern') {
216
+ const paths = extract_paths(props);
217
+
218
+ for (const path of paths) {
219
+ const name = path.node.name;
220
+ const binding = context.state.scope.get(name);
221
+
222
+ if (binding !== null) {
223
+ binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
224
+
225
+ binding.transform = {
226
+ read: (_) => {
227
+ return path.expression(b.id('__props'));
228
+ },
229
+ assign: (node, value) => {
230
+ return b.assignment('=', path.expression(b.id('__props')), value);
231
+ },
232
+ update: (node) =>
233
+ b.update(node.operator, path.expression(b.id('__props')), node.prefix),
234
+ };
235
+ }
236
+ }
237
+ } else if (props.type === 'AssignmentPattern') {
238
+ error(
239
+ 'Props are always an object, use destructured props with default values instead',
240
+ context.state.analysis.module.filename,
241
+ props,
242
+ );
243
+ }
244
+ }
245
+ const elements = [];
246
+
247
+ context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1 });
248
+
249
+ const css = node.css;
250
+
251
+ if (css !== null) {
252
+ for (const node of elements) {
253
+ prune_css(css, node);
254
+ }
255
+ }
256
+ },
257
+
258
+ ForStatement(node, context) {
259
+ if (is_inside_component(context)) {
260
+ error(
261
+ 'For loops are not supported in components. Use for...of instead.',
262
+ context.state.analysis.module.filename,
263
+ node,
264
+ );
265
+ }
266
+
267
+ context.next();
268
+ },
269
+
270
+ ForOfStatement(node, context) {
271
+ if (!is_inside_component(context)) {
272
+ return context.next();
273
+ }
274
+
275
+ if (node.index) {
276
+ const state = context.state;
277
+ const scope = state.scopes.get(node);
278
+ const binding = scope.get(node.index.name);
279
+ binding.kind = 'index';
280
+
281
+ if (binding !== null) {
282
+ binding.transform = {
283
+ read: (node) => {
284
+ return b.call('_$_.get', node);
285
+ },
286
+ };
287
+ }
288
+ }
289
+
290
+ node.metadata = {
291
+ has_template: false,
292
+ };
293
+ context.next();
294
+
295
+ if (!node.metadata.has_template) {
296
+ error(
297
+ 'Component for...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
298
+ context.state.analysis.module.filename,
299
+ node,
300
+ );
301
+ }
302
+ },
303
+
304
+ IfStatement(node, context) {
305
+ if (!is_inside_component(context)) {
306
+ return context.next();
307
+ }
308
+
309
+ node.metadata = {
310
+ has_template: false,
311
+ };
312
+
313
+ context.visit(node.consequent, context.state);
314
+
315
+ if (!node.metadata.has_template) {
316
+ error(
317
+ 'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
318
+ context.state.analysis.module.filename,
319
+ node,
320
+ );
321
+ }
322
+
323
+ if (node.alternate) {
324
+ node.metadata = {
325
+ has_template: false,
326
+ };
327
+ context.visit(node.alternate, context.state);
328
+
329
+ if (!node.metadata.has_template) {
330
+ error(
331
+ 'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
332
+ context.state.analysis.module.filename,
333
+ node,
334
+ );
335
+ }
336
+ }
337
+ },
338
+
339
+ TryStatement(node, context) {
340
+ if (!is_inside_component(context)) {
341
+ return context.next();
342
+ }
343
+
344
+ if (node.pending) {
345
+ node.metadata = {
346
+ has_template: false,
347
+ };
348
+
349
+ context.visit(node.block, context.state);
350
+
351
+ if (!node.metadata.has_template) {
352
+ error(
353
+ 'Component try statements must contain a template in their main body. Move the try statement into an effect if it does not render anything.',
354
+ context.state.analysis.module.filename,
355
+ node,
356
+ );
357
+ }
358
+
359
+ node.metadata = {
360
+ has_template: false,
361
+ };
362
+
363
+ context.visit(node.pending, context.state);
364
+
365
+ if (!node.metadata.has_template) {
366
+ error(
367
+ 'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
368
+ context.state.analysis.module.filename,
369
+ node,
370
+ );
371
+ }
372
+ }
373
+
374
+ if (node.finalizer) {
375
+ context.visit(node.finalizer, context.state);
376
+ }
377
+ },
378
+
379
+ ForInStatement(node, context) {
380
+ if (is_inside_component(context)) {
381
+ error(
382
+ 'For...in loops are not supported in components. Use for...of instead.',
383
+ context.state.analysis.module.filename,
384
+ node,
385
+ );
386
+ }
387
+
388
+ context.next();
389
+ },
390
+
391
+ JSXElement(node, context) {
392
+ {
393
+ error(
394
+ 'Elements cannot be used as generic expressions, only as statements within a component',
395
+ context.state.analysis.module.filename,
396
+ node,
397
+ );
398
+ }
399
+ },
400
+
401
+ Element(node, context) {
402
+ const { state, visit, path } = context;
403
+ const is_dom_element = is_element_dom_element(node);
404
+ const attribute_names = new Set();
405
+
406
+ mark_control_flow_has_template(path);
407
+
408
+ if (is_dom_element) {
409
+ if (node.id.name === 'head') {
410
+ // head validation
411
+ if (node.attributes.length > 0) {
412
+ error('<head> cannot have any attributes', state.analysis.module.filename, node);
413
+ }
414
+ if (node.children.length === 0) {
415
+ error('<head> must have children', state.analysis.module.filename, node);
416
+ }
417
+
418
+ for (const child of node.children) {
419
+ context.visit(child, { ...state, inside_head: true });
420
+ }
421
+
422
+ return;
423
+ }
424
+ if (state.inside_head) {
425
+ if (node.id.name === 'title') {
426
+ const chiildren = normalize_children(node.children);
427
+
428
+ if (chiildren.length !== 1 || chiildren[0].type !== 'Text') {
429
+ error(
430
+ '<title> must have only contain text nodes',
431
+ state.analysis.module.filename,
432
+ node,
433
+ );
434
+ }
435
+ }
436
+
437
+ // check for invalid elements in head
438
+ if (!valid_in_head.has(node.id.name)) {
439
+ error(`<${node.id.name}> cannot be used in <head>`, state.analysis.module.filename, node);
440
+ }
441
+ }
442
+
443
+ const is_void = is_void_element(node.id.name);
444
+
445
+ if (state.elements) {
446
+ state.elements.push(node);
447
+ }
448
+
449
+ for (const attr of node.attributes) {
450
+ if (attr.type === 'Attribute') {
451
+ if (attr.name.type === 'Identifier') {
452
+ attribute_names.add(attr.name);
453
+
454
+ if (is_event_attribute(attr.name.name)) {
455
+ const event_name = attr.name.name.slice(2).toLowerCase();
456
+ const handler = visit(attr.value, state);
457
+ const delegated_event = get_delegated_event(event_name, handler, state);
458
+
459
+ if (delegated_event !== null) {
460
+ if (delegated_event.hoisted) {
461
+ delegated_event.function.metadata.hoisted = true;
462
+ delegated_event.hoisted = true;
463
+ }
464
+
465
+ if (attr.metadata === undefined) {
466
+ attr.metadata = {};
467
+ }
468
+
469
+ attr.metadata.delegated = delegated_event;
470
+ }
471
+ } else if (attr.value !== null) {
472
+ visit(attr.value, state);
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ if (is_void && node.children.length > 0) {
479
+ error(
480
+ `The <${node.id.name}> element is a void element and cannot have children`,
481
+ state.analysis.module.filename,
482
+ node,
483
+ );
484
+ }
485
+ } else {
486
+ for (const attr of node.attributes) {
487
+ if (attr.type === 'Attribute') {
488
+ if (attr.name.type === 'Identifier') {
489
+ attribute_names.add(attr.name);
490
+ }
491
+ visit(attr.value, state);
492
+ } else if (attr.type === 'SpreadAttribute') {
493
+ visit(attr.argument, state);
494
+ } else if (attr.type === 'RefAttribute') {
495
+ visit(attr.argument, state);
496
+ }
497
+ }
498
+ let implicit_children = false;
499
+ let explicit_children = false;
500
+
501
+ for (const child of node.children) {
502
+ if (child.type === 'Component') {
503
+ if (child.id.name === 'children') {
504
+ explicit_children = true;
505
+ if (implicit_children) {
506
+ error(
507
+ 'Cannot have both implicit and explicit children',
508
+ state.analysis.module.filename,
509
+ node,
510
+ );
511
+ }
512
+ }
513
+ } else if (child.type !== 'EmptyStatement') {
514
+ implicit_children = true;
515
+ if (explicit_children) {
516
+ error(
517
+ 'Cannot have both implicit and explicit children',
518
+ state.analysis.module.filename,
519
+ node,
520
+ );
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ // Validation
527
+ for (const attribute of attribute_names) {
528
+ const name = attribute.name;
529
+ if (name === 'children') {
530
+ if (is_dom_element) {
531
+ error(
532
+ 'Cannot have a `children` prop on an element',
533
+ state.analysis.module.filename,
534
+ attribute,
535
+ );
536
+ }
537
+ }
538
+ }
539
+
540
+ return {
541
+ ...node,
542
+ children: node.children.map((child) => visit(child)),
543
+ };
544
+ },
545
+
546
+ Text(node, context) {
547
+ mark_control_flow_has_template(context.path);
548
+ context.next();
549
+ },
550
+
551
+ AwaitExpression(node, context) {
552
+ if (is_inside_component(context)) {
553
+ if (context.state.metadata?.await === false) {
554
+ context.state.metadata.await = true;
555
+ }
556
+ }
557
+
558
+ context.next();
559
+ },
523
560
  };
524
561
 
525
562
  export function analyze(ast, filename) {
526
- const scope_root = new ScopeRoot();
527
-
528
- const { scope, scopes } = create_scopes(ast, scope_root, null);
529
-
530
- const analysis = {
531
- module: { ast, scope, scopes, filename },
532
- ast,
533
- scope,
534
- scopes,
535
- };
536
-
537
- walk(
538
- ast,
539
- {
540
- scope,
541
- scopes,
542
- analysis,
543
- },
544
- visitors,
545
- );
546
-
547
- return analysis;
563
+ const scope_root = new ScopeRoot();
564
+
565
+ const { scope, scopes } = create_scopes(ast, scope_root, null);
566
+
567
+ const analysis = {
568
+ module: { ast, scope, scopes, filename },
569
+ ast,
570
+ scope,
571
+ scopes,
572
+ };
573
+
574
+ walk(
575
+ ast,
576
+ {
577
+ scope,
578
+ scopes,
579
+ analysis,
580
+ inside_head: false,
581
+ },
582
+ visitors,
583
+ );
584
+
585
+ return analysis;
548
586
  }