ripple 0.3.13 → 0.3.14
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.
- package/CHANGELOG.md +21 -0
- package/package.json +5 -30
- package/src/runtime/array.js +38 -38
- package/src/runtime/create-subscriber.js +2 -2
- package/src/runtime/internal/client/bindings.js +4 -6
- package/src/runtime/internal/client/events.js +8 -3
- package/src/runtime/internal/client/hmr.js +5 -17
- package/src/runtime/internal/client/runtime.js +1 -0
- package/src/runtime/internal/server/blocks.js +7 -9
- package/src/runtime/internal/server/index.js +14 -22
- package/src/runtime/media-query.js +34 -33
- package/src/runtime/object.js +7 -10
- package/src/runtime/proxy.js +2 -3
- package/src/runtime/reactive-value.js +23 -21
- package/src/utils/ast.js +1 -1
- package/src/utils/attributes.js +43 -0
- package/src/utils/builders.js +2 -2
- package/tests/client/basic/basic.errors.test.rsrx +1 -1
- package/tests/client/basic/basic.styling.test.rsrx +1 -1
- package/tests/client/compiler/compiler.assignments.test.rsrx +1 -1
- package/tests/client/compiler/compiler.attributes.test.rsrx +1 -1
- package/tests/client/compiler/compiler.basic.test.rsrx +13 -13
- package/tests/client/compiler/compiler.tracked-access.test.rsrx +1 -1
- package/tests/client/compiler/compiler.try-in-function.test.rsrx +1 -1
- package/tests/client/compiler/compiler.typescript.test.rsrx +1 -1
- package/tests/client/css/global-additional-cases.test.rsrx +1 -1
- package/tests/client/css/global-advanced-selectors.test.rsrx +1 -1
- package/tests/client/css/global-at-rules.test.rsrx +1 -1
- package/tests/client/css/global-basic.test.rsrx +1 -1
- package/tests/client/css/global-classes-ids.test.rsrx +1 -1
- package/tests/client/css/global-combinators.test.rsrx +1 -1
- package/tests/client/css/global-complex-nesting.test.rsrx +1 -1
- package/tests/client/css/global-edge-cases.test.rsrx +1 -1
- package/tests/client/css/global-keyframes.test.rsrx +1 -1
- package/tests/client/css/global-nested.test.rsrx +1 -1
- package/tests/client/css/global-pseudo.test.rsrx +1 -1
- package/tests/client/css/global-scoping.test.rsrx +1 -1
- package/tests/client/css/style-identifier.test.rsrx +1 -1
- package/tests/client/return.test.rsrx +1 -1
- package/tests/hydration/build-components.js +1 -1
- package/tests/server/style-identifier.test.rsrx +1 -1
- package/tests/setup-server.js +1 -1
- package/tests/utils/compiler-compat-config.test.js +1 -1
- package/src/compiler/comment-utils.js +0 -91
- package/src/compiler/errors.js +0 -77
- package/src/compiler/identifier-utils.js +0 -80
- package/src/compiler/index.d.ts +0 -127
- package/src/compiler/index.js +0 -89
- package/src/compiler/phases/1-parse/index.js +0 -3007
- package/src/compiler/phases/1-parse/style.js +0 -704
- package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
- package/src/compiler/phases/2-analyze/index.js +0 -2208
- package/src/compiler/phases/2-analyze/prune.js +0 -1131
- package/src/compiler/phases/2-analyze/validation.js +0 -168
- package/src/compiler/phases/3-transform/client/index.js +0 -5264
- package/src/compiler/phases/3-transform/segments.js +0 -2125
- package/src/compiler/phases/3-transform/server/index.js +0 -1749
- package/src/compiler/phases/3-transform/stylesheet.js +0 -545
- package/src/compiler/scope.js +0 -476
- package/src/compiler/source-map-utils.js +0 -358
- package/src/compiler/types/acorn.d.ts +0 -11
- package/src/compiler/types/estree-jsx.d.ts +0 -11
- package/src/compiler/types/estree.d.ts +0 -11
- package/src/compiler/types/index.d.ts +0 -1411
- package/src/compiler/types/parse.d.ts +0 -1723
- 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
|
-
}
|