@rrjs/babel-plugin 0.1.1 → 0.2.1
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/LICENSE +21 -0
- package/README.md +82 -9
- package/dist/index.d.ts +21 -0
- package/dist/index.js +168 -39
- package/dist/module-contracts.d.ts +32 -0
- package/dist/module-contracts.js +32 -0
- package/dist/run-once.d.ts +4 -0
- package/dist/run-once.js +2004 -0
- package/package.json +25 -8
package/dist/run-once.js
ADDED
|
@@ -0,0 +1,2004 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
// This pass accepts a deliberately checked subset. It never introduces a
|
|
3
|
+
// component render loop, React fallback, or old/new tree comparison.
|
|
4
|
+
export function compileRunOnce(program, derive, choose, rendererHelper, moduleMetadata) {
|
|
5
|
+
const reactive = new Map();
|
|
6
|
+
const generated = new Set();
|
|
7
|
+
const deferredRemovals = [];
|
|
8
|
+
const components = new Set();
|
|
9
|
+
const stateSetters = new Map();
|
|
10
|
+
const operationStates = new Set();
|
|
11
|
+
const mapUpdaters = new Map();
|
|
12
|
+
const importedOperationEdges = [];
|
|
13
|
+
const recordMapUpdater = (state, mapper) => {
|
|
14
|
+
const updaters = mapUpdaters.get(state) ?? [];
|
|
15
|
+
updaters.push(t.cloneNode(mapper, true));
|
|
16
|
+
mapUpdaters.set(state, updaters);
|
|
17
|
+
};
|
|
18
|
+
const mapperPreservesProperty = (mapper, property) => {
|
|
19
|
+
if (!t.isArrowFunctionExpression(mapper) && !t.isFunctionExpression(mapper))
|
|
20
|
+
return false;
|
|
21
|
+
const item = mapper.params[0];
|
|
22
|
+
if (!t.isIdentifier(item))
|
|
23
|
+
return false;
|
|
24
|
+
const preserves = (value) => {
|
|
25
|
+
if (t.isIdentifier(value, { name: item.name }))
|
|
26
|
+
return true;
|
|
27
|
+
if (t.isConditionalExpression(value))
|
|
28
|
+
return preserves(value.consequent) && preserves(value.alternate);
|
|
29
|
+
if (!t.isObjectExpression(value))
|
|
30
|
+
return false;
|
|
31
|
+
let lastIdentityWrite = -1;
|
|
32
|
+
let lastPropertyWrite = -1;
|
|
33
|
+
let unknownSpreadAfterIdentity = false;
|
|
34
|
+
value.properties.forEach((entry, index) => {
|
|
35
|
+
if (t.isSpreadElement(entry)) {
|
|
36
|
+
if (t.isIdentifier(entry.argument, { name: item.name }))
|
|
37
|
+
lastIdentityWrite = index;
|
|
38
|
+
else if (lastIdentityWrite >= 0)
|
|
39
|
+
unknownSpreadAfterIdentity = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!t.isObjectProperty(entry) || entry.computed)
|
|
43
|
+
return;
|
|
44
|
+
const name = t.isIdentifier(entry.key) ? entry.key.name
|
|
45
|
+
: t.isStringLiteral(entry.key) ? entry.key.value : undefined;
|
|
46
|
+
if (name === property)
|
|
47
|
+
lastPropertyWrite = index;
|
|
48
|
+
});
|
|
49
|
+
return lastIdentityWrite >= 0 && lastPropertyWrite < lastIdentityWrite && !unknownSpreadAfterIdentity;
|
|
50
|
+
};
|
|
51
|
+
if (!t.isBlockStatement(mapper.body))
|
|
52
|
+
return preserves(mapper.body);
|
|
53
|
+
const returns = [];
|
|
54
|
+
t.traverseFast(mapper.body, node => { if (t.isReturnStatement(node))
|
|
55
|
+
returns.push(node); });
|
|
56
|
+
return returns.length > 0 && returns.every(statement => preserves(statement.argument));
|
|
57
|
+
};
|
|
58
|
+
// Ordinary React source often writes `function C(props)` and reads
|
|
59
|
+
// `props.title` rather than destructuring in the parameter list. Both forms
|
|
60
|
+
// carry the same information, so normalise the identifier form into the
|
|
61
|
+
// destructured one before any analysis runs. Everything downstream then sees
|
|
62
|
+
// the single shape it already supports, with no new reactive machinery.
|
|
63
|
+
//
|
|
64
|
+
// Only provably simple uses are rewritten. If `props` escapes -- spread,
|
|
65
|
+
// passed as a value, computed access, or written to -- the parameter is left
|
|
66
|
+
// alone so the existing diagnostics still reject it rather than this pass
|
|
67
|
+
// silently guessing.
|
|
68
|
+
program.traverse({
|
|
69
|
+
Function(path) {
|
|
70
|
+
const params = path.get('params');
|
|
71
|
+
const first = params[0];
|
|
72
|
+
if (!first?.isIdentifier())
|
|
73
|
+
return;
|
|
74
|
+
if (params.length > 2)
|
|
75
|
+
return;
|
|
76
|
+
// Only component functions. A capitalised binding name is the boundary
|
|
77
|
+
// React itself uses, and it keeps list mappers such as
|
|
78
|
+
// `item => <li>{item.text}</li>` out of this rewrite -- their parameter
|
|
79
|
+
// is data, not a props object.
|
|
80
|
+
const declared = path.isFunctionDeclaration() ? path.node.id?.name
|
|
81
|
+
: path.parentPath?.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id)
|
|
82
|
+
? path.parentPath.node.id.name : undefined;
|
|
83
|
+
if (!declared || !/^[A-Z]/.test(declared))
|
|
84
|
+
return;
|
|
85
|
+
let hasJsx = false;
|
|
86
|
+
path.traverse({ JSXElement() { hasJsx = true; }, JSXFragment() { hasJsx = true; } });
|
|
87
|
+
if (!hasJsx)
|
|
88
|
+
return;
|
|
89
|
+
const binding = path.scope.getBinding(first.node.name);
|
|
90
|
+
if (!binding || !binding.constant)
|
|
91
|
+
return;
|
|
92
|
+
const reads = [];
|
|
93
|
+
const keys = new Set();
|
|
94
|
+
for (const reference of binding.referencePaths) {
|
|
95
|
+
const parent = reference.parentPath;
|
|
96
|
+
if (!parent?.isMemberExpression() || parent.node.object !== reference.node
|
|
97
|
+
|| parent.node.computed || !t.isIdentifier(parent.node.property))
|
|
98
|
+
return;
|
|
99
|
+
const grand = parent.parentPath;
|
|
100
|
+
if (grand?.isAssignmentExpression() && grand.node.left === parent.node)
|
|
101
|
+
return;
|
|
102
|
+
if (grand?.isUpdateExpression() || grand?.isUnaryExpression({ operator: 'delete' }))
|
|
103
|
+
return;
|
|
104
|
+
reads.push(parent);
|
|
105
|
+
keys.add(parent.node.property.name);
|
|
106
|
+
}
|
|
107
|
+
if (binding.constantViolations.length || keys.size === 0)
|
|
108
|
+
return;
|
|
109
|
+
const locals = new Map();
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const free = !path.scope.hasBinding(key) && !path.scope.parent?.hasBinding(key);
|
|
112
|
+
locals.set(key, t.identifier(free ? key : path.scope.generateUid(key)));
|
|
113
|
+
}
|
|
114
|
+
first.replaceWith(t.objectPattern([...locals].map(([key, local]) => t.objectProperty(t.identifier(key), t.cloneNode(local, true), false, key === local.name))));
|
|
115
|
+
for (const read of reads) {
|
|
116
|
+
read.replaceWith(t.cloneNode(locals.get(read.node.property.name), true));
|
|
117
|
+
}
|
|
118
|
+
path.scope.crawl();
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
// `const rows = items.map(render); return <ul>{rows}</ul>` is the same list as
|
|
122
|
+
// writing the map inline, just named. Naming it must not cost the direct list
|
|
123
|
+
// operations, so inline the single JSX use and let the list machinery see the
|
|
124
|
+
// shape it already compiles. The JSX ban on derived bindings stays: a mapper
|
|
125
|
+
// inside a derive would rebuild every row when a dependency changed, which is
|
|
126
|
+
// the reconciliation this compiler exists to avoid.
|
|
127
|
+
//
|
|
128
|
+
// Only the unambiguous case is inlined -- one reference, used as a JSX child,
|
|
129
|
+
// in the return statement that directly follows the declaration -- so the map
|
|
130
|
+
// still runs exactly where it used to.
|
|
131
|
+
program.traverse({
|
|
132
|
+
VariableDeclarator(path) {
|
|
133
|
+
if (!t.isIdentifier(path.node.id))
|
|
134
|
+
return;
|
|
135
|
+
const init = path.get('init');
|
|
136
|
+
if (!init.isCallExpression())
|
|
137
|
+
return;
|
|
138
|
+
const callee = init.get('callee');
|
|
139
|
+
if (!callee.isMemberExpression() || callee.node.computed
|
|
140
|
+
|| !callee.get('property').isIdentifier({ name: 'map' }))
|
|
141
|
+
return;
|
|
142
|
+
const declaration = path.parentPath;
|
|
143
|
+
if (!declaration.isVariableDeclaration({ kind: 'const' })
|
|
144
|
+
|| declaration.node.declarations.length !== 1)
|
|
145
|
+
return;
|
|
146
|
+
const binding = path.scope.getBinding(path.node.id.name);
|
|
147
|
+
if (!binding || !binding.constant || binding.referencePaths.length !== 1)
|
|
148
|
+
return;
|
|
149
|
+
const reference = binding.referencePaths[0];
|
|
150
|
+
const container = reference.parentPath;
|
|
151
|
+
if (!container?.isJSXExpressionContainer()
|
|
152
|
+
|| container.node.expression !== reference.node
|
|
153
|
+
|| !container.parentPath?.isJSXElement())
|
|
154
|
+
return;
|
|
155
|
+
// The map must still evaluate where it was written.
|
|
156
|
+
const statement = reference.getStatementParent();
|
|
157
|
+
if (!statement?.isReturnStatement())
|
|
158
|
+
return;
|
|
159
|
+
const siblings = declaration.getAllNextSiblings();
|
|
160
|
+
if (siblings.length !== 1 || siblings[0].node !== statement.node)
|
|
161
|
+
return;
|
|
162
|
+
reference.replaceWith(t.cloneNode(init.node, true));
|
|
163
|
+
declaration.remove();
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
// `useReducer` is `useState` with the update spelled as a reducer. React
|
|
167
|
+
// queues dispatches and applies them in order -- two `d(1)` calls in one
|
|
168
|
+
// handler move the count by two, not one -- so the rewrite uses the
|
|
169
|
+
// functional updater form, which has exactly that behaviour here.
|
|
170
|
+
//
|
|
171
|
+
// const [s, d] = useReducer(reducer, init)
|
|
172
|
+
// becomes
|
|
173
|
+
// const [s, _set] = useState(init)
|
|
174
|
+
// const d = action => _set(prev => reducer(prev, action))
|
|
175
|
+
//
|
|
176
|
+
// React's third argument is a lazy initialiser: the initial state is
|
|
177
|
+
// `init(arg)`. Everything downstream then sees ordinary `useState`.
|
|
178
|
+
program.traverse({
|
|
179
|
+
VariableDeclarator(path) {
|
|
180
|
+
const init = path.get('init');
|
|
181
|
+
if (!init.isCallExpression() || hookName(init) !== 'useReducer')
|
|
182
|
+
return;
|
|
183
|
+
const args = init.get('arguments');
|
|
184
|
+
if (args.length < 2 || args.length > 3) {
|
|
185
|
+
throw init.buildCodeFrameError('runOnce: useReducer takes a reducer, an initial value and an optional initialiser');
|
|
186
|
+
}
|
|
187
|
+
const reducer = args[0];
|
|
188
|
+
if (!reducer.isIdentifier() && !reducer.isArrowFunctionExpression() && !reducer.isFunctionExpression()) {
|
|
189
|
+
throw reducer.buildCodeFrameError('runOnce: useReducer requires a named or inline reducer');
|
|
190
|
+
}
|
|
191
|
+
if (!t.isArrayPattern(path.node.id) || !t.isIdentifier(path.node.id.elements[0])
|
|
192
|
+
|| !t.isIdentifier(path.node.id.elements[1])) {
|
|
193
|
+
throw path.buildCodeFrameError('runOnce: useReducer requires a named state and dispatch binding');
|
|
194
|
+
}
|
|
195
|
+
const state = path.node.id.elements[0];
|
|
196
|
+
const dispatch = path.node.id.elements[1];
|
|
197
|
+
const setter = path.scope.generateUidIdentifier('set' + state.name);
|
|
198
|
+
const previous = path.scope.generateUidIdentifier('previous');
|
|
199
|
+
const action = path.scope.generateUidIdentifier('action');
|
|
200
|
+
const initial = args.length === 3
|
|
201
|
+
? t.callExpression(t.cloneNode(args[2].node, true), [t.cloneNode(args[1].node, true)])
|
|
202
|
+
: t.cloneNode(args[1].node, true);
|
|
203
|
+
const updater = t.arrowFunctionExpression([t.cloneNode(previous, true)], t.callExpression(t.cloneNode(reducer.node, true), [t.cloneNode(previous, true), t.cloneNode(action, true)]));
|
|
204
|
+
const dispatchFn = t.arrowFunctionExpression([t.cloneNode(action, true)], t.callExpression(t.cloneNode(setter, true), [updater]));
|
|
205
|
+
const declaration = path.parentPath;
|
|
206
|
+
if (!declaration.isVariableDeclaration()) {
|
|
207
|
+
throw path.buildCodeFrameError('runOnce: useReducer must be a direct const declaration');
|
|
208
|
+
}
|
|
209
|
+
declaration.insertAfter(t.variableDeclaration('const', [
|
|
210
|
+
t.variableDeclarator(t.cloneNode(dispatch, true), dispatchFn),
|
|
211
|
+
]));
|
|
212
|
+
path.node.id = t.arrayPattern([t.cloneNode(state, true), t.cloneNode(setter, true)]);
|
|
213
|
+
init.node.callee = t.identifier('useState');
|
|
214
|
+
init.node.arguments = [initial];
|
|
215
|
+
path.scope.crawl();
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
// `useMemo` and `useCallback` exist in React because the component body runs
|
|
219
|
+
// again on every render: one caches a result across those runs, the other
|
|
220
|
+
// keeps a function's identity stable across them. Neither happens here. The
|
|
221
|
+
// body runs once, so a function written in it is already created once, and a
|
|
222
|
+
// derived expression is already a computation that recomputes only when
|
|
223
|
+
// something it reads changes.
|
|
224
|
+
//
|
|
225
|
+
// So both unwrap to the value they were wrapping, and the rest of the compiler
|
|
226
|
+
// sees ordinary source. The dependency array is dropped: dependencies are
|
|
227
|
+
// tracked from the reads themselves. That differs from React for a list that
|
|
228
|
+
// deliberately understates the reads -- `useMemo(() => x, [])` freezes in
|
|
229
|
+
// React and stays live here -- and that difference is asserted in
|
|
230
|
+
// apps/compat-audit/tests/react-pattern-corpus.test.ts rather than left
|
|
231
|
+
// unstated.
|
|
232
|
+
program.traverse({
|
|
233
|
+
CallExpression(path) {
|
|
234
|
+
const hook = hookName(path);
|
|
235
|
+
if (hook !== 'useMemo' && hook !== 'useCallback')
|
|
236
|
+
return;
|
|
237
|
+
const args = path.get('arguments');
|
|
238
|
+
if (args.length > 2) {
|
|
239
|
+
throw path.buildCodeFrameError(`runOnce: ${hook} takes a function and an optional dependency array`);
|
|
240
|
+
}
|
|
241
|
+
const fn = args[0];
|
|
242
|
+
if (!fn || !(fn.isArrowFunctionExpression() || fn.isFunctionExpression())) {
|
|
243
|
+
throw path.buildCodeFrameError(`runOnce: ${hook} requires an inline function`);
|
|
244
|
+
}
|
|
245
|
+
if (fn.node.async || fn.node.generator || fn.node.params.length > 0) {
|
|
246
|
+
throw path.buildCodeFrameError(`runOnce: ${hook} requires a plain zero-argument function`);
|
|
247
|
+
}
|
|
248
|
+
if (hook === 'useCallback') {
|
|
249
|
+
// The identity is already stable, so the wrapper is the only thing to remove.
|
|
250
|
+
path.replaceWith(fn.node);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (t.isBlockStatement(fn.node.body)) {
|
|
254
|
+
throw path.buildCodeFrameError('runOnce: useMemo requires an expression body in the current scope');
|
|
255
|
+
}
|
|
256
|
+
path.replaceWith(fn.node.body);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
// Import spelling can hide a hook from call-site name checks. Only direct
|
|
260
|
+
// named supported hook calls have established state-binding semantics.
|
|
261
|
+
program.traverse({
|
|
262
|
+
ImportSpecifier(path) {
|
|
263
|
+
const imported = path.node.imported;
|
|
264
|
+
const name = t.isIdentifier(imported) ? imported.name : imported.value;
|
|
265
|
+
if (!/^use[A-Z]/.test(name))
|
|
266
|
+
return;
|
|
267
|
+
const declaration = path.parentPath;
|
|
268
|
+
const supported = declaration.isImportDeclaration()
|
|
269
|
+
&& ['react', '@rrjs/react-compat'].includes(declaration.node.source.value)
|
|
270
|
+
&& ['useState', 'useRef', 'useEffect', 'useContext'].includes(name);
|
|
271
|
+
const binding = path.scope.getBinding(path.node.local.name);
|
|
272
|
+
const analyzed = isImportedReactiveHook(binding);
|
|
273
|
+
for (const reference of binding?.referencePaths ?? []) {
|
|
274
|
+
if (!supported && !analyzed)
|
|
275
|
+
throw reference.buildCodeFrameError('runOnce: imported custom or unsupported hooks require module analysis');
|
|
276
|
+
if (!reference.parentPath?.isCallExpression() || reference.key !== 'callee') {
|
|
277
|
+
throw reference.buildCodeFrameError('runOnce: hook indirection requires binding analysis; call the named import directly');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
// Fold only a tail if-return followed by a return. No component statements
|
|
283
|
+
// are moved into an update callback.
|
|
284
|
+
program.traverse({
|
|
285
|
+
Function(path) {
|
|
286
|
+
if (!t.isBlockStatement(path.node.body))
|
|
287
|
+
return;
|
|
288
|
+
const statements = path.node.body.body;
|
|
289
|
+
while (statements.length >= 2) {
|
|
290
|
+
const last = statements[statements.length - 1];
|
|
291
|
+
const guard = statements[statements.length - 2];
|
|
292
|
+
if (!t.isReturnStatement(last) || !t.isIfStatement(guard) || guard.alternate)
|
|
293
|
+
break;
|
|
294
|
+
const branch = t.isBlockStatement(guard.consequent) && guard.consequent.body.length === 1
|
|
295
|
+
? guard.consequent.body[0] : guard.consequent;
|
|
296
|
+
if (!t.isReturnStatement(branch))
|
|
297
|
+
break;
|
|
298
|
+
statements.splice(-2, 2, t.returnStatement(t.conditionalExpression(guard.test, branch.argument ?? t.nullLiteral(), last.argument ?? t.nullLiteral())));
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
function hookName(call) {
|
|
303
|
+
const callee = call.get('callee');
|
|
304
|
+
if (!callee.isIdentifier())
|
|
305
|
+
return undefined;
|
|
306
|
+
const binding = callee.scope.getBinding(callee.node.name);
|
|
307
|
+
if (!binding)
|
|
308
|
+
return callee.node.name; // classic runtime supplied by the caller
|
|
309
|
+
if (!binding.path.isImportSpecifier())
|
|
310
|
+
return undefined;
|
|
311
|
+
const declaration = binding.path.parentPath;
|
|
312
|
+
if (!declaration.isImportDeclaration() || !['react', '@rrjs/react-compat'].includes(declaration.node.source.value))
|
|
313
|
+
return undefined;
|
|
314
|
+
const imported = binding.path.node.imported;
|
|
315
|
+
return t.isIdentifier(imported) ? imported.name : imported.value;
|
|
316
|
+
}
|
|
317
|
+
function importedIdentity(binding) {
|
|
318
|
+
if (!binding?.path.isImportSpecifier())
|
|
319
|
+
return undefined;
|
|
320
|
+
const declaration = binding.path.parentPath;
|
|
321
|
+
if (!declaration.isImportDeclaration())
|
|
322
|
+
return undefined;
|
|
323
|
+
const imported = binding.path.node.imported;
|
|
324
|
+
return {
|
|
325
|
+
source: declaration.node.source.value,
|
|
326
|
+
exported: t.isIdentifier(imported) ? imported.name : imported.value,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function importedComponentContract(binding) {
|
|
330
|
+
const identity = importedIdentity(binding);
|
|
331
|
+
const exact = identity && moduleMetadata?.imports?.[identity.source]?.components?.[identity.exported];
|
|
332
|
+
if (exact)
|
|
333
|
+
return exact;
|
|
334
|
+
return binding ? moduleMetadata?.importedComponents?.[binding.identifier.name] : undefined;
|
|
335
|
+
}
|
|
336
|
+
function isImportedReactiveHook(binding) {
|
|
337
|
+
const identity = importedIdentity(binding);
|
|
338
|
+
if (identity && moduleMetadata?.imports?.[identity.source]?.hooks?.includes(identity.exported))
|
|
339
|
+
return true;
|
|
340
|
+
return Boolean(binding && moduleMetadata?.importedHooks?.includes(binding.identifier.name));
|
|
341
|
+
}
|
|
342
|
+
const componentUses = [];
|
|
343
|
+
const providerUses = [];
|
|
344
|
+
const componentProps = new Map();
|
|
345
|
+
const componentDefaultProps = new Map();
|
|
346
|
+
const propSources = [];
|
|
347
|
+
program.traverse({
|
|
348
|
+
JSXOpeningElement(path) {
|
|
349
|
+
const name = path.node.name;
|
|
350
|
+
if (t.isJSXIdentifier(name) && /^[a-z]/.test(name.name))
|
|
351
|
+
return;
|
|
352
|
+
if (t.isJSXMemberExpression(name) && t.isJSXIdentifier(name.object)
|
|
353
|
+
&& t.isJSXIdentifier(name.property, { name: 'Provider' })) {
|
|
354
|
+
const contextBinding = path.scope.getBinding(name.object.name);
|
|
355
|
+
const init = contextBinding?.path.isVariableDeclarator() ? contextBinding.path.get('init') : undefined;
|
|
356
|
+
if (!init?.isCallExpression() || !t.isIdentifier(init.node.callee, { name: 'createContext' })) {
|
|
357
|
+
throw path.buildCodeFrameError('runOnce: Provider must come from a direct local createContext call');
|
|
358
|
+
}
|
|
359
|
+
providerUses.push(path);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!t.isJSXIdentifier(name))
|
|
363
|
+
throw path.buildCodeFrameError('runOnce: member components require wrapper compilation');
|
|
364
|
+
const binding = path.scope.getBinding(name.name);
|
|
365
|
+
let forwardRef = false;
|
|
366
|
+
let component = binding?.path.isFunctionDeclaration() ? binding.path
|
|
367
|
+
: binding?.path.isVariableDeclarator()
|
|
368
|
+
&& (binding.path.get('init').isFunctionExpression() || binding.path.get('init').isArrowFunctionExpression())
|
|
369
|
+
? binding.path.get('init') : undefined;
|
|
370
|
+
if (!component && binding?.path.isVariableDeclarator()) {
|
|
371
|
+
const init = binding.path.get('init');
|
|
372
|
+
if (init.isCallExpression() && t.isIdentifier(init.node.callee, { name: 'forwardRef' })) {
|
|
373
|
+
const render = init.get('arguments.0');
|
|
374
|
+
if (render?.isFunctionExpression() || render?.isArrowFunctionExpression()) {
|
|
375
|
+
component = render;
|
|
376
|
+
forwardRef = true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (!component) {
|
|
381
|
+
const contract = importedComponentContract(binding);
|
|
382
|
+
const declared = Array.isArray(contract) ? contract : contract?.props;
|
|
383
|
+
if (!binding?.path.isImportSpecifier() || !declared) {
|
|
384
|
+
throw path.buildCodeFrameError('runOnce: only analyzed imported or direct local function components are supported in the current composition scope');
|
|
385
|
+
}
|
|
386
|
+
componentUses.push({ opening: path, props: new Map(), declaredProps: new Set(declared), forwardRef: false });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const element = path.parentPath;
|
|
390
|
+
const params = component.get('params');
|
|
391
|
+
if ((forwardRef ? params.length !== 2 : params.length !== 1) || !params[0].isObjectPattern()) {
|
|
392
|
+
if (path.node.attributes.length)
|
|
393
|
+
throw path.buildCodeFrameError('runOnce: component props currently require one destructured object parameter');
|
|
394
|
+
componentUses.push({ opening: path, component, props: new Map(), forwardRef });
|
|
395
|
+
components.add(component);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
let props = componentProps.get(component);
|
|
399
|
+
if (!props) {
|
|
400
|
+
props = new Map();
|
|
401
|
+
const defaults = new Set();
|
|
402
|
+
for (const property of params[0].get('properties')) {
|
|
403
|
+
if (!property.isObjectProperty() || property.node.computed
|
|
404
|
+
|| !(t.isIdentifier(property.node.key) || t.isStringLiteral(property.node.key))
|
|
405
|
+
|| !(t.isIdentifier(property.node.value) || (t.isAssignmentPattern(property.node.value)
|
|
406
|
+
&& t.isIdentifier(property.node.value.left)))) {
|
|
407
|
+
throw params[0].buildCodeFrameError('runOnce: component props currently require direct destructured bindings');
|
|
408
|
+
}
|
|
409
|
+
const external = t.isIdentifier(property.node.key) ? property.node.key.name : property.node.key.value;
|
|
410
|
+
const local = (t.isIdentifier(property.node.value) ? property.node.value : property.node.value.left);
|
|
411
|
+
if (t.isAssignmentPattern(property.node.value)) {
|
|
412
|
+
const fallback = property.node.value.right;
|
|
413
|
+
if (!(t.isStringLiteral(fallback) || t.isNumericLiteral(fallback)
|
|
414
|
+
|| t.isBooleanLiteral(fallback) || t.isNullLiteral(fallback))) {
|
|
415
|
+
throw property.buildCodeFrameError('runOnce: default component props currently require a static scalar');
|
|
416
|
+
}
|
|
417
|
+
property.node.value.right = t.arrowFunctionExpression([], fallback);
|
|
418
|
+
defaults.add(external);
|
|
419
|
+
}
|
|
420
|
+
const propBinding = component.scope.getBinding(local.name);
|
|
421
|
+
if (!propBinding)
|
|
422
|
+
throw property.buildCodeFrameError('runOnce: unable to resolve component prop binding');
|
|
423
|
+
props.set(external, propBinding);
|
|
424
|
+
}
|
|
425
|
+
componentProps.set(component, props);
|
|
426
|
+
componentDefaultProps.set(component, defaults);
|
|
427
|
+
}
|
|
428
|
+
componentUses.push({ opening: path, component, props, forwardRef });
|
|
429
|
+
components.add(component);
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
program.traverse({
|
|
433
|
+
VariableDeclarator(path) {
|
|
434
|
+
const init = path.get('init');
|
|
435
|
+
if (!init.isCallExpression() || hookName(init) !== 'useState')
|
|
436
|
+
return;
|
|
437
|
+
const fn = path.getFunctionParent();
|
|
438
|
+
if (!fn)
|
|
439
|
+
throw path.buildCodeFrameError('runOnce: useState must be inside a component');
|
|
440
|
+
if (!t.isArrayPattern(path.node.id) || !t.isIdentifier(path.node.id.elements[0]))
|
|
441
|
+
throw path.buildCodeFrameError('runOnce: useState requires a named state binding');
|
|
442
|
+
const binding = path.scope.getBinding(path.node.id.elements[0].name);
|
|
443
|
+
if (!binding.constant)
|
|
444
|
+
throw path.buildCodeFrameError('runOnce: state bindings cannot be reassigned');
|
|
445
|
+
reactive.set(binding, fn);
|
|
446
|
+
// `count()` is the older spelling, where state was a getter. Under these
|
|
447
|
+
// semantics `count` is the value, so calling it is a call on whatever the
|
|
448
|
+
// state holds -- for `useState(0)` that is a TypeError at the first click
|
|
449
|
+
// rather than at build time. When the initial value is plainly not
|
|
450
|
+
// callable, say so now instead of shipping that crash.
|
|
451
|
+
const initial = init.node.arguments[0];
|
|
452
|
+
const plainlyNotCallable = t.isNumericLiteral(initial) || t.isStringLiteral(initial)
|
|
453
|
+
|| t.isBooleanLiteral(initial) || t.isNullLiteral(initial)
|
|
454
|
+
|| t.isArrayExpression(initial) || t.isObjectExpression(initial)
|
|
455
|
+
|| t.isTemplateLiteral(initial);
|
|
456
|
+
if (plainlyNotCallable) {
|
|
457
|
+
for (const reference of binding.referencePaths) {
|
|
458
|
+
const parent = reference.parentPath;
|
|
459
|
+
if (parent?.isCallExpression() && parent.node.callee === reference.node) {
|
|
460
|
+
throw parent.buildCodeFrameError(`runOnce: state is a value here, so \`${binding.identifier.name}()\` calls it; read \`${binding.identifier.name}\` instead`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const setter = path.node.id.elements[1];
|
|
465
|
+
if (t.isIdentifier(setter)) {
|
|
466
|
+
const setterBinding = path.scope.getBinding(setter.name);
|
|
467
|
+
if (setterBinding)
|
|
468
|
+
stateSetters.set(setterBinding, binding);
|
|
469
|
+
}
|
|
470
|
+
components.add(fn);
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
program.traverse({
|
|
474
|
+
VariableDeclarator(path) {
|
|
475
|
+
const init = path.get('init');
|
|
476
|
+
if (!t.isIdentifier(path.node.id) || !init.isCallExpression() || hookName(init) !== 'useContext')
|
|
477
|
+
return;
|
|
478
|
+
const fn = path.getFunctionParent();
|
|
479
|
+
if (!fn)
|
|
480
|
+
throw path.buildCodeFrameError('runOnce: useContext must be inside a component');
|
|
481
|
+
const binding = path.scope.getBinding(path.node.id.name);
|
|
482
|
+
if (!binding.constant)
|
|
483
|
+
throw path.buildCodeFrameError('runOnce: context bindings cannot be reassigned');
|
|
484
|
+
reactive.set(binding, fn);
|
|
485
|
+
components.add(fn);
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
program.traverse({
|
|
489
|
+
VariableDeclarator(path) {
|
|
490
|
+
if (!t.isIdentifier(path.node.id))
|
|
491
|
+
return;
|
|
492
|
+
const init = path.get('init');
|
|
493
|
+
if (!init.isCallExpression() || !t.isIdentifier(init.node.callee))
|
|
494
|
+
return;
|
|
495
|
+
const imported = init.scope.getBinding(init.node.callee.name);
|
|
496
|
+
if (!isImportedReactiveHook(imported))
|
|
497
|
+
return;
|
|
498
|
+
const fn = path.getFunctionParent();
|
|
499
|
+
if (!fn)
|
|
500
|
+
throw path.buildCodeFrameError('runOnce: analyzed imported hooks must be called inside a component');
|
|
501
|
+
reactive.set(path.scope.getBinding(path.node.id.name), fn);
|
|
502
|
+
components.add(fn);
|
|
503
|
+
},
|
|
504
|
+
FunctionDeclaration(path) {
|
|
505
|
+
if (!path.node.id || !/^[A-Z]/.test(path.node.id.name)
|
|
506
|
+
|| !path.parentPath.isExportNamedDeclaration() || componentProps.has(path))
|
|
507
|
+
return;
|
|
508
|
+
const param = path.get('params.0');
|
|
509
|
+
if (!param?.isObjectPattern())
|
|
510
|
+
return;
|
|
511
|
+
const props = new Map();
|
|
512
|
+
for (const property of param.get('properties')) {
|
|
513
|
+
if (!property.isObjectProperty() || property.node.computed
|
|
514
|
+
|| !(t.isIdentifier(property.node.key) || t.isStringLiteral(property.node.key))
|
|
515
|
+
|| !t.isIdentifier(property.node.value)) {
|
|
516
|
+
throw property.buildCodeFrameError('runOnce: analyzed exported component props require direct destructured bindings');
|
|
517
|
+
}
|
|
518
|
+
const external = t.isIdentifier(property.node.key) ? property.node.key.name : property.node.key.value;
|
|
519
|
+
const binding = path.scope.getBinding(property.node.value.name);
|
|
520
|
+
if (!binding)
|
|
521
|
+
throw property.buildCodeFrameError('runOnce: unable to resolve exported component prop binding');
|
|
522
|
+
props.set(external, binding);
|
|
523
|
+
}
|
|
524
|
+
componentProps.set(path, props);
|
|
525
|
+
for (const [name, binding] of props) {
|
|
526
|
+
reactive.set(binding, path);
|
|
527
|
+
if (moduleMetadata?.operationProps?.includes(name))
|
|
528
|
+
operationStates.add(binding);
|
|
529
|
+
}
|
|
530
|
+
components.add(path);
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
for (const [component, props] of componentProps) {
|
|
534
|
+
for (const binding of props.values())
|
|
535
|
+
reactive.set(binding, component);
|
|
536
|
+
}
|
|
537
|
+
const customHooks = new Map();
|
|
538
|
+
program.traverse({
|
|
539
|
+
Function(path) {
|
|
540
|
+
const name = path.isFunctionDeclaration() ? path.node.id?.name
|
|
541
|
+
: path.parentPath.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id)
|
|
542
|
+
? path.parentPath.node.id.name : undefined;
|
|
543
|
+
if (!name || !/^use[A-Z]/.test(name) || !t.isBlockStatement(path.node.body))
|
|
544
|
+
return;
|
|
545
|
+
const returned = path.node.body.body.find(statement => t.isReturnStatement(statement));
|
|
546
|
+
if (!returned || !t.isReturnStatement(returned) || !t.isArrayExpression(returned.argument))
|
|
547
|
+
return;
|
|
548
|
+
const values = new Map();
|
|
549
|
+
returned.argument.elements.forEach((element, index) => {
|
|
550
|
+
if (!t.isIdentifier(element))
|
|
551
|
+
return;
|
|
552
|
+
const binding = path.scope.getBinding(element.name);
|
|
553
|
+
if (binding && reactive.get(binding) === path)
|
|
554
|
+
values.set(index, binding);
|
|
555
|
+
});
|
|
556
|
+
const ownBinding = path.isFunctionDeclaration() && path.node.id
|
|
557
|
+
? path.parentPath.scope.getBinding(path.node.id.name)
|
|
558
|
+
: path.parentPath.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id)
|
|
559
|
+
? path.parentPath.scope.getBinding(path.parentPath.node.id.name) : undefined;
|
|
560
|
+
if (ownBinding && values.size)
|
|
561
|
+
customHooks.set(ownBinding, values);
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
program.traverse({
|
|
565
|
+
VariableDeclarator(path) {
|
|
566
|
+
const init = path.get('init');
|
|
567
|
+
if (!init.isCallExpression() || !t.isIdentifier(init.node.callee))
|
|
568
|
+
return;
|
|
569
|
+
const hook = customHooks.get(init.scope.getBinding(init.node.callee.name));
|
|
570
|
+
if (!hook)
|
|
571
|
+
return;
|
|
572
|
+
const component = path.getFunctionParent();
|
|
573
|
+
if (!component || !t.isArrayPattern(path.node.id))
|
|
574
|
+
throw path.buildCodeFrameError('runOnce: analyzed custom hooks require array destructuring inside a component');
|
|
575
|
+
for (const index of hook.keys()) {
|
|
576
|
+
const local = path.node.id.elements[index];
|
|
577
|
+
if (!t.isIdentifier(local))
|
|
578
|
+
throw path.buildCodeFrameError('runOnce: custom-hook reactive returns require named array bindings');
|
|
579
|
+
const binding = path.scope.getBinding(local.name);
|
|
580
|
+
if (!binding)
|
|
581
|
+
throw path.buildCodeFrameError('runOnce: unable to resolve custom-hook return binding');
|
|
582
|
+
reactive.set(binding, component);
|
|
583
|
+
}
|
|
584
|
+
components.add(component);
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
// Every accepted component prop is a getter in the target. This gives the
|
|
588
|
+
// child a stable prop binding that can subscribe directly to the parent's
|
|
589
|
+
// signal while both component bodies remain single-execution.
|
|
590
|
+
for (const { opening, component, props, declaredProps, forwardRef } of componentUses) {
|
|
591
|
+
const attributes = new Map();
|
|
592
|
+
for (const attribute of opening.get('attributes')) {
|
|
593
|
+
if (attribute.isJSXSpreadAttribute())
|
|
594
|
+
throw opening.buildCodeFrameError('runOnce: component spread props require reactive prop compilation');
|
|
595
|
+
if (!attribute.isJSXAttribute() || !t.isJSXIdentifier(attribute.node.name))
|
|
596
|
+
throw opening.buildCodeFrameError('runOnce: namespaced component props are unsupported');
|
|
597
|
+
const name = attribute.node.name.name;
|
|
598
|
+
if (name === 'ref') {
|
|
599
|
+
if (!forwardRef)
|
|
600
|
+
throw opening.buildCodeFrameError('runOnce: component refs require a direct local forwardRef wrapper');
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (name === 'key')
|
|
604
|
+
throw opening.buildCodeFrameError('runOnce: component keys require lifetime compilation');
|
|
605
|
+
if (!props.has(name) && !declaredProps?.has(name))
|
|
606
|
+
throw attribute.buildCodeFrameError(`runOnce: component prop ${name} is not declared by the analyzed component`);
|
|
607
|
+
attributes.set(name, attribute);
|
|
608
|
+
}
|
|
609
|
+
for (const name of props.keys()) {
|
|
610
|
+
if (name === 'children')
|
|
611
|
+
continue;
|
|
612
|
+
if (component && componentDefaultProps.get(component)?.has(name))
|
|
613
|
+
continue;
|
|
614
|
+
if (!attributes.has(name))
|
|
615
|
+
throw opening.buildCodeFrameError(`runOnce: component prop ${name} must be supplied until default/optional prop compilation is implemented`);
|
|
616
|
+
}
|
|
617
|
+
for (const [name, attribute] of attributes) {
|
|
618
|
+
const value = attribute.node.value;
|
|
619
|
+
const expression = value == null ? t.booleanLiteral(true)
|
|
620
|
+
: t.isStringLiteral(value) ? value
|
|
621
|
+
: t.isJSXExpressionContainer(value) && !t.isJSXEmptyExpression(value.expression)
|
|
622
|
+
? value.expression : undefined;
|
|
623
|
+
if (!expression)
|
|
624
|
+
throw attribute.buildCodeFrameError(`runOnce: unsupported value for component prop ${name}`);
|
|
625
|
+
const staticValue = t.isStringLiteral(expression) || t.isNumericLiteral(expression)
|
|
626
|
+
|| t.isBooleanLiteral(expression) || t.isNullLiteral(expression);
|
|
627
|
+
const parent = opening.getFunctionParent();
|
|
628
|
+
const directBinding = t.isIdentifier(expression) ? opening.scope.getBinding(expression.name) : undefined;
|
|
629
|
+
const inlineFunction = t.isFunctionExpression(expression) || t.isArrowFunctionExpression(expression);
|
|
630
|
+
const directFunction = Boolean(directBinding?.constant
|
|
631
|
+
&& (directBinding.path.isFunctionDeclaration()
|
|
632
|
+
|| (directBinding.path.isVariableDeclarator()
|
|
633
|
+
&& (directBinding.path.get('init').isFunctionExpression() || directBinding.path.get('init').isArrowFunctionExpression())))
|
|
634
|
+
&& directBinding.path.getFunctionParent() === parent);
|
|
635
|
+
if (!staticValue && !inlineFunction && !directFunction
|
|
636
|
+
&& (!parent || !directBinding || reactive.get(directBinding) !== parent)) {
|
|
637
|
+
throw attribute.buildCodeFrameError('runOnce: component props currently accept static scalars or direct reactive bindings');
|
|
638
|
+
}
|
|
639
|
+
const childBinding = props.get(name);
|
|
640
|
+
if (directBinding && childBinding) {
|
|
641
|
+
propSources.push({
|
|
642
|
+
childScope: childBinding.scope,
|
|
643
|
+
childName: childBinding.identifier.name,
|
|
644
|
+
parentScope: directBinding.scope,
|
|
645
|
+
parentName: directBinding.identifier.name,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (directBinding && t.isJSXIdentifier(opening.node.name)) {
|
|
649
|
+
const contract = importedComponentContract(opening.scope.getBinding(opening.node.name.name));
|
|
650
|
+
const operationKey = !Array.isArray(contract) ? contract?.operationKeys?.[name] : undefined;
|
|
651
|
+
if (operationKey) {
|
|
652
|
+
operationStates.add(directBinding);
|
|
653
|
+
importedOperationEdges.push({
|
|
654
|
+
scope: directBinding.scope,
|
|
655
|
+
state: directBinding.identifier.name,
|
|
656
|
+
prop: name,
|
|
657
|
+
key: operationKey,
|
|
658
|
+
origin: attribute,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const getter = t.arrowFunctionExpression([], expression);
|
|
663
|
+
generated.add(getter);
|
|
664
|
+
attribute.node.value = t.jsxExpressionContainer(getter);
|
|
665
|
+
}
|
|
666
|
+
const element = opening.parentPath;
|
|
667
|
+
if (element.isJSXElement()) {
|
|
668
|
+
const actualChildren = element.node.children.filter(child => !t.isJSXText(child) || child.value.trim().length > 0);
|
|
669
|
+
if (actualChildren.length) {
|
|
670
|
+
if (!props.has('children') && !declaredProps?.has('children'))
|
|
671
|
+
throw opening.buildCodeFrameError('runOnce: component children require a declared children prop');
|
|
672
|
+
if (actualChildren.length !== 1 || t.isJSXSpreadChild(actualChildren[0])) {
|
|
673
|
+
throw opening.buildCodeFrameError('runOnce: multiple and spread component children require child collection compilation');
|
|
674
|
+
}
|
|
675
|
+
const child = actualChildren[0];
|
|
676
|
+
const expression = t.isJSXText(child) ? t.stringLiteral(child.value)
|
|
677
|
+
: t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)
|
|
678
|
+
? child.expression
|
|
679
|
+
: t.isJSXElement(child) || t.isJSXFragment(child) ? child : undefined;
|
|
680
|
+
if (!expression)
|
|
681
|
+
throw opening.buildCodeFrameError('runOnce: unsupported component child');
|
|
682
|
+
const factory = t.arrowFunctionExpression([], expression);
|
|
683
|
+
factory.extra = { ...(factory.extra ?? {}), rrjsRegion: true };
|
|
684
|
+
generated.add(factory);
|
|
685
|
+
element.node.children = [t.jsxExpressionContainer(factory)];
|
|
686
|
+
}
|
|
687
|
+
else if (props.has('children') || declaredProps?.has('children')) {
|
|
688
|
+
throw opening.buildCodeFrameError('runOnce: a destructured children prop must receive one child in the current scope');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const opening of providerUses) {
|
|
693
|
+
const attributes = opening.get('attributes');
|
|
694
|
+
if (attributes.length !== 1 || !attributes[0].isJSXAttribute()
|
|
695
|
+
|| !t.isJSXIdentifier(attributes[0].node.name, { name: 'value' })) {
|
|
696
|
+
throw opening.buildCodeFrameError('runOnce: Context.Provider currently requires exactly one value prop');
|
|
697
|
+
}
|
|
698
|
+
const value = attributes[0].node.value;
|
|
699
|
+
const expression = t.isStringLiteral(value) ? value
|
|
700
|
+
: t.isJSXExpressionContainer(value) && !t.isJSXEmptyExpression(value.expression)
|
|
701
|
+
? value.expression : undefined;
|
|
702
|
+
if (!expression)
|
|
703
|
+
throw attributes[0].buildCodeFrameError('runOnce: unsupported Context.Provider value');
|
|
704
|
+
const parent = opening.getFunctionParent();
|
|
705
|
+
// The value is compiled into a getter, so it is re-read whenever anything
|
|
706
|
+
// it depends on changes. That is sound for a scalar, for a binding this
|
|
707
|
+
// component owns, and -- by the same argument applied to each entry -- for
|
|
708
|
+
// an object or array literal built only out of those. `value={{ user }}`
|
|
709
|
+
// is the ordinary React spelling, so accept it rather than forcing callers
|
|
710
|
+
// to hoist a binding they never needed.
|
|
711
|
+
const accepts = (node) => {
|
|
712
|
+
if (t.isStringLiteral(node) || t.isNumericLiteral(node)
|
|
713
|
+
|| t.isBooleanLiteral(node) || t.isNullLiteral(node))
|
|
714
|
+
return true;
|
|
715
|
+
if (t.isIdentifier(node)) {
|
|
716
|
+
const binding = opening.scope.getBinding(node.name);
|
|
717
|
+
if (!binding)
|
|
718
|
+
return false;
|
|
719
|
+
const local = Boolean(parent && binding.constant
|
|
720
|
+
&& binding.path.isVariableDeclarator()
|
|
721
|
+
&& binding.path.getFunctionParent() === parent);
|
|
722
|
+
return local || Boolean(parent && reactive.get(binding) === parent);
|
|
723
|
+
}
|
|
724
|
+
if (t.isObjectExpression(node)) {
|
|
725
|
+
return node.properties.every(property => t.isObjectProperty(property)
|
|
726
|
+
&& !property.computed
|
|
727
|
+
&& (t.isIdentifier(property.key) || t.isStringLiteral(property.key))
|
|
728
|
+
&& t.isExpression(property.value) && accepts(property.value));
|
|
729
|
+
}
|
|
730
|
+
if (t.isArrayExpression(node)) {
|
|
731
|
+
return node.elements.every(element => element !== null && t.isExpression(element) && accepts(element));
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
};
|
|
735
|
+
if (!accepts(expression)) {
|
|
736
|
+
throw attributes[0].buildCodeFrameError('runOnce: provider values currently accept static scalars, bindings this component owns, and object or array literals of those');
|
|
737
|
+
}
|
|
738
|
+
const valueGetter = t.arrowFunctionExpression([], expression);
|
|
739
|
+
generated.add(valueGetter);
|
|
740
|
+
attributes[0].node.value = t.jsxExpressionContainer(valueGetter);
|
|
741
|
+
const element = opening.parentPath;
|
|
742
|
+
if (!element.isJSXElement())
|
|
743
|
+
throw opening.buildCodeFrameError('runOnce: provider must be a JSX element');
|
|
744
|
+
const actualChildren = element.node.children.filter(child => !t.isJSXText(child) || child.value.trim().length > 0);
|
|
745
|
+
if (actualChildren.length !== 1 || t.isJSXSpreadChild(actualChildren[0])) {
|
|
746
|
+
throw opening.buildCodeFrameError('runOnce: provider currently requires exactly one owned child');
|
|
747
|
+
}
|
|
748
|
+
const child = actualChildren[0];
|
|
749
|
+
const childExpression = t.isJSXText(child) ? t.stringLiteral(child.value)
|
|
750
|
+
: t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)
|
|
751
|
+
? child.expression
|
|
752
|
+
: t.isJSXElement(child) || t.isJSXFragment(child) ? child : undefined;
|
|
753
|
+
if (!childExpression)
|
|
754
|
+
throw opening.buildCodeFrameError('runOnce: unsupported provider child');
|
|
755
|
+
const childFactory = t.arrowFunctionExpression([], childExpression);
|
|
756
|
+
childFactory.extra = { ...(childFactory.extra ?? {}), rrjsRegion: true };
|
|
757
|
+
generated.add(childFactory);
|
|
758
|
+
element.node.children = [t.jsxExpressionContainer(childFactory)];
|
|
759
|
+
}
|
|
760
|
+
// Phase 3 starts with direct inline callbacks and literal empty dependency
|
|
761
|
+
// lists. Reactive captures are snapshotted below at component execution, so
|
|
762
|
+
// both effect setup and its cleanup observe the initial render values.
|
|
763
|
+
const changingEffects = new Set();
|
|
764
|
+
const inferredEffects = new Set();
|
|
765
|
+
program.traverse({
|
|
766
|
+
CallExpression(path) {
|
|
767
|
+
if (hookName(path) !== 'useEffect')
|
|
768
|
+
return;
|
|
769
|
+
const [callback, deps] = path.get('arguments');
|
|
770
|
+
if (path.node.arguments.length > 2) {
|
|
771
|
+
throw path.buildCodeFrameError('runOnce: useEffect takes a callback and an optional dependency array');
|
|
772
|
+
}
|
|
773
|
+
if (!callback || !(callback.isFunctionExpression() || callback.isArrowFunctionExpression())) {
|
|
774
|
+
throw path.buildCodeFrameError('runOnce: useEffect requires an inline callback in the supported empty-dependency scope');
|
|
775
|
+
}
|
|
776
|
+
// No dependency argument is React's "after every render". There is no
|
|
777
|
+
// second render here, so the closest honest reading is to re-run when
|
|
778
|
+
// something the callback actually reads changes. The dependency list for
|
|
779
|
+
// that is synthesised below, once every reactive binding is known.
|
|
780
|
+
if (!deps) {
|
|
781
|
+
inferredEffects.add(path.node);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (!deps.isArrayExpression()) {
|
|
785
|
+
throw path.buildCodeFrameError('runOnce: useEffect requires a literal dependency array');
|
|
786
|
+
}
|
|
787
|
+
if (deps.node.elements.length > 0)
|
|
788
|
+
changingEffects.add(path.node);
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
// Find derived bindings transitively before replacing references. Binding
|
|
792
|
+
// identity, rather than identifier spelling, preserves lexical shadowing.
|
|
793
|
+
function helperKind(path) {
|
|
794
|
+
if (!t.isIdentifier(path.node.callee))
|
|
795
|
+
return 'unsupported';
|
|
796
|
+
const binding = path.scope.getBinding(path.node.callee.name);
|
|
797
|
+
if (!binding?.constant)
|
|
798
|
+
return 'unsupported';
|
|
799
|
+
const node = binding.path.isVariableDeclarator() ? binding.path.node.init : binding.path.node;
|
|
800
|
+
if (!node || (!t.isFunctionDeclaration(node) && !t.isArrowFunctionExpression(node) && !t.isFunctionExpression(node)))
|
|
801
|
+
return 'unsupported';
|
|
802
|
+
if (node.async || node.generator || !node.params.every(parameter => t.isIdentifier(parameter)))
|
|
803
|
+
return 'unsupported';
|
|
804
|
+
const parameters = new Set(node.params.map(parameter => parameter.name));
|
|
805
|
+
const pure = (value) => {
|
|
806
|
+
if (!value)
|
|
807
|
+
return false;
|
|
808
|
+
if (t.isNumericLiteral(value) || t.isStringLiteral(value) || t.isBooleanLiteral(value) || t.isNullLiteral(value))
|
|
809
|
+
return true;
|
|
810
|
+
if (t.isIdentifier(value))
|
|
811
|
+
return parameters.has(value.name);
|
|
812
|
+
if (t.isBinaryExpression(value) || t.isLogicalExpression(value))
|
|
813
|
+
return pure(value.left) && pure(value.right);
|
|
814
|
+
if (t.isUnaryExpression(value))
|
|
815
|
+
return value.operator !== 'delete' && pure(value.argument);
|
|
816
|
+
if (t.isConditionalExpression(value))
|
|
817
|
+
return pure(value.test) && pure(value.consequent) && pure(value.alternate);
|
|
818
|
+
return false;
|
|
819
|
+
};
|
|
820
|
+
const expression = t.isBlockStatement(node.body)
|
|
821
|
+
? node.body.body.length === 1 && t.isReturnStatement(node.body.body[0]) ? node.body.body[0].argument : null
|
|
822
|
+
: node.body;
|
|
823
|
+
if (pure(expression))
|
|
824
|
+
return 'pure';
|
|
825
|
+
return 'unsupported';
|
|
826
|
+
}
|
|
827
|
+
function supportedCall(path, _component) {
|
|
828
|
+
const callee = path.get('callee');
|
|
829
|
+
if (callee.isMemberExpression() && !callee.node.computed
|
|
830
|
+
&& callee.get('property').isIdentifier({ name: 'trim' })
|
|
831
|
+
&& path.node.arguments.length === 0)
|
|
832
|
+
return true;
|
|
833
|
+
if (callee.isMemberExpression() && !callee.node.computed
|
|
834
|
+
&& callee.get('property').isIdentifier({ name: 'filter' })
|
|
835
|
+
&& path.node.arguments.length === 1) {
|
|
836
|
+
const predicate = path.get('arguments.0');
|
|
837
|
+
if (!predicate.isArrowFunctionExpression() && !predicate.isFunctionExpression())
|
|
838
|
+
return false;
|
|
839
|
+
let impure = predicate.node.async || predicate.node.generator;
|
|
840
|
+
predicate.traverse({
|
|
841
|
+
Function(nested) { if (nested.node !== predicate.node)
|
|
842
|
+
nested.skip(); },
|
|
843
|
+
CallExpression() { impure = true; },
|
|
844
|
+
NewExpression() { impure = true; },
|
|
845
|
+
AssignmentExpression() { impure = true; },
|
|
846
|
+
UpdateExpression() { impure = true; },
|
|
847
|
+
AwaitExpression() { impure = true; },
|
|
848
|
+
});
|
|
849
|
+
return !impure;
|
|
850
|
+
}
|
|
851
|
+
return helperKind(path) === 'pure';
|
|
852
|
+
}
|
|
853
|
+
// A map over a source-declared array has fixed membership. Preserve those
|
|
854
|
+
// rows and expose only reactive object fields as lazy children; the renderer
|
|
855
|
+
// already tracks function children without rebuilding their enclosing nodes.
|
|
856
|
+
program.traverse({
|
|
857
|
+
CallExpression(path) {
|
|
858
|
+
const fn = path.getFunctionParent();
|
|
859
|
+
const callee = path.get('callee');
|
|
860
|
+
if (!fn || !components.has(fn) || !callee.isMemberExpression() || callee.node.computed
|
|
861
|
+
|| !callee.get('property').isIdentifier({ name: 'map' }))
|
|
862
|
+
return;
|
|
863
|
+
const source = callee.get('object');
|
|
864
|
+
if (!source.isArrayExpression())
|
|
865
|
+
return;
|
|
866
|
+
for (const element of source.get('elements')) {
|
|
867
|
+
if (!element?.isObjectExpression())
|
|
868
|
+
continue;
|
|
869
|
+
for (const property of element.get('properties')) {
|
|
870
|
+
if (!property.isObjectProperty() || property.node.computed)
|
|
871
|
+
continue;
|
|
872
|
+
const value = property.get('value');
|
|
873
|
+
let depends = value.isReferencedIdentifier()
|
|
874
|
+
&& reactive.has(value.scope.getBinding(value.node.name));
|
|
875
|
+
value.traverse({
|
|
876
|
+
ReferencedIdentifier(ref) {
|
|
877
|
+
if (reactive.has(ref.scope.getBinding(ref.node.name)))
|
|
878
|
+
depends = true;
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
if (!depends || value.isFunctionExpression() || value.isArrowFunctionExpression())
|
|
882
|
+
continue;
|
|
883
|
+
const getter = t.arrowFunctionExpression([], value.node);
|
|
884
|
+
getter.extra = { rrjsReactiveGetter: true };
|
|
885
|
+
generated.add(getter);
|
|
886
|
+
value.replaceWith(getter);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
let added = true;
|
|
892
|
+
while (added) {
|
|
893
|
+
added = false;
|
|
894
|
+
program.traverse({
|
|
895
|
+
VariableDeclarator(path) {
|
|
896
|
+
if (!t.isIdentifier(path.node.id) || !path.node.init)
|
|
897
|
+
return;
|
|
898
|
+
const fn = path.getFunctionParent();
|
|
899
|
+
if (!fn || !components.has(fn))
|
|
900
|
+
return;
|
|
901
|
+
const binding = path.scope.getBinding(path.node.id.name);
|
|
902
|
+
if (reactive.has(binding))
|
|
903
|
+
return;
|
|
904
|
+
const init = path.get('init');
|
|
905
|
+
const initCallee = init.isCallExpression() ? init.get('callee') : undefined;
|
|
906
|
+
if (initCallee?.isMemberExpression() && !initCallee.node.computed
|
|
907
|
+
&& initCallee.get('property').isIdentifier({ name: 'map' })
|
|
908
|
+
&& initCallee.get('object').isArrayExpression())
|
|
909
|
+
return;
|
|
910
|
+
let depends = init.isReferencedIdentifier() && reactive.has(init.scope.getBinding(init.node.name));
|
|
911
|
+
init.traverse({
|
|
912
|
+
Function(nested) { if (generated.has(nested.node))
|
|
913
|
+
nested.skip(); },
|
|
914
|
+
ReferencedIdentifier(ref) { if (reactive.has(ref.scope.getBinding(ref.node.name)))
|
|
915
|
+
depends = true; },
|
|
916
|
+
});
|
|
917
|
+
if (!depends)
|
|
918
|
+
return;
|
|
919
|
+
if (init.isFunctionExpression() || init.isArrowFunctionExpression())
|
|
920
|
+
return;
|
|
921
|
+
if (!binding.constant || !path.parentPath.isVariableDeclaration({ kind: 'const' }))
|
|
922
|
+
throw path.buildCodeFrameError('runOnce: derived bindings must be const');
|
|
923
|
+
let unsupported = false;
|
|
924
|
+
init.traverse({
|
|
925
|
+
Function(nested) { nested.skip(); },
|
|
926
|
+
CallExpression(call) { if (!supportedCall(call, fn))
|
|
927
|
+
unsupported = true; },
|
|
928
|
+
NewExpression() { unsupported = true; },
|
|
929
|
+
AssignmentExpression() { unsupported = true; },
|
|
930
|
+
UpdateExpression() { unsupported = true; },
|
|
931
|
+
JSXElement() { unsupported = true; },
|
|
932
|
+
AwaitExpression() { unsupported = true; },
|
|
933
|
+
});
|
|
934
|
+
if ((init.isCallExpression() && !supportedCall(init, fn)) || init.isNewExpression() || init.isJSXElement() || unsupported)
|
|
935
|
+
throw init.buildCodeFrameError('runOnce: derived calls, mutations, and JSX need further compiler analysis');
|
|
936
|
+
reactive.set(binding, fn);
|
|
937
|
+
added = true;
|
|
938
|
+
const factory = t.arrowFunctionExpression([], init.node);
|
|
939
|
+
generated.add(factory);
|
|
940
|
+
init.replaceWith(t.callExpression(derive(), [factory]));
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
// Derived bindings (for example `done`) become known during the fixed-point
|
|
945
|
+
// pass above, so complete the same fixed-row lowering for those fields now.
|
|
946
|
+
program.traverse({
|
|
947
|
+
CallExpression(path) {
|
|
948
|
+
const fn = path.getFunctionParent();
|
|
949
|
+
const callee = path.get('callee');
|
|
950
|
+
if (!fn || !components.has(fn) || !callee.isMemberExpression() || callee.node.computed
|
|
951
|
+
|| !callee.get('property').isIdentifier({ name: 'map' }))
|
|
952
|
+
return;
|
|
953
|
+
const source = callee.get('object');
|
|
954
|
+
if (!source.isArrayExpression())
|
|
955
|
+
return;
|
|
956
|
+
for (const element of source.get('elements')) {
|
|
957
|
+
if (!element?.isObjectExpression())
|
|
958
|
+
continue;
|
|
959
|
+
for (const property of element.get('properties')) {
|
|
960
|
+
if (!property.isObjectProperty() || property.node.computed)
|
|
961
|
+
continue;
|
|
962
|
+
const value = property.get('value');
|
|
963
|
+
if (value.isFunctionExpression() || value.isArrowFunctionExpression())
|
|
964
|
+
continue;
|
|
965
|
+
let depends = value.isReferencedIdentifier()
|
|
966
|
+
&& reactive.has(value.scope.getBinding(value.node.name));
|
|
967
|
+
value.traverse({ ReferencedIdentifier(ref) {
|
|
968
|
+
if (reactive.has(ref.scope.getBinding(ref.node.name)))
|
|
969
|
+
depends = true;
|
|
970
|
+
} });
|
|
971
|
+
if (!depends)
|
|
972
|
+
continue;
|
|
973
|
+
const getter = t.arrowFunctionExpression([], value.node);
|
|
974
|
+
getter.extra = { rrjsReactiveGetter: true };
|
|
975
|
+
generated.add(getter);
|
|
976
|
+
value.replaceWith(getter);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
// Fixed source rows may carry reactive fields as generated getters. Restore
|
|
982
|
+
// ordinary JavaScript value semantics inside the mapper by invoking exactly
|
|
983
|
+
// those fields. Normalize every row to the same getter shape first so a
|
|
984
|
+
// mapper cannot sometimes receive a value and sometimes a function.
|
|
985
|
+
program.traverse({
|
|
986
|
+
CallExpression(path) {
|
|
987
|
+
const callee = path.get('callee');
|
|
988
|
+
if (!callee.isMemberExpression() || callee.node.computed
|
|
989
|
+
|| !callee.get('property').isIdentifier({ name: 'map' }))
|
|
990
|
+
return;
|
|
991
|
+
const source = callee.get('object');
|
|
992
|
+
const callback = path.get('arguments.0');
|
|
993
|
+
if (!source.isArrayExpression() || !callback?.isArrowFunctionExpression())
|
|
994
|
+
return;
|
|
995
|
+
const objects = source.get('elements').filter((element) => Boolean(element?.isObjectExpression()));
|
|
996
|
+
if (objects.length !== source.node.elements.length) {
|
|
997
|
+
let reactiveElement = false;
|
|
998
|
+
source.traverse({ ReferencedIdentifier(ref) {
|
|
999
|
+
if (reactive.has(ref.scope.getBinding(ref.node.name)))
|
|
1000
|
+
reactiveElement = true;
|
|
1001
|
+
} });
|
|
1002
|
+
if (reactiveElement)
|
|
1003
|
+
throw source.buildCodeFrameError('runOnce: reactive primitive fixed-map elements require explicit value compilation');
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const keyOf = (property) => {
|
|
1007
|
+
if (!property.isObjectProperty() || property.node.computed)
|
|
1008
|
+
return undefined;
|
|
1009
|
+
return t.isIdentifier(property.node.key) ? property.node.key.name
|
|
1010
|
+
: t.isStringLiteral(property.node.key) ? property.node.key.value : undefined;
|
|
1011
|
+
};
|
|
1012
|
+
const dynamic = new Set();
|
|
1013
|
+
for (const object of objects)
|
|
1014
|
+
for (const property of object.get('properties')) {
|
|
1015
|
+
const key = keyOf(property);
|
|
1016
|
+
const value = property.isObjectProperty() ? property.get('value') : undefined;
|
|
1017
|
+
if (key && value?.isArrowFunctionExpression() && value.node.extra?.rrjsReactiveGetter)
|
|
1018
|
+
dynamic.add(key);
|
|
1019
|
+
}
|
|
1020
|
+
if (!dynamic.size)
|
|
1021
|
+
return;
|
|
1022
|
+
const parameter = callback.get('params.0');
|
|
1023
|
+
if (!parameter?.isIdentifier())
|
|
1024
|
+
throw callback.buildCodeFrameError('runOnce: reactive fixed-map rows require an identifier parameter');
|
|
1025
|
+
for (const object of objects) {
|
|
1026
|
+
const fields = new Map(object.get('properties').map(property => [keyOf(property), property]));
|
|
1027
|
+
for (const key of dynamic) {
|
|
1028
|
+
const property = fields.get(key);
|
|
1029
|
+
if (!property?.isObjectProperty())
|
|
1030
|
+
throw object.buildCodeFrameError('runOnce: reactive fixed-map rows require a consistent direct-property shape');
|
|
1031
|
+
const value = property.get('value');
|
|
1032
|
+
if (value.isArrowFunctionExpression() && value.node.extra?.rrjsReactiveGetter)
|
|
1033
|
+
continue;
|
|
1034
|
+
const getter = t.arrowFunctionExpression([], value.node);
|
|
1035
|
+
getter.extra = { rrjsReactiveGetter: true };
|
|
1036
|
+
generated.add(getter);
|
|
1037
|
+
value.replaceWith(getter);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const parameterBinding = callback.scope.getBinding(parameter.node.name);
|
|
1041
|
+
callback.traverse({
|
|
1042
|
+
MemberExpression(member) {
|
|
1043
|
+
const object = member.get('object');
|
|
1044
|
+
const property = member.get('property');
|
|
1045
|
+
if (!object.isIdentifier() || object.scope.getBinding(object.node.name) !== parameterBinding)
|
|
1046
|
+
return;
|
|
1047
|
+
const key = !member.node.computed && property.isIdentifier() ? property.node.name
|
|
1048
|
+
: member.node.computed && property.isStringLiteral() ? property.node.value : undefined;
|
|
1049
|
+
if (!key || !dynamic.has(key))
|
|
1050
|
+
return;
|
|
1051
|
+
const keyAttribute = member.findParent(parent => parent.isJSXAttribute()
|
|
1052
|
+
&& t.isJSXIdentifier(parent.node.name, { name: 'key' }));
|
|
1053
|
+
if (keyAttribute)
|
|
1054
|
+
throw member.buildCodeFrameError('runOnce: reactive fixed-map keys are unsupported without reconciliation');
|
|
1055
|
+
if (member.parentPath.isCallExpression() && member.key === 'callee')
|
|
1056
|
+
return;
|
|
1057
|
+
member.replaceWith(t.callExpression(member.node, []));
|
|
1058
|
+
member.skip();
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
// Rebuild references after moving initializer nodes beneath generated
|
|
1064
|
+
// factories, including the direct-alias case `const doubled = count`.
|
|
1065
|
+
const identities = [...reactive].map(([binding, component]) => ({ scope: binding.scope, name: binding.identifier.name, component }));
|
|
1066
|
+
const operationIdentities = [...operationStates].map(binding => ({ scope: binding.scope, name: binding.identifier.name }));
|
|
1067
|
+
const setterIdentities = [...stateSetters].map(([setter, state]) => ({
|
|
1068
|
+
scope: setter.scope,
|
|
1069
|
+
setter: setter.identifier.name,
|
|
1070
|
+
stateScope: state.scope,
|
|
1071
|
+
state: state.identifier.name,
|
|
1072
|
+
}));
|
|
1073
|
+
program.scope.crawl();
|
|
1074
|
+
reactive.clear();
|
|
1075
|
+
for (const { scope, name, component } of identities)
|
|
1076
|
+
reactive.set(scope.getBinding(name), component);
|
|
1077
|
+
operationStates.clear();
|
|
1078
|
+
for (const { scope, name } of operationIdentities) {
|
|
1079
|
+
const binding = scope.getBinding(name);
|
|
1080
|
+
if (binding)
|
|
1081
|
+
operationStates.add(binding);
|
|
1082
|
+
}
|
|
1083
|
+
stateSetters.clear();
|
|
1084
|
+
for (const identity of setterIdentities) {
|
|
1085
|
+
const setter = identity.scope.getBinding(identity.setter);
|
|
1086
|
+
const state = identity.stateScope.getBinding(identity.state);
|
|
1087
|
+
if (setter && state)
|
|
1088
|
+
stateSetters.set(setter, state);
|
|
1089
|
+
}
|
|
1090
|
+
// Give every `useEffect(fn)` the dependency list its callback implies: each
|
|
1091
|
+
// reactive binding the callback reads, in first-read order. The existing
|
|
1092
|
+
// changing-dependency machinery then does the rest -- the list is read live,
|
|
1093
|
+
// and the callback sees a fresh snapshot per run.
|
|
1094
|
+
//
|
|
1095
|
+
// This is deliberately narrower than React, which re-runs such an effect on
|
|
1096
|
+
// every render including ones it has no stake in. Reading nothing reactive
|
|
1097
|
+
// therefore yields `[]` and the effect runs once, which is the only thing
|
|
1098
|
+
// that can be true when the body never runs a second time.
|
|
1099
|
+
program.traverse({
|
|
1100
|
+
CallExpression(path) {
|
|
1101
|
+
if (!inferredEffects.has(path.node))
|
|
1102
|
+
return;
|
|
1103
|
+
const effectCall = path.node;
|
|
1104
|
+
const callback = path.get('arguments')[0];
|
|
1105
|
+
const component = path.getFunctionParent();
|
|
1106
|
+
const dependencies = [];
|
|
1107
|
+
const seen = new Set();
|
|
1108
|
+
callback.traverse({
|
|
1109
|
+
ReferencedIdentifier(reference) {
|
|
1110
|
+
if (generated.has(reference.node))
|
|
1111
|
+
return;
|
|
1112
|
+
const binding = reference.scope.getBinding(reference.node.name);
|
|
1113
|
+
if (!binding || seen.has(binding))
|
|
1114
|
+
return;
|
|
1115
|
+
if (!component || reactive.get(binding) !== component)
|
|
1116
|
+
return;
|
|
1117
|
+
seen.add(binding);
|
|
1118
|
+
// Emit the live read directly. The reference-rewriting pass has
|
|
1119
|
+
// already collected its work from an earlier scope crawl, so a bare
|
|
1120
|
+
// identifier added here would stay the getter itself -- an identity
|
|
1121
|
+
// that never changes, and an effect that never re-runs.
|
|
1122
|
+
dependencies.push(t.callExpression(t.identifier(binding.identifier.name), []));
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
path.node.arguments.push(t.arrayExpression(dependencies));
|
|
1126
|
+
if (dependencies.length > 0)
|
|
1127
|
+
changingEffects.add(effectCall);
|
|
1128
|
+
},
|
|
1129
|
+
});
|
|
1130
|
+
const snapshots = new Map();
|
|
1131
|
+
const snapshotFor = (origin, binding) => {
|
|
1132
|
+
const component = reactive.get(binding);
|
|
1133
|
+
let callback;
|
|
1134
|
+
let ancestor = origin.parentPath;
|
|
1135
|
+
while (ancestor && ancestor !== component) {
|
|
1136
|
+
if (ancestor.isFunction() && !generated.has(ancestor.node))
|
|
1137
|
+
callback = ancestor;
|
|
1138
|
+
ancestor = ancestor.parentPath;
|
|
1139
|
+
}
|
|
1140
|
+
if (!callback)
|
|
1141
|
+
throw origin.buildCodeFrameError('runOnce: direct state snapshots require an event callback');
|
|
1142
|
+
const bindings = snapshots.get(callback) ?? new Map();
|
|
1143
|
+
let snapshot = bindings.get(binding);
|
|
1144
|
+
if (!snapshot) {
|
|
1145
|
+
snapshot = callback.scope.generateUidIdentifier(binding.identifier.name);
|
|
1146
|
+
bindings.set(binding, snapshot);
|
|
1147
|
+
snapshots.set(callback, bindings);
|
|
1148
|
+
}
|
|
1149
|
+
return t.cloneNode(snapshot);
|
|
1150
|
+
};
|
|
1151
|
+
// Preserve direct state-value list operations before event capture rewrites
|
|
1152
|
+
// their reads into snapshot locals.
|
|
1153
|
+
program.traverse({
|
|
1154
|
+
CallExpression(path) {
|
|
1155
|
+
const callee = path.get('callee');
|
|
1156
|
+
if (!callee.isIdentifier() || path.node.arguments.length !== 1)
|
|
1157
|
+
return;
|
|
1158
|
+
const setter = callee.scope.getBinding(callee.node.name);
|
|
1159
|
+
const state = setter ? stateSetters.get(setter) : undefined;
|
|
1160
|
+
if (!state)
|
|
1161
|
+
return;
|
|
1162
|
+
const argument = path.get('arguments.0');
|
|
1163
|
+
const isStateReference = (node) => t.isIdentifier(node) && path.scope.getBinding(node.name) === state;
|
|
1164
|
+
if (argument?.isIdentifier()) {
|
|
1165
|
+
const copied = argument.scope.getBinding(argument.node.name);
|
|
1166
|
+
const declaration = copied?.path.isVariableDeclarator() ? copied.path : undefined;
|
|
1167
|
+
const declarationStatement = declaration?.parentPath;
|
|
1168
|
+
const setterStatement = path.parentPath;
|
|
1169
|
+
const block = setterStatement?.parentPath;
|
|
1170
|
+
if (declaration && declarationStatement?.isVariableDeclaration()
|
|
1171
|
+
&& setterStatement?.isExpressionStatement() && block?.isBlockStatement()) {
|
|
1172
|
+
const init = declaration.get('init');
|
|
1173
|
+
const statements = block.get('body');
|
|
1174
|
+
const declarationIndex = statements.findIndex(statement => statement.node === declarationStatement.node);
|
|
1175
|
+
const setterIndex = statements.findIndex(statement => statement.node === setterStatement.node);
|
|
1176
|
+
const removeStatement = statements[declarationIndex + 1];
|
|
1177
|
+
const insertStatement = statements[declarationIndex + 2];
|
|
1178
|
+
const removeDeclaration = removeStatement?.isVariableDeclaration()
|
|
1179
|
+
&& removeStatement.node.declarations.length === 1 ? removeStatement.get('declarations.0') : undefined;
|
|
1180
|
+
const removeInit = removeDeclaration?.isVariableDeclarator() ? removeDeclaration.get('init') : undefined;
|
|
1181
|
+
const removedId = removeDeclaration?.isVariableDeclarator() && t.isArrayPattern(removeDeclaration.node.id)
|
|
1182
|
+
? removeDeclaration.node.id.elements[0] : undefined;
|
|
1183
|
+
const removeCall = removeInit?.isCallExpression() ? removeInit : undefined;
|
|
1184
|
+
const insertCall = insertStatement?.isExpressionStatement() && insertStatement.get('expression').isCallExpression()
|
|
1185
|
+
? insertStatement.get('expression') : undefined;
|
|
1186
|
+
const matchesSplice = (call) => {
|
|
1187
|
+
const callee = call?.get('callee');
|
|
1188
|
+
return Boolean(callee?.isMemberExpression() && !callee.node.computed
|
|
1189
|
+
&& callee.get('property').isIdentifier({ name: 'splice' })
|
|
1190
|
+
&& callee.get('object').isIdentifier({ name: argument.node.name })
|
|
1191
|
+
&& callee.get('object').scope.getBinding(argument.node.name) === copied);
|
|
1192
|
+
};
|
|
1193
|
+
const sliceCallee = init.isCallExpression() ? init.get('callee') : undefined;
|
|
1194
|
+
const copiedState = (init.isArrayExpression() && init.node.elements.length === 1
|
|
1195
|
+
&& t.isSpreadElement(init.node.elements[0]) && isStateReference(init.node.elements[0].argument))
|
|
1196
|
+
|| (init.isCallExpression() && init.node.arguments.length === 0
|
|
1197
|
+
&& sliceCallee?.isMemberExpression() && !sliceCallee.node.computed
|
|
1198
|
+
&& sliceCallee.get('property').isIdentifier({ name: 'slice' })
|
|
1199
|
+
&& sliceCallee.get('object').isIdentifier()
|
|
1200
|
+
&& isStateReference(sliceCallee.get('object').node));
|
|
1201
|
+
const from = removeCall?.get('arguments.0');
|
|
1202
|
+
const to = insertCall?.get('arguments.0');
|
|
1203
|
+
const guarded = from?.isIdentifier() && to?.isIdentifier() && statements.slice(0, declarationIndex).some(statement => {
|
|
1204
|
+
if (!statement.isIfStatement())
|
|
1205
|
+
return false;
|
|
1206
|
+
const consequent = statement.get('consequent');
|
|
1207
|
+
const exits = consequent.isReturnStatement() || (consequent.isBlockStatement()
|
|
1208
|
+
&& consequent.get('body').some(entry => entry.isReturnStatement()));
|
|
1209
|
+
if (!exits)
|
|
1210
|
+
return false;
|
|
1211
|
+
let fromLower = false, toLower = false, toUpper = false;
|
|
1212
|
+
statement.get('test').traverse({
|
|
1213
|
+
BinaryExpression(binary) {
|
|
1214
|
+
const left = binary.get('left'), right = binary.get('right');
|
|
1215
|
+
if (binary.node.operator === '<' && left.isIdentifier({ name: from.node.name })
|
|
1216
|
+
&& left.scope.getBinding(left.node.name) === from.scope.getBinding(from.node.name)
|
|
1217
|
+
&& right.isNumericLiteral({ value: 0 }))
|
|
1218
|
+
fromLower = true;
|
|
1219
|
+
if (binary.node.operator === '<' && left.isIdentifier({ name: to.node.name })
|
|
1220
|
+
&& left.scope.getBinding(left.node.name) === to.scope.getBinding(to.node.name)
|
|
1221
|
+
&& right.isNumericLiteral({ value: 0 }))
|
|
1222
|
+
toLower = true;
|
|
1223
|
+
if (binary.node.operator === '>=' && left.isIdentifier({ name: to.node.name })
|
|
1224
|
+
&& left.scope.getBinding(left.node.name) === to.scope.getBinding(to.node.name)
|
|
1225
|
+
&& right.isMemberExpression() && !right.node.computed
|
|
1226
|
+
&& right.get('object').isIdentifier()
|
|
1227
|
+
&& isStateReference(right.get('object').node)
|
|
1228
|
+
&& right.get('property').isIdentifier({ name: 'length' }))
|
|
1229
|
+
toUpper = true;
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
1232
|
+
return fromLower && toLower && toUpper;
|
|
1233
|
+
});
|
|
1234
|
+
const movedBinding = t.isIdentifier(removedId) ? removeDeclaration?.scope.getBinding(removedId.name) : undefined;
|
|
1235
|
+
if (copiedState
|
|
1236
|
+
&& declarationStatement.node.declarations.length === 1
|
|
1237
|
+
&& declarationIndex >= 0 && setterIndex === declarationIndex + 3
|
|
1238
|
+
&& copied?.referencePaths.length === 3 && movedBinding?.referencePaths.length === 1
|
|
1239
|
+
&& guarded
|
|
1240
|
+
&& matchesSplice(removeCall) && removeCall.node.arguments.length === 2
|
|
1241
|
+
&& t.isNumericLiteral(removeCall.node.arguments[1], { value: 1 })
|
|
1242
|
+
&& t.isIdentifier(removedId)
|
|
1243
|
+
&& matchesSplice(insertCall) && insertCall.node.arguments.length === 3
|
|
1244
|
+
&& t.isNumericLiteral(insertCall.node.arguments[1], { value: 0 })
|
|
1245
|
+
&& t.isIdentifier(insertCall.node.arguments[2], { name: removedId.name })
|
|
1246
|
+
&& t.isExpression(removeCall.node.arguments[0]) && t.isExpression(insertCall.node.arguments[0])) {
|
|
1247
|
+
operationStates.add(state);
|
|
1248
|
+
const move = t.callExpression(rendererHelper('listMove'), [
|
|
1249
|
+
snapshotFor(path, state),
|
|
1250
|
+
t.cloneNode(removeCall.node.arguments[0]),
|
|
1251
|
+
t.cloneNode(insertCall.node.arguments[0]),
|
|
1252
|
+
]);
|
|
1253
|
+
move.extra = { rrjsListOperation: true };
|
|
1254
|
+
path.node.arguments[0] = move;
|
|
1255
|
+
deferredRemovals.push(declarationStatement, removeStatement, insertStatement);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (argument?.isArrayExpression()) {
|
|
1261
|
+
const [head, ...tail] = argument.node.elements;
|
|
1262
|
+
if (t.isSpreadElement(head) && isStateReference(head.argument)
|
|
1263
|
+
&& tail.length > 0 && tail.every((element) => Boolean(element) && t.isExpression(element))) {
|
|
1264
|
+
operationStates.add(state);
|
|
1265
|
+
const operation = t.callExpression(rendererHelper('listAppend'), [
|
|
1266
|
+
snapshotFor(path, state),
|
|
1267
|
+
...tail.map(element => t.cloneNode(element)),
|
|
1268
|
+
]);
|
|
1269
|
+
operation.extra = { rrjsListOperation: true };
|
|
1270
|
+
path.node.arguments[0] = operation;
|
|
1271
|
+
}
|
|
1272
|
+
else if (t.isSpreadElement(head)) {
|
|
1273
|
+
throw argument.buildCodeFrameError('runOnce: array replacement from a non-state snapshot is unsupported without identity matching');
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
else if (argument?.isCallExpression() && t.isMemberExpression(argument.node.callee)
|
|
1277
|
+
&& !argument.node.callee.computed && isStateReference(argument.node.callee.object)
|
|
1278
|
+
&& t.isIdentifier(argument.node.callee.property, { name: 'filter' })
|
|
1279
|
+
&& argument.node.arguments.length === 1 && t.isExpression(argument.node.arguments[0])) {
|
|
1280
|
+
operationStates.add(state);
|
|
1281
|
+
const operation = t.callExpression(rendererHelper('listFilter'), [
|
|
1282
|
+
snapshotFor(path, state),
|
|
1283
|
+
t.cloneNode(argument.node.arguments[0]),
|
|
1284
|
+
]);
|
|
1285
|
+
operation.extra = { rrjsListOperation: true };
|
|
1286
|
+
path.node.arguments[0] = operation;
|
|
1287
|
+
}
|
|
1288
|
+
else if (argument?.isCallExpression() && t.isMemberExpression(argument.node.callee)
|
|
1289
|
+
&& !argument.node.callee.computed && isStateReference(argument.node.callee.object)
|
|
1290
|
+
&& t.isIdentifier(argument.node.callee.property, { name: 'map' })
|
|
1291
|
+
&& argument.node.arguments.length === 1 && t.isExpression(argument.node.arguments[0])) {
|
|
1292
|
+
operationStates.add(state);
|
|
1293
|
+
recordMapUpdater(state, argument.node.arguments[0]);
|
|
1294
|
+
const operation = t.callExpression(rendererHelper('listMap'), [
|
|
1295
|
+
snapshotFor(path, state),
|
|
1296
|
+
t.cloneNode(argument.node.arguments[0]),
|
|
1297
|
+
]);
|
|
1298
|
+
operation.extra = { rrjsListOperation: true };
|
|
1299
|
+
path.node.arguments[0] = operation;
|
|
1300
|
+
}
|
|
1301
|
+
},
|
|
1302
|
+
});
|
|
1303
|
+
const effectSnapshots = new Map();
|
|
1304
|
+
const changingEffectSnapshots = new Map();
|
|
1305
|
+
for (const [binding, component] of reactive) {
|
|
1306
|
+
for (const reference of [...binding.referencePaths]) {
|
|
1307
|
+
const componentName = component.isFunctionDeclaration() ? component.node.id?.name
|
|
1308
|
+
: component.parentPath.isVariableDeclarator() && t.isIdentifier(component.parentPath.node.id)
|
|
1309
|
+
? component.parentPath.node.id.name : undefined;
|
|
1310
|
+
const customReturn = /^use[A-Z]/.test(componentName ?? '')
|
|
1311
|
+
&& reference.parentPath?.isArrayExpression()
|
|
1312
|
+
&& reference.findParent(parent => parent.isReturnStatement()
|
|
1313
|
+
&& parent.getFunctionParent() === component);
|
|
1314
|
+
if (customReturn)
|
|
1315
|
+
continue;
|
|
1316
|
+
const changingDeps = reference.findParent(parent => parent.isArrayExpression()
|
|
1317
|
+
&& parent.parentPath.isCallExpression() && changingEffects.has(parent.parentPath.node));
|
|
1318
|
+
if (changingDeps) {
|
|
1319
|
+
reference.replaceWith(t.callExpression(t.identifier(binding.identifier.name), []));
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (reference.parentPath?.isCallExpression() && reference.key === 'callee'
|
|
1323
|
+
&& [...(componentProps.get(component)?.values() ?? [])].includes(binding)) {
|
|
1324
|
+
reference.replaceWith(t.callExpression(t.identifier(binding.identifier.name), []));
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
// Find the outermost callback below this component. Nested asynchronous
|
|
1328
|
+
// callbacks then close over its snapshot rather than reading future state.
|
|
1329
|
+
let callback;
|
|
1330
|
+
let ancestor = reference.parentPath;
|
|
1331
|
+
let inGenerated = false;
|
|
1332
|
+
while (ancestor && ancestor !== component) {
|
|
1333
|
+
if (generated.has(ancestor.node) || ancestor.node.extra?.rrjsReactiveGetter)
|
|
1334
|
+
inGenerated = true;
|
|
1335
|
+
else if (ancestor.isFunction())
|
|
1336
|
+
callback = ancestor;
|
|
1337
|
+
ancestor = ancestor.parentPath;
|
|
1338
|
+
}
|
|
1339
|
+
if (callback && !inGenerated) {
|
|
1340
|
+
const parent = callback.parentPath;
|
|
1341
|
+
const args = parent.isCallExpression() ? parent.get('arguments') : [];
|
|
1342
|
+
const effectCallback = parent.isCallExpression() && hookName(parent)
|
|
1343
|
+
=== 'useEffect' && args[0] === callback && args[1]?.isArrayExpression()
|
|
1344
|
+
&& args[1].node.elements.length === 0;
|
|
1345
|
+
const changingEffectCallback = parent.isCallExpression()
|
|
1346
|
+
&& changingEffects.has(parent.node) && args[0] === callback;
|
|
1347
|
+
if (changingEffectCallback) {
|
|
1348
|
+
const bindings = changingEffectSnapshots.get(callback) ?? new Map();
|
|
1349
|
+
let snapshot = bindings.get(binding);
|
|
1350
|
+
if (!snapshot) {
|
|
1351
|
+
snapshot = callback.scope.generateUidIdentifier(binding.identifier.name);
|
|
1352
|
+
bindings.set(binding, snapshot);
|
|
1353
|
+
changingEffectSnapshots.set(callback, bindings);
|
|
1354
|
+
}
|
|
1355
|
+
reference.replaceWith(t.cloneNode(snapshot));
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const listCallback = parent.isCallExpression() && (() => {
|
|
1359
|
+
const callee = parent.get('callee');
|
|
1360
|
+
if (!callee.isMemberExpression() || callee.node.computed
|
|
1361
|
+
|| !callee.get('property').isIdentifier({ name: 'map' }))
|
|
1362
|
+
return false;
|
|
1363
|
+
return (() => {
|
|
1364
|
+
const source = parent.get('callee.object');
|
|
1365
|
+
const identifier = source.isIdentifier() ? source
|
|
1366
|
+
: source.isCallExpression() && source.get('callee').isIdentifier()
|
|
1367
|
+
? source.get('callee') : undefined;
|
|
1368
|
+
return Boolean(identifier && operationStates.has(identifier.scope.getBinding(identifier.node.name)));
|
|
1369
|
+
})();
|
|
1370
|
+
})();
|
|
1371
|
+
if (listCallback) {
|
|
1372
|
+
reference.replaceWith(t.callExpression(t.identifier(binding.identifier.name), []));
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (effectCallback) {
|
|
1376
|
+
const bindings = effectSnapshots.get(callback) ?? new Map();
|
|
1377
|
+
let snapshot = bindings.get(binding);
|
|
1378
|
+
if (!snapshot) {
|
|
1379
|
+
snapshot = callback.scope.generateUidIdentifier(binding.identifier.name);
|
|
1380
|
+
bindings.set(binding, snapshot);
|
|
1381
|
+
}
|
|
1382
|
+
effectSnapshots.set(callback, bindings);
|
|
1383
|
+
reference.replaceWith(t.cloneNode(snapshot));
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (parent.isCallExpression())
|
|
1387
|
+
throw callback.buildCodeFrameError('runOnce: reactive callback arguments require explicit effect/callback compilation');
|
|
1388
|
+
const bindings = snapshots.get(callback) ?? new Map();
|
|
1389
|
+
let snapshot = bindings.get(binding);
|
|
1390
|
+
if (!snapshot) {
|
|
1391
|
+
snapshot = callback.scope.generateUidIdentifier(binding.identifier.name);
|
|
1392
|
+
bindings.set(binding, snapshot);
|
|
1393
|
+
}
|
|
1394
|
+
snapshots.set(callback, bindings);
|
|
1395
|
+
reference.replaceWith(t.cloneNode(snapshot));
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
const jsx = reference.findParent(parent => parent.isJSXExpressionContainer());
|
|
1399
|
+
const rootReturn = reference.findParent(parent => parent.isReturnStatement() && parent.getFunctionParent() === component);
|
|
1400
|
+
const conditionalRoot = rootReturn?.isReturnStatement() && t.isConditionalExpression(rootReturn.node.argument);
|
|
1401
|
+
if (!inGenerated && !jsx && !conditionalRoot)
|
|
1402
|
+
throw reference.buildCodeFrameError('runOnce: state-dependent component control flow is not yet supported; use a JSX expression or const derivation');
|
|
1403
|
+
reference.replaceWith(t.callExpression(t.identifier(binding.identifier.name), []));
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const [callback, bindings] of effectSnapshots) {
|
|
1408
|
+
const call = callback.parentPath;
|
|
1409
|
+
const statement = call.findParent(parent => parent.isStatement());
|
|
1410
|
+
if (!statement)
|
|
1411
|
+
throw callback.buildCodeFrameError('runOnce: useEffect must be a component statement');
|
|
1412
|
+
// An effect callback may close over a state binding declared later in the
|
|
1413
|
+
// component. Register the callback in its original hook position, but fill
|
|
1414
|
+
// its snapshot after each captured binding has been initialized. Passive
|
|
1415
|
+
// work cannot run until after component construction and commit.
|
|
1416
|
+
const component = reactive.get(bindings.keys().next().value);
|
|
1417
|
+
const body = component.get('body');
|
|
1418
|
+
if (!body.isBlockStatement())
|
|
1419
|
+
throw callback.buildCodeFrameError('runOnce: effect snapshots require a block component body');
|
|
1420
|
+
body.unshiftContainer('body', t.variableDeclaration('let', [...bindings].map(([, snapshot]) => t.variableDeclarator(snapshot))));
|
|
1421
|
+
for (const [binding, snapshot] of bindings) {
|
|
1422
|
+
const declaration = binding.path.findParent(parent => parent.isStatement() && parent.getFunctionParent() === component);
|
|
1423
|
+
if (!declaration)
|
|
1424
|
+
throw callback.buildCodeFrameError('runOnce: effect snapshot binding must be initialized by a component statement');
|
|
1425
|
+
declaration.insertAfter(t.expressionStatement(t.assignmentExpression('=', t.cloneNode(snapshot), t.callExpression(t.identifier(binding.identifier.name), []))));
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
for (const [callback, bindings] of changingEffectSnapshots) {
|
|
1429
|
+
if (!t.isBlockStatement(callback.node.body)) {
|
|
1430
|
+
callback.node.body = t.blockStatement([t.returnStatement(callback.node.body)]);
|
|
1431
|
+
}
|
|
1432
|
+
callback.node.body.body.unshift(t.variableDeclaration('const', [...bindings].map(([binding, snapshot]) => t.variableDeclarator(snapshot, t.callExpression(t.identifier(binding.identifier.name), [])))));
|
|
1433
|
+
}
|
|
1434
|
+
for (const effectCall of changingEffects) {
|
|
1435
|
+
const deps = effectCall.arguments[1];
|
|
1436
|
+
if (t.isArrayExpression(deps))
|
|
1437
|
+
effectCall.arguments[1] = t.arrowFunctionExpression([], deps);
|
|
1438
|
+
}
|
|
1439
|
+
for (const [callback, bindings] of snapshots) {
|
|
1440
|
+
const eventReference = (path) => {
|
|
1441
|
+
const container = path.parentPath;
|
|
1442
|
+
const attribute = container?.parentPath;
|
|
1443
|
+
return Boolean(container?.isJSXExpressionContainer() && attribute?.isJSXAttribute() && t.isJSXIdentifier(attribute.node.name) && /^on[A-Z]/.test(attribute.node.name.name));
|
|
1444
|
+
};
|
|
1445
|
+
const eventUse = (path) => eventReference(path)
|
|
1446
|
+
|| Boolean(path.findParent(parent => parent.isFunction() && eventReference(parent)));
|
|
1447
|
+
let references = [];
|
|
1448
|
+
if (callback.isFunctionDeclaration() && callback.node.id)
|
|
1449
|
+
references = callback.parentPath.scope.getBinding(callback.node.id.name)?.referencePaths ?? [];
|
|
1450
|
+
else if (callback.parentPath.isVariableDeclarator() && t.isIdentifier(callback.parentPath.node.id))
|
|
1451
|
+
references = callback.parentPath.scope.getBinding(callback.parentPath.node.id.name)?.referencePaths ?? [];
|
|
1452
|
+
if (!eventReference(callback) && (!references.length || references.some(reference => !eventUse(reference))))
|
|
1453
|
+
throw callback.buildCodeFrameError('runOnce: state-capturing functions must be DOM event handlers; escaping callbacks need snapshot compilation');
|
|
1454
|
+
if (!t.isBlockStatement(callback.node.body))
|
|
1455
|
+
callback.node.body = t.blockStatement([t.returnStatement(callback.node.body)]);
|
|
1456
|
+
callback.node.body.body.unshift(t.variableDeclaration('const', [...bindings].map(([binding, snapshot]) => t.variableDeclarator(snapshot, t.callExpression(t.identifier(binding.identifier.name), [])))));
|
|
1457
|
+
}
|
|
1458
|
+
// Build a lazy selector per region. Arm factories execute only on a real
|
|
1459
|
+
// selection change; nested bindings own their own subscriptions.
|
|
1460
|
+
// A portal call is also structural: selecting it constructs an externally
|
|
1461
|
+
// placed subtree whose lifetime is represented by its source-tree anchor.
|
|
1462
|
+
// Mark only an unbound classic-harness helper or an import whose binding is
|
|
1463
|
+
// the named renderer/react-dom export; a same-spelled local is ordinary code.
|
|
1464
|
+
program.traverse({
|
|
1465
|
+
CallExpression(path) {
|
|
1466
|
+
const callee = path.get('callee');
|
|
1467
|
+
if (!callee.isIdentifier({ name: 'createPortal' }))
|
|
1468
|
+
return;
|
|
1469
|
+
const binding = callee.scope.getBinding(callee.node.name);
|
|
1470
|
+
if (binding) {
|
|
1471
|
+
if (!binding.path.isImportSpecifier())
|
|
1472
|
+
return;
|
|
1473
|
+
const imported = binding.path.node.imported;
|
|
1474
|
+
const importedName = t.isIdentifier(imported) ? imported.name : imported.value;
|
|
1475
|
+
const declaration = binding.path.parentPath;
|
|
1476
|
+
if (importedName !== 'createPortal' || !declaration.isImportDeclaration()
|
|
1477
|
+
|| !['react-dom', '@rrjs/renderer'].includes(declaration.node.source.value))
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (path.node.arguments.length < 2 || !t.isExpression(path.node.arguments[1])) {
|
|
1481
|
+
throw path.buildCodeFrameError('runOnce: createPortal requires a child and a stable target identifier');
|
|
1482
|
+
}
|
|
1483
|
+
const target = path.get('arguments.1');
|
|
1484
|
+
if (!target.isIdentifier()) {
|
|
1485
|
+
throw target.buildCodeFrameError('runOnce: createPortal target must be a stable identifier');
|
|
1486
|
+
}
|
|
1487
|
+
const targetBinding = target.scope.getBinding(target.node.name);
|
|
1488
|
+
if (targetBinding && (reactive.has(targetBinding) || !targetBinding.constant)) {
|
|
1489
|
+
throw target.buildCodeFrameError('runOnce: changing createPortal targets are unsupported');
|
|
1490
|
+
}
|
|
1491
|
+
if (path.node.arguments.length > 2 && !t.isNullLiteral(path.node.arguments[2])) {
|
|
1492
|
+
throw path.get('arguments.2').buildCodeFrameError('runOnce: createPortal keys are unsupported');
|
|
1493
|
+
}
|
|
1494
|
+
path.node.extra = { ...(path.node.extra ?? {}), rrjsRegion: true };
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
const structural = (node) => {
|
|
1498
|
+
if (t.isJSXElement(node) || t.isJSXFragment(node) || node.extra?.rrjsRegion)
|
|
1499
|
+
return true;
|
|
1500
|
+
if (t.isConditionalExpression(node))
|
|
1501
|
+
return structural(node.consequent) || structural(node.alternate);
|
|
1502
|
+
if (t.isLogicalExpression(node))
|
|
1503
|
+
return structural(node.left) || structural(node.right);
|
|
1504
|
+
return false;
|
|
1505
|
+
};
|
|
1506
|
+
const children = (node) => node.children.filter(child => !t.isJSXText(child) || child.value.trim().length > 0 || !/[\r\n]/.test(child.value));
|
|
1507
|
+
const childExpr = (child) => !child ? t.nullLiteral()
|
|
1508
|
+
: t.isJSXText(child) ? t.stringLiteral(child.value)
|
|
1509
|
+
: t.isJSXExpressionContainer(child) ? t.isJSXEmptyExpression(child.expression) ? t.nullLiteral() : child.expression
|
|
1510
|
+
: t.isJSXSpreadChild(child) ? child.expression : child;
|
|
1511
|
+
function gateByLazyAncestors(origin, test) {
|
|
1512
|
+
const component = origin.getFunctionParent();
|
|
1513
|
+
let gated = test;
|
|
1514
|
+
let cursor = origin;
|
|
1515
|
+
while (cursor?.parentPath && cursor.parentPath !== component) {
|
|
1516
|
+
const parent = cursor.parentPath;
|
|
1517
|
+
if (parent.isConditionalExpression()) {
|
|
1518
|
+
if (cursor.key === 'consequent') {
|
|
1519
|
+
gated = t.conditionalExpression(t.cloneNode(parent.node.test), gated, t.booleanLiteral(false));
|
|
1520
|
+
}
|
|
1521
|
+
else if (cursor.key === 'alternate') {
|
|
1522
|
+
gated = t.conditionalExpression(t.cloneNode(parent.node.test), t.booleanLiteral(false), gated);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
else if (parent.isLogicalExpression() && cursor.key === 'right') {
|
|
1526
|
+
if (parent.node.operator === '&&') {
|
|
1527
|
+
gated = t.conditionalExpression(t.cloneNode(parent.node.left), gated, t.booleanLiteral(false));
|
|
1528
|
+
}
|
|
1529
|
+
else if (parent.node.operator === '||') {
|
|
1530
|
+
gated = t.conditionalExpression(t.cloneNode(parent.node.left), t.booleanLiteral(false), gated);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
cursor = parent;
|
|
1534
|
+
}
|
|
1535
|
+
return gated;
|
|
1536
|
+
}
|
|
1537
|
+
function cacheCondition(origin, test) {
|
|
1538
|
+
const component = origin.getFunctionParent();
|
|
1539
|
+
const statement = origin.findParent(parent => parent.isStatement() && parent.getFunctionParent() === component);
|
|
1540
|
+
if (!component || !components.has(component) || !statement) {
|
|
1541
|
+
throw origin.buildCodeFrameError('runOnce: common branch conditions require a component statement');
|
|
1542
|
+
}
|
|
1543
|
+
const binding = component.scope.generateUidIdentifier('condition');
|
|
1544
|
+
const factory = t.arrowFunctionExpression([], gateByLazyAncestors(origin, test));
|
|
1545
|
+
statement.insertBefore(t.variableDeclaration('const', [
|
|
1546
|
+
t.variableDeclarator(t.cloneNode(binding), t.callExpression(derive(), [factory])),
|
|
1547
|
+
]));
|
|
1548
|
+
return t.callExpression(t.cloneNode(binding), []);
|
|
1549
|
+
}
|
|
1550
|
+
function select(test, yes, no, origin, passthrough, cached = false) {
|
|
1551
|
+
// Unkeyed fragment syntax has the same positional identity semantics as its
|
|
1552
|
+
// single child. Peel that transparent layer before merging common elements.
|
|
1553
|
+
if (t.isJSXFragment(yes) && t.isJSXFragment(no)) {
|
|
1554
|
+
const yesChildren = children(yes), noChildren = children(no);
|
|
1555
|
+
if (yesChildren.length === 1 && noChildren.length === 1) {
|
|
1556
|
+
return select(test, childExpr(yesChildren[0]), childExpr(noChildren[0]), origin, passthrough, cached);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
// Compile common native structure once. This comparison is at compile time;
|
|
1560
|
+
// the generated runtime has no old/new element-tree matching operation.
|
|
1561
|
+
if (t.isJSXElement(yes) && t.isJSXElement(no)
|
|
1562
|
+
&& t.isJSXIdentifier(yes.openingElement.name) && t.isJSXIdentifier(no.openingElement.name)
|
|
1563
|
+
&& yes.openingElement.name.name === no.openingElement.name.name) {
|
|
1564
|
+
const attrs = (node) => new Map(node.openingElement.attributes
|
|
1565
|
+
.filter((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name))
|
|
1566
|
+
.map(attr => [attr.name.name, attr]));
|
|
1567
|
+
const a = attrs(yes), b = attrs(no);
|
|
1568
|
+
const same = (left, right) => !left || !right ? left === right : t.isNodesEquivalent(left, right);
|
|
1569
|
+
if (same(a.get('key'), b.get('key'))) {
|
|
1570
|
+
if (yes.openingElement.attributes.some(attr => t.isJSXSpreadAttribute(attr)) || no.openingElement.attributes.some(attr => t.isJSXSpreadAttribute(attr))) {
|
|
1571
|
+
throw program.buildCodeFrameError('runOnce: common branch elements with spread props require further analysis');
|
|
1572
|
+
}
|
|
1573
|
+
const condition = cached ? test : cacheCondition(origin, test);
|
|
1574
|
+
const expression = (attr) => !attr ? t.unaryExpression('void', t.numericLiteral(0))
|
|
1575
|
+
: !attr.value ? t.booleanLiteral(true)
|
|
1576
|
+
: t.isJSXExpressionContainer(attr.value) ? attr.value.expression : attr.value;
|
|
1577
|
+
const merged = t.cloneNode(yes);
|
|
1578
|
+
merged.openingElement.attributes = [...new Set([...a.keys(), ...b.keys()])].map(name => {
|
|
1579
|
+
if (same(a.get(name), b.get(name)))
|
|
1580
|
+
return t.cloneNode(a.get(name));
|
|
1581
|
+
if (/^on[A-Z]/.test(name) || name === 'ref')
|
|
1582
|
+
throw program.buildCodeFrameError('runOnce: changing event/ref props across common branch elements require commit compilation');
|
|
1583
|
+
return t.jsxAttribute(t.jsxIdentifier(name), t.jsxExpressionContainer(t.conditionalExpression(t.cloneNode(condition), expression(a.get(name)), expression(b.get(name)))));
|
|
1584
|
+
});
|
|
1585
|
+
const ac = children(yes), bc = children(no);
|
|
1586
|
+
merged.children = Array.from({ length: Math.max(ac.length, bc.length) }, (_, index) => {
|
|
1587
|
+
const left = childExpr(ac[index]), right = childExpr(bc[index]);
|
|
1588
|
+
return t.jsxExpressionContainer(structural(left) || structural(right)
|
|
1589
|
+
? select(t.cloneNode(condition), left, right, origin, undefined, true) : t.conditionalExpression(t.cloneNode(condition), left, right));
|
|
1590
|
+
});
|
|
1591
|
+
return merged;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const arm = (node, side) => passthrough === side
|
|
1595
|
+
? (() => {
|
|
1596
|
+
const condition = origin.scope.generateUidIdentifier('condition');
|
|
1597
|
+
return t.arrowFunctionExpression([condition], t.arrowFunctionExpression([], t.callExpression(t.cloneNode(condition), [])));
|
|
1598
|
+
})()
|
|
1599
|
+
: t.arrowFunctionExpression([], t.isJSXElement(node) || t.isJSXFragment(node) || node.extra?.rrjsRegion
|
|
1600
|
+
? node : t.arrowFunctionExpression([], node));
|
|
1601
|
+
const call = t.callExpression(choose(), [t.arrowFunctionExpression([], test), arm(yes, 'yes'), arm(no, 'no')]);
|
|
1602
|
+
call.extra = { rrjsRegion: true };
|
|
1603
|
+
return call;
|
|
1604
|
+
}
|
|
1605
|
+
program.traverse({
|
|
1606
|
+
ConditionalExpression: { exit(path) {
|
|
1607
|
+
if (!structural(path.node))
|
|
1608
|
+
return;
|
|
1609
|
+
path.replaceWith(select(path.node.test, path.node.consequent, path.node.alternate, path));
|
|
1610
|
+
path.skip();
|
|
1611
|
+
} },
|
|
1612
|
+
LogicalExpression: { exit(path) {
|
|
1613
|
+
if (!structural(path.node) || path.node.operator === '??')
|
|
1614
|
+
return;
|
|
1615
|
+
const { left, right, operator } = path.node;
|
|
1616
|
+
path.replaceWith(operator === '&&'
|
|
1617
|
+
? select(left, right, t.cloneNode(left), path, 'no')
|
|
1618
|
+
: select(left, t.cloneNode(left), right, path, 'yes'));
|
|
1619
|
+
path.skip();
|
|
1620
|
+
} },
|
|
1621
|
+
});
|
|
1622
|
+
// A direct local prop getter retains the parent's operation-bearing array
|
|
1623
|
+
// unchanged. Resolve bindings after scope rebuilding and propagate only that
|
|
1624
|
+
// exact identity edge; aliases, spreads and computed wrappers remain rejected.
|
|
1625
|
+
let propagated = true;
|
|
1626
|
+
while (propagated) {
|
|
1627
|
+
propagated = false;
|
|
1628
|
+
for (const edge of propSources) {
|
|
1629
|
+
const parent = edge.parentScope.getBinding(edge.parentName);
|
|
1630
|
+
const child = edge.childScope.getBinding(edge.childName);
|
|
1631
|
+
if (!parent || !child || operationStates.has(child))
|
|
1632
|
+
continue;
|
|
1633
|
+
operationStates.add(parent);
|
|
1634
|
+
operationStates.add(child);
|
|
1635
|
+
const parentMappers = mapUpdaters.get(parent);
|
|
1636
|
+
if (parentMappers)
|
|
1637
|
+
mapUpdaters.set(child, parentMappers);
|
|
1638
|
+
propagated = true;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// Phase 5 first slice: retain source-visible append provenance. The state
|
|
1642
|
+
// value remains an ordinary array; non-enumerable metadata carries the exact
|
|
1643
|
+
// operation to the owned list region.
|
|
1644
|
+
program.traverse({
|
|
1645
|
+
CallExpression: { exit(path) {
|
|
1646
|
+
const callee = path.get('callee');
|
|
1647
|
+
if (callee.isIdentifier()) {
|
|
1648
|
+
const setterBinding = callee.scope.getBinding(callee.node.name);
|
|
1649
|
+
if (setterBinding && stateSetters.has(setterBinding) && path.node.arguments.length === 1) {
|
|
1650
|
+
const stateBinding = stateSetters.get(setterBinding);
|
|
1651
|
+
const updater = path.get('arguments.0');
|
|
1652
|
+
const operationHelpers = new Set(['listAppend', 'listPrepend', 'listClear', 'listTruncate', 'listSplice', 'listReverse', 'listFilter', 'listSort', 'listMap', 'listMove']);
|
|
1653
|
+
const generatedOperation = updater?.isCallExpression() && updater.get('callee').isIdentifier() && (() => {
|
|
1654
|
+
const helper = updater.get('callee');
|
|
1655
|
+
if (operationHelpers.has(helper.node.name))
|
|
1656
|
+
return true;
|
|
1657
|
+
const helperBinding = helper.scope.getBinding(helper.node.name);
|
|
1658
|
+
const imported = helperBinding?.path.isImportSpecifier() ? helperBinding.path.node.imported : undefined;
|
|
1659
|
+
return t.isIdentifier(imported) && operationHelpers.has(imported.name);
|
|
1660
|
+
})();
|
|
1661
|
+
let recognized = Boolean(updater?.node.extra?.rrjsListOperation || generatedOperation);
|
|
1662
|
+
const isSnapshot = (node) => {
|
|
1663
|
+
if (!t.isIdentifier(node))
|
|
1664
|
+
return false;
|
|
1665
|
+
const binding = path.scope.getBinding(node.name);
|
|
1666
|
+
if (binding === stateBinding)
|
|
1667
|
+
return true;
|
|
1668
|
+
const init = binding?.path.isVariableDeclarator() ? binding.path.node.init : undefined;
|
|
1669
|
+
return Boolean(binding && t.isCallExpression(init) && init.arguments.length === 0
|
|
1670
|
+
&& t.isIdentifier(init.callee)
|
|
1671
|
+
&& binding.path.scope.getBinding(init.callee.name) === stateBinding);
|
|
1672
|
+
};
|
|
1673
|
+
if (updater?.isArrayExpression() && updater.node.elements.length === 0) {
|
|
1674
|
+
recognized = true;
|
|
1675
|
+
operationStates.add(stateBinding);
|
|
1676
|
+
path.node.arguments[0] = t.callExpression(rendererHelper('listClear'), [
|
|
1677
|
+
t.callExpression(t.identifier(stateBinding.identifier.name), []),
|
|
1678
|
+
]);
|
|
1679
|
+
}
|
|
1680
|
+
if (updater?.isArrayExpression()) {
|
|
1681
|
+
const [head, ...tail] = updater.node.elements;
|
|
1682
|
+
if (t.isSpreadElement(head) && isSnapshot(head.argument)
|
|
1683
|
+
&& tail.length > 0 && tail.every((element) => Boolean(element) && t.isExpression(element))) {
|
|
1684
|
+
recognized = true;
|
|
1685
|
+
operationStates.add(stateBinding);
|
|
1686
|
+
path.node.arguments[0] = t.callExpression(rendererHelper('listAppend'), [
|
|
1687
|
+
t.cloneNode(head.argument),
|
|
1688
|
+
...tail.map(element => t.cloneNode(element)),
|
|
1689
|
+
]);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (updater?.isCallExpression() && t.isMemberExpression(updater.node.callee)
|
|
1693
|
+
&& !updater.node.callee.computed && isSnapshot(updater.node.callee.object)
|
|
1694
|
+
&& t.isIdentifier(updater.node.callee.property, { name: 'filter' })
|
|
1695
|
+
&& updater.node.arguments.length === 1 && t.isExpression(updater.node.arguments[0])) {
|
|
1696
|
+
recognized = true;
|
|
1697
|
+
operationStates.add(stateBinding);
|
|
1698
|
+
path.node.arguments[0] = t.callExpression(rendererHelper('listFilter'), [
|
|
1699
|
+
t.cloneNode(updater.node.callee.object),
|
|
1700
|
+
t.cloneNode(updater.node.arguments[0]),
|
|
1701
|
+
]);
|
|
1702
|
+
}
|
|
1703
|
+
if (updater?.isCallExpression() && t.isMemberExpression(updater.node.callee)
|
|
1704
|
+
&& !updater.node.callee.computed && isSnapshot(updater.node.callee.object)
|
|
1705
|
+
&& t.isIdentifier(updater.node.callee.property, { name: 'map' })
|
|
1706
|
+
&& updater.node.arguments.length === 1 && t.isExpression(updater.node.arguments[0])) {
|
|
1707
|
+
recognized = true;
|
|
1708
|
+
operationStates.add(stateBinding);
|
|
1709
|
+
recordMapUpdater(stateBinding, updater.node.arguments[0]);
|
|
1710
|
+
path.node.arguments[0] = t.callExpression(rendererHelper('listMap'), [
|
|
1711
|
+
t.cloneNode(updater.node.callee.object),
|
|
1712
|
+
t.cloneNode(updater.node.arguments[0]),
|
|
1713
|
+
]);
|
|
1714
|
+
}
|
|
1715
|
+
if (updater?.isArrowFunctionExpression() && updater.node.params.length === 1
|
|
1716
|
+
&& t.isIdentifier(updater.node.params[0]) && t.isArrayExpression(updater.node.body)) {
|
|
1717
|
+
const elements = updater.node.body.elements;
|
|
1718
|
+
const [head, ...tail] = elements;
|
|
1719
|
+
const last = elements.at(-1);
|
|
1720
|
+
const prefix = elements.slice(0, -1);
|
|
1721
|
+
if (t.isSpreadElement(head) && t.isIdentifier(head.argument, { name: updater.node.params[0].name })
|
|
1722
|
+
&& tail.length > 0 && tail.every((element) => Boolean(element) && t.isExpression(element))) {
|
|
1723
|
+
recognized = true;
|
|
1724
|
+
operationStates.add(stateBinding);
|
|
1725
|
+
updater.node.body = t.callExpression(rendererHelper('listAppend'), [
|
|
1726
|
+
t.cloneNode(updater.node.params[0]),
|
|
1727
|
+
...tail.map(element => t.cloneNode(element)),
|
|
1728
|
+
]);
|
|
1729
|
+
}
|
|
1730
|
+
else if (t.isSpreadElement(last) && t.isIdentifier(last.argument, { name: updater.node.params[0].name })
|
|
1731
|
+
&& prefix.length > 0 && prefix.every((element) => Boolean(element) && t.isExpression(element))) {
|
|
1732
|
+
recognized = true;
|
|
1733
|
+
operationStates.add(stateBinding);
|
|
1734
|
+
updater.node.body = t.callExpression(rendererHelper('listPrepend'), [
|
|
1735
|
+
t.cloneNode(updater.node.params[0]),
|
|
1736
|
+
...prefix.map(element => t.cloneNode(element)),
|
|
1737
|
+
]);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (updater?.isArrowFunctionExpression() && updater.node.params.length === 1
|
|
1741
|
+
&& t.isIdentifier(updater.node.params[0]) && t.isCallExpression(updater.node.body)
|
|
1742
|
+
&& t.isMemberExpression(updater.node.body.callee) && !updater.node.body.callee.computed
|
|
1743
|
+
&& t.isIdentifier(updater.node.body.callee.object, { name: updater.node.params[0].name })
|
|
1744
|
+
&& t.isIdentifier(updater.node.body.callee.property, { name: 'slice' })
|
|
1745
|
+
&& updater.node.body.arguments.length === 2
|
|
1746
|
+
&& t.isNumericLiteral(updater.node.body.arguments[0], { value: 0 })
|
|
1747
|
+
&& t.isUnaryExpression(updater.node.body.arguments[1], { operator: '-' })
|
|
1748
|
+
&& t.isNumericLiteral(updater.node.body.arguments[1].argument, { value: 1 })) {
|
|
1749
|
+
recognized = true;
|
|
1750
|
+
operationStates.add(stateBinding);
|
|
1751
|
+
updater.node.body = t.callExpression(rendererHelper('listTruncate'), [
|
|
1752
|
+
t.cloneNode(updater.node.params[0]),
|
|
1753
|
+
t.binaryExpression('-', t.memberExpression(t.cloneNode(updater.node.params[0]), t.identifier('length')), t.numericLiteral(1)),
|
|
1754
|
+
]);
|
|
1755
|
+
}
|
|
1756
|
+
if (updater?.isArrowFunctionExpression() && updater.node.params.length === 1
|
|
1757
|
+
&& t.isIdentifier(updater.node.params[0]) && t.isCallExpression(updater.node.body)
|
|
1758
|
+
&& t.isMemberExpression(updater.node.body.callee) && !updater.node.body.callee.computed
|
|
1759
|
+
&& t.isIdentifier(updater.node.body.callee.object, { name: updater.node.params[0].name })
|
|
1760
|
+
&& t.isIdentifier(updater.node.body.callee.property, { name: 'toSpliced' })
|
|
1761
|
+
&& updater.node.body.arguments.length >= 2
|
|
1762
|
+
&& updater.node.body.arguments.every(argument => t.isExpression(argument))) {
|
|
1763
|
+
recognized = true;
|
|
1764
|
+
operationStates.add(stateBinding);
|
|
1765
|
+
updater.node.body = t.callExpression(rendererHelper('listSplice'), [
|
|
1766
|
+
t.cloneNode(updater.node.params[0]),
|
|
1767
|
+
...updater.node.body.arguments.map(argument => t.cloneNode(argument)),
|
|
1768
|
+
]);
|
|
1769
|
+
}
|
|
1770
|
+
if (updater?.isArrowFunctionExpression() && updater.node.params.length === 1
|
|
1771
|
+
&& t.isIdentifier(updater.node.params[0]) && t.isCallExpression(updater.node.body)
|
|
1772
|
+
&& t.isMemberExpression(updater.node.body.callee) && !updater.node.body.callee.computed
|
|
1773
|
+
&& t.isIdentifier(updater.node.body.callee.object, { name: updater.node.params[0].name })
|
|
1774
|
+
&& t.isIdentifier(updater.node.body.callee.property, { name: 'toReversed' })
|
|
1775
|
+
&& updater.node.body.arguments.length === 0) {
|
|
1776
|
+
recognized = true;
|
|
1777
|
+
operationStates.add(stateBinding);
|
|
1778
|
+
updater.node.body = t.callExpression(rendererHelper('listReverse'), [t.cloneNode(updater.node.params[0])]);
|
|
1779
|
+
}
|
|
1780
|
+
if (updater?.isArrowFunctionExpression() && updater.node.params.length === 1
|
|
1781
|
+
&& t.isIdentifier(updater.node.params[0]) && t.isCallExpression(updater.node.body)
|
|
1782
|
+
&& t.isMemberExpression(updater.node.body.callee) && !updater.node.body.callee.computed
|
|
1783
|
+
&& t.isIdentifier(updater.node.body.callee.object, { name: updater.node.params[0].name })
|
|
1784
|
+
&& t.isIdentifier(updater.node.body.callee.property, { name: 'filter' })
|
|
1785
|
+
&& updater.node.body.arguments.length === 1 && t.isExpression(updater.node.body.arguments[0])) {
|
|
1786
|
+
recognized = true;
|
|
1787
|
+
operationStates.add(stateBinding);
|
|
1788
|
+
updater.node.body = t.callExpression(rendererHelper('listFilter'), [
|
|
1789
|
+
t.cloneNode(updater.node.params[0]),
|
|
1790
|
+
t.cloneNode(updater.node.body.arguments[0]),
|
|
1791
|
+
]);
|
|
1792
|
+
}
|
|
1793
|
+
if (updater?.isArrowFunctionExpression() && updater.node.params.length === 1
|
|
1794
|
+
&& t.isIdentifier(updater.node.params[0]) && t.isCallExpression(updater.node.body)
|
|
1795
|
+
&& t.isMemberExpression(updater.node.body.callee) && !updater.node.body.callee.computed
|
|
1796
|
+
&& t.isIdentifier(updater.node.body.callee.object, { name: updater.node.params[0].name })
|
|
1797
|
+
&& t.isIdentifier(updater.node.body.callee.property, { name: 'toSorted' })
|
|
1798
|
+
&& updater.node.body.arguments.length <= 1
|
|
1799
|
+
&& updater.node.body.arguments.every(argument => t.isExpression(argument))) {
|
|
1800
|
+
recognized = true;
|
|
1801
|
+
operationStates.add(stateBinding);
|
|
1802
|
+
updater.node.body = t.callExpression(rendererHelper('listSort'), [
|
|
1803
|
+
t.cloneNode(updater.node.params[0]),
|
|
1804
|
+
...updater.node.body.arguments.map(argument => t.cloneNode(argument)),
|
|
1805
|
+
]);
|
|
1806
|
+
}
|
|
1807
|
+
if (!recognized && updater?.isArrayExpression()
|
|
1808
|
+
&& updater.node.elements.some(element => t.isSpreadElement(element))) {
|
|
1809
|
+
throw updater.buildCodeFrameError('runOnce: array replacement from a non-state snapshot is unsupported without identity matching');
|
|
1810
|
+
}
|
|
1811
|
+
if (!recognized && operationStates.has(stateBinding)) {
|
|
1812
|
+
throw updater.buildCodeFrameError('runOnce: opaque list replacement is unsupported without direct operation provenance');
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (!callee.isMemberExpression() || callee.node.computed
|
|
1817
|
+
|| !callee.get('property').isIdentifier({ name: 'map' }) || path.node.arguments.length !== 1)
|
|
1818
|
+
return;
|
|
1819
|
+
const source = callee.get('object');
|
|
1820
|
+
const sourceIdentifier = source.isIdentifier() ? source
|
|
1821
|
+
: source.isCallExpression() && source.node.arguments.length === 0 && source.get('callee').isIdentifier()
|
|
1822
|
+
? source.get('callee') : undefined;
|
|
1823
|
+
if (!sourceIdentifier)
|
|
1824
|
+
return;
|
|
1825
|
+
const sourceBinding = sourceIdentifier.scope.getBinding(sourceIdentifier.node.name);
|
|
1826
|
+
if (!sourceBinding || !reactive.has(sourceBinding) || !operationStates.has(sourceBinding))
|
|
1827
|
+
return;
|
|
1828
|
+
const callback = path.get('arguments.0');
|
|
1829
|
+
if (!callback?.isArrowFunctionExpression() || callback.node.async
|
|
1830
|
+
|| callback.node.params.length < 1 || callback.node.params.length > 2
|
|
1831
|
+
|| !callback.node.params.every(parameter => t.isIdentifier(parameter)))
|
|
1832
|
+
return;
|
|
1833
|
+
const returned = t.isJSXElement(callback.node.body) ? callback.node.body
|
|
1834
|
+
: t.isBlockStatement(callback.node.body)
|
|
1835
|
+
? [...callback.node.body.body].reverse().find(statement => t.isReturnStatement(statement))?.argument
|
|
1836
|
+
: undefined;
|
|
1837
|
+
if (!returned || !t.isJSXElement(returned))
|
|
1838
|
+
return;
|
|
1839
|
+
const key = returned.openingElement.attributes.find(attribute => t.isJSXAttribute(attribute)
|
|
1840
|
+
&& t.isJSXIdentifier(attribute.name, { name: 'key' }));
|
|
1841
|
+
if (!key)
|
|
1842
|
+
return;
|
|
1843
|
+
const mappedByState = mapUpdaters.get(sourceBinding) ?? (() => {
|
|
1844
|
+
for (const edge of propSources) {
|
|
1845
|
+
const child = edge.childScope.getBinding(edge.childName);
|
|
1846
|
+
const parent = edge.parentScope.getBinding(edge.parentName);
|
|
1847
|
+
if (child === sourceBinding && parent && mapUpdaters.has(parent))
|
|
1848
|
+
return mapUpdaters.get(parent);
|
|
1849
|
+
}
|
|
1850
|
+
return undefined;
|
|
1851
|
+
})();
|
|
1852
|
+
const item = callback.node.params[0];
|
|
1853
|
+
const keyValue = t.isJSXAttribute(key) && t.isJSXExpressionContainer(key.value)
|
|
1854
|
+
? key.value.expression : undefined;
|
|
1855
|
+
const property = t.isIdentifier(item) && t.isMemberExpression(keyValue)
|
|
1856
|
+
&& !keyValue.computed && t.isIdentifier(keyValue.object, { name: item.name })
|
|
1857
|
+
&& t.isIdentifier(keyValue.property) ? keyValue.property.name : undefined;
|
|
1858
|
+
const owner = path.getFunctionParent();
|
|
1859
|
+
const propName = owner ? [...(componentProps.get(owner)?.entries() ?? [])]
|
|
1860
|
+
.find(([, binding]) => binding.identifier.name === sourceBinding.identifier.name
|
|
1861
|
+
&& binding.scope === sourceBinding.scope)?.[0] : undefined;
|
|
1862
|
+
const contractedKey = propName ? moduleMetadata?.operationKeyProps?.[propName] : undefined;
|
|
1863
|
+
if (contractedKey && property !== contractedKey) {
|
|
1864
|
+
throw path.buildCodeFrameError(`runOnce: operation prop ${propName} must use direct JSX key ${contractedKey}`);
|
|
1865
|
+
}
|
|
1866
|
+
let stableKeyProperty = contractedKey;
|
|
1867
|
+
if (mappedByState?.length) {
|
|
1868
|
+
if (!property || mappedByState.some(mapper => !mapperPreservesProperty(mapper, property))) {
|
|
1869
|
+
throw path.buildCodeFrameError('runOnce: listMap must statically preserve the direct-property JSX key');
|
|
1870
|
+
}
|
|
1871
|
+
stableKeyProperty = property;
|
|
1872
|
+
}
|
|
1873
|
+
if (t.isIdentifier(callback.node.params[0]) && t.isBlockStatement(callback.node.body)) {
|
|
1874
|
+
const itemBinding = callback.scope.getBinding(callback.node.params[0].name);
|
|
1875
|
+
const callbackBody = callback.get('body');
|
|
1876
|
+
if (!callbackBody.isBlockStatement())
|
|
1877
|
+
throw callback.buildCodeFrameError('runOnce: expected a list callback block');
|
|
1878
|
+
for (const statement of callbackBody.get('body')) {
|
|
1879
|
+
if (!statement.isVariableDeclaration())
|
|
1880
|
+
continue;
|
|
1881
|
+
for (const declaration of statement.get('declarations')) {
|
|
1882
|
+
const init = declaration.get('init');
|
|
1883
|
+
if (!init.isExpression())
|
|
1884
|
+
continue;
|
|
1885
|
+
const properties = new Set();
|
|
1886
|
+
init.traverse({
|
|
1887
|
+
Identifier(reference) {
|
|
1888
|
+
if (!itemBinding || reference.scope.getBinding(reference.node.name) !== itemBinding)
|
|
1889
|
+
return;
|
|
1890
|
+
const member = reference.parentPath;
|
|
1891
|
+
if (member.isMemberExpression() && reference.key === 'object'
|
|
1892
|
+
&& !member.node.computed && t.isIdentifier(member.node.property)) {
|
|
1893
|
+
properties.add(member.node.property.name);
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
properties.add('*');
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1899
|
+
});
|
|
1900
|
+
if (properties.size && (!stableKeyProperty
|
|
1901
|
+
|| [...properties].some(property => property !== stableKeyProperty))) {
|
|
1902
|
+
throw declaration.buildCodeFrameError('runOnce: listMap callback locals derived from mutable item fields require row-owned reactive compilation');
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (callback.node.params.length === 2 && t.isIdentifier(callback.node.params[1])) {
|
|
1908
|
+
const indexBinding = callback.scope.getBinding(callback.node.params[1].name);
|
|
1909
|
+
for (const reference of indexBinding?.referencePaths ?? []) {
|
|
1910
|
+
reference.replaceWith(t.callExpression(t.cloneNode(callback.node.params[1]), []));
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
path.replaceWith(t.callExpression(rendererHelper('operationList'), [
|
|
1914
|
+
t.arrowFunctionExpression([], source.isCallExpression()
|
|
1915
|
+
? t.cloneNode(source.node) : t.callExpression(t.cloneNode(sourceIdentifier.node), [])),
|
|
1916
|
+
t.cloneNode(callback.node),
|
|
1917
|
+
]));
|
|
1918
|
+
path.skip();
|
|
1919
|
+
} },
|
|
1920
|
+
});
|
|
1921
|
+
for (const edge of importedOperationEdges) {
|
|
1922
|
+
const state = edge.scope.getBinding(edge.state);
|
|
1923
|
+
const mappers = state ? mapUpdaters.get(state) : undefined;
|
|
1924
|
+
if (mappers?.some(mapper => !mapperPreservesProperty(mapper, edge.key))) {
|
|
1925
|
+
throw edge.origin.buildCodeFrameError(`runOnce: imported operation prop ${edge.prop} must preserve JSX key ${edge.key}`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
// A rejected construct must not quietly route through the keyed reconciler.
|
|
1929
|
+
program.traverse({
|
|
1930
|
+
JSXOpeningElement(path) {
|
|
1931
|
+
if (!t.isJSXIdentifier(path.node.name, { name: 'input' }))
|
|
1932
|
+
return;
|
|
1933
|
+
const attributes = path.get('attributes');
|
|
1934
|
+
const hasChange = attributes.some(attribute => attribute.isJSXAttribute()
|
|
1935
|
+
&& t.isJSXIdentifier(attribute.node.name, { name: 'onChange' }));
|
|
1936
|
+
if (!hasChange)
|
|
1937
|
+
return;
|
|
1938
|
+
const type = attributes.find(attribute => attribute.isJSXAttribute()
|
|
1939
|
+
&& t.isJSXIdentifier(attribute.node.name, { name: 'type' }));
|
|
1940
|
+
if (!type?.isJSXAttribute() || type.node.value == null || t.isStringLiteral(type.node.value))
|
|
1941
|
+
return;
|
|
1942
|
+
if (t.isJSXExpressionContainer(type.node.value)
|
|
1943
|
+
&& t.isStringLiteral(type.node.value.expression))
|
|
1944
|
+
return;
|
|
1945
|
+
throw type.buildCodeFrameError('runOnce: dynamic input type with onChange is unsupported because the native event mapping can change');
|
|
1946
|
+
},
|
|
1947
|
+
CallExpression(path) {
|
|
1948
|
+
const name = hookName(path);
|
|
1949
|
+
const callee = path.node.callee;
|
|
1950
|
+
const hookSyntax = t.isIdentifier(callee) ? callee.name : t.isMemberExpression(callee) && t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
1951
|
+
const localHookBinding = t.isIdentifier(callee) ? path.scope.getBinding(callee.name) : undefined;
|
|
1952
|
+
const localHook = Boolean(localHookBinding
|
|
1953
|
+
&& [...customHooks.keys()].some(binding => binding.path.node === localHookBinding.path.node));
|
|
1954
|
+
const analyzedHook = Boolean(t.isIdentifier(callee) && isImportedReactiveHook(localHookBinding));
|
|
1955
|
+
if (/^use[A-Z]/.test(hookSyntax) && !name && !localHook && !analyzedHook)
|
|
1956
|
+
throw path.buildCodeFrameError('runOnce: custom and namespace hook calls require additional compiler analysis; use named supported hooks');
|
|
1957
|
+
const supportedHooks = new Set(['useState', 'useRef', 'useEffect', 'useContext', 'useCallback', 'useMemo', 'useReducer']);
|
|
1958
|
+
if (name && /^use[A-Z]/.test(name) && !supportedHooks.has(name))
|
|
1959
|
+
throw path.buildCodeFrameError(`runOnce: ${name} is not supported by this compiler pass yet`);
|
|
1960
|
+
if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property, { name: 'map' })) {
|
|
1961
|
+
const object = path.get('callee.object');
|
|
1962
|
+
const binding = object.isIdentifier() ? object.scope.getBinding(object.node.name) : undefined;
|
|
1963
|
+
// A const array literal that nothing mutates is fixed for as long as the
|
|
1964
|
+
// component exists, so its rows can be emitted once. Module scope shows
|
|
1965
|
+
// that trivially; a declaration inside the component qualifies too,
|
|
1966
|
+
// because the body runs once.
|
|
1967
|
+
//
|
|
1968
|
+
// An array that reads state, a derived value, a prop or context is a
|
|
1969
|
+
// reactive binding by the time this runs, and reactive bindings are read
|
|
1970
|
+
// live -- the call site is `xs().map(...)`. Its callee object is a call
|
|
1971
|
+
// rather than an identifier, so no binding is resolved here and the
|
|
1972
|
+
// array is refused instead of being emitted once and left stale. The
|
|
1973
|
+
// `refuses a fixed array whose contents read ...` cases in
|
|
1974
|
+
// apps/compat-audit/tests/runonce-constructs.test.ts pin that.
|
|
1975
|
+
const onlyMapped = (candidate) => candidate.referencePaths.every(reference => {
|
|
1976
|
+
const member = reference.parentPath;
|
|
1977
|
+
const call = member?.parentPath;
|
|
1978
|
+
return Boolean(member?.isMemberExpression() && reference.key === 'object' && !member.node.computed
|
|
1979
|
+
&& member.get('property').isIdentifier({ name: 'map' })
|
|
1980
|
+
&& call?.isCallExpression() && member.key === 'callee');
|
|
1981
|
+
});
|
|
1982
|
+
const literal = binding?.constant && binding.path.isVariableDeclarator()
|
|
1983
|
+
&& t.isArrayExpression(binding.path.node.init) ? binding.path.get('init') : undefined;
|
|
1984
|
+
const ownedByThisComponent = Boolean(binding
|
|
1985
|
+
&& binding.scope.getFunctionParent() === path.scope.getFunctionParent());
|
|
1986
|
+
const fixedArray = Boolean(literal && onlyMapped(binding)
|
|
1987
|
+
&& (binding.scope.path.isProgram() || ownedByThisComponent));
|
|
1988
|
+
const staticArray = object.isArrayExpression() || fixedArray;
|
|
1989
|
+
if (!staticArray)
|
|
1990
|
+
throw path.buildCodeFrameError('runOnce: lists require direct operation compilation; keyed reconciliation is disabled');
|
|
1991
|
+
path.node.extra = { ...(path.node.extra ?? {}), rrjsStaticMap: true };
|
|
1992
|
+
}
|
|
1993
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
1994
|
+
const binding = path.scope.getBinding(path.node.callee.name);
|
|
1995
|
+
const imported = binding?.path.isImportSpecifier() ? binding.path.node.imported : undefined;
|
|
1996
|
+
if ((!binding && path.node.callee.name === 'list') || (imported && t.isIdentifier(imported, { name: 'list' })))
|
|
1997
|
+
throw path.buildCodeFrameError('runOnce: explicit keyed reconciler calls are disabled');
|
|
1998
|
+
}
|
|
1999
|
+
},
|
|
2000
|
+
});
|
|
2001
|
+
for (const path of deferredRemovals)
|
|
2002
|
+
if (!path.removed)
|
|
2003
|
+
path.remove();
|
|
2004
|
+
}
|