@llui/vite-plugin 0.0.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 +19 -0
- package/dist/collect-deps.d.ts +7 -0
- package/dist/collect-deps.d.ts.map +1 -0
- package/dist/collect-deps.js +267 -0
- package/dist/collect-deps.js.map +1 -0
- package/dist/diagnostics.d.ts +7 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +533 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/msg-schema.d.ts +9 -0
- package/dist/msg-schema.d.ts.map +1 -0
- package/dist/msg-schema.js +83 -0
- package/dist/msg-schema.js.map +1 -0
- package/dist/state-schema.d.ts +28 -0
- package/dist/state-schema.d.ts.map +1 -0
- package/dist/state-schema.js +108 -0
- package/dist/state-schema.js.map +1 -0
- package/dist/transform.d.ts +14 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +1930 -0
- package/dist/transform.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1930 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { collectDeps } from './collect-deps.js';
|
|
3
|
+
import { extractMsgSchema, extractEffectSchema } from './msg-schema.js';
|
|
4
|
+
import { extractStateSchema } from './state-schema.js';
|
|
5
|
+
function createMaskLiteral(f, mask) {
|
|
6
|
+
if (mask >= 0)
|
|
7
|
+
return f.createNumericLiteral(mask);
|
|
8
|
+
// -1 (0xFFFFFFFF | 0) — emit as bitwise OR: 0xFFFFFFFF | 0
|
|
9
|
+
return f.createBinaryExpression(f.createNumericLiteral(0xffffffff), ts.SyntaxKind.BarToken, f.createNumericLiteral(0));
|
|
10
|
+
}
|
|
11
|
+
// HTML element helper names that the compiler can transform
|
|
12
|
+
const ELEMENT_HELPERS = new Set([
|
|
13
|
+
'a',
|
|
14
|
+
'abbr',
|
|
15
|
+
'article',
|
|
16
|
+
'aside',
|
|
17
|
+
'b',
|
|
18
|
+
'blockquote',
|
|
19
|
+
'br',
|
|
20
|
+
'button',
|
|
21
|
+
'canvas',
|
|
22
|
+
'code',
|
|
23
|
+
'dd',
|
|
24
|
+
'details',
|
|
25
|
+
'dialog',
|
|
26
|
+
'div',
|
|
27
|
+
'dl',
|
|
28
|
+
'dt',
|
|
29
|
+
'em',
|
|
30
|
+
'fieldset',
|
|
31
|
+
'figcaption',
|
|
32
|
+
'figure',
|
|
33
|
+
'footer',
|
|
34
|
+
'form',
|
|
35
|
+
'h1',
|
|
36
|
+
'h2',
|
|
37
|
+
'h3',
|
|
38
|
+
'h4',
|
|
39
|
+
'h5',
|
|
40
|
+
'h6',
|
|
41
|
+
'header',
|
|
42
|
+
'hr',
|
|
43
|
+
'i',
|
|
44
|
+
'iframe',
|
|
45
|
+
'img',
|
|
46
|
+
'input',
|
|
47
|
+
'label',
|
|
48
|
+
'legend',
|
|
49
|
+
'li',
|
|
50
|
+
'main',
|
|
51
|
+
'mark',
|
|
52
|
+
'nav',
|
|
53
|
+
'ol',
|
|
54
|
+
'optgroup',
|
|
55
|
+
'option',
|
|
56
|
+
'output',
|
|
57
|
+
'p',
|
|
58
|
+
'pre',
|
|
59
|
+
'progress',
|
|
60
|
+
'section',
|
|
61
|
+
'select',
|
|
62
|
+
'small',
|
|
63
|
+
'span',
|
|
64
|
+
'strong',
|
|
65
|
+
'sub',
|
|
66
|
+
'summary',
|
|
67
|
+
'sup',
|
|
68
|
+
'table',
|
|
69
|
+
'tbody',
|
|
70
|
+
'td',
|
|
71
|
+
'textarea',
|
|
72
|
+
'tfoot',
|
|
73
|
+
'th',
|
|
74
|
+
'thead',
|
|
75
|
+
'time',
|
|
76
|
+
'tr',
|
|
77
|
+
'ul',
|
|
78
|
+
'video',
|
|
79
|
+
]);
|
|
80
|
+
const PROP_KEYS = new Set([
|
|
81
|
+
'value',
|
|
82
|
+
'checked',
|
|
83
|
+
'selected',
|
|
84
|
+
'disabled',
|
|
85
|
+
'readOnly',
|
|
86
|
+
'multiple',
|
|
87
|
+
'indeterminate',
|
|
88
|
+
'defaultValue',
|
|
89
|
+
'defaultChecked',
|
|
90
|
+
'innerHTML',
|
|
91
|
+
'textContent',
|
|
92
|
+
]);
|
|
93
|
+
function classifyKind(key) {
|
|
94
|
+
if (key === 'class' || key === 'className')
|
|
95
|
+
return 'class';
|
|
96
|
+
if (key.startsWith('style.'))
|
|
97
|
+
return 'style';
|
|
98
|
+
if (PROP_KEYS.has(key))
|
|
99
|
+
return 'prop';
|
|
100
|
+
return 'attr';
|
|
101
|
+
}
|
|
102
|
+
function resolveKey(key, kind) {
|
|
103
|
+
if (kind === 'class')
|
|
104
|
+
return 'class';
|
|
105
|
+
if (kind === 'style')
|
|
106
|
+
return key.slice(6);
|
|
107
|
+
if (kind === 'prop')
|
|
108
|
+
return key;
|
|
109
|
+
if (key === 'className')
|
|
110
|
+
return 'class';
|
|
111
|
+
return key;
|
|
112
|
+
}
|
|
113
|
+
export function transformLlui(source, _filename, devMode = false, mcpPort = 5200) {
|
|
114
|
+
const sourceFile = ts.createSourceFile('input.ts', source, ts.ScriptTarget.Latest, true);
|
|
115
|
+
// Find the @llui/dom import
|
|
116
|
+
const imp = findLluiImport(sourceFile);
|
|
117
|
+
if (!imp)
|
|
118
|
+
return null;
|
|
119
|
+
const lluiImport = imp;
|
|
120
|
+
// Collect imported element helper names (local → original)
|
|
121
|
+
const importedHelpers = getImportedHelpers(lluiImport);
|
|
122
|
+
if (importedHelpers.size === 0 && !hasReactiveAccessors(sourceFile))
|
|
123
|
+
return null;
|
|
124
|
+
// Pass 2 pre-scan: collect all state access paths
|
|
125
|
+
// Only use precise masks in files that define a component() — the __dirty
|
|
126
|
+
// function is generated per-component, so bit assignments in other files
|
|
127
|
+
// won't match. Files without component() get FULL_MASK on all bindings.
|
|
128
|
+
const fileHasComponent = hasComponentDef(sourceFile, lluiImport);
|
|
129
|
+
const fieldBits = fileHasComponent ? collectDeps(source) : new Map();
|
|
130
|
+
// Identifier names bound to the View<S,M> helpers parameter of a `view` callback.
|
|
131
|
+
// When the user writes `h.text(...)` / `h.show(...)` / `h.each(...)`, the
|
|
132
|
+
// compiler treats the call as if it were a bare import call.
|
|
133
|
+
const viewHelperNames = collectViewHelperNames(sourceFile, lluiImport);
|
|
134
|
+
// Destructured aliases: `view: (_, { show, text: t }) => [...]` → { show→show, t→text }.
|
|
135
|
+
const viewHelperAliases = collectViewHelperAliases(sourceFile, lluiImport, viewHelperNames);
|
|
136
|
+
// Track which helpers were compiled vs bailed out
|
|
137
|
+
const compiledHelpers = new Set();
|
|
138
|
+
const bailedHelpers = new Set();
|
|
139
|
+
let usesElTemplate = false;
|
|
140
|
+
let usesElSplit = false;
|
|
141
|
+
let usesMemo = false;
|
|
142
|
+
const f = ts.factory;
|
|
143
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
144
|
+
// Collect source positions of transformed nodes for source mapping
|
|
145
|
+
const edits = [];
|
|
146
|
+
function visitor(node) {
|
|
147
|
+
// Synthetic nodes (created by ts.factory) don't have real positions
|
|
148
|
+
const hasPos = node.pos >= 0 && node.end >= 0;
|
|
149
|
+
const origStart = hasPos ? node.getStart(sourceFile) : -1;
|
|
150
|
+
const origEnd = hasPos ? node.getEnd() : -1;
|
|
151
|
+
// Pass 0: each() optimizations — dedup item() selectors + auto-wrap items in memo
|
|
152
|
+
if (ts.isCallExpression(node) &&
|
|
153
|
+
isHelperCall(node.expression, 'each', viewHelperNames, viewHelperAliases)) {
|
|
154
|
+
let current = node;
|
|
155
|
+
let changed = false;
|
|
156
|
+
const memoWrapped = tryWrapEachItemsWithMemo(current, fieldBits, f);
|
|
157
|
+
if (memoWrapped) {
|
|
158
|
+
current = memoWrapped;
|
|
159
|
+
changed = true;
|
|
160
|
+
usesMemo = true;
|
|
161
|
+
}
|
|
162
|
+
const deduped = tryDeduplicateItemSelectors(current, f, printer, sourceFile);
|
|
163
|
+
if (deduped) {
|
|
164
|
+
current = deduped;
|
|
165
|
+
changed = true;
|
|
166
|
+
}
|
|
167
|
+
if (changed) {
|
|
168
|
+
const result = ts.visitEachChild(current, visitor, undefined);
|
|
169
|
+
if (hasPos)
|
|
170
|
+
edits.push({ start: origStart, end: origEnd, replacement: '' });
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Pass 1: Transform element helper calls to elSplit or elTemplate
|
|
175
|
+
if (ts.isCallExpression(node)) {
|
|
176
|
+
const transformed = tryTransformElementCall(node, importedHelpers, fieldBits, compiledHelpers, bailedHelpers, f);
|
|
177
|
+
if (transformed) {
|
|
178
|
+
if (ts.isIdentifier(transformed.expression)) {
|
|
179
|
+
if (transformed.expression.text === 'elTemplate')
|
|
180
|
+
usesElTemplate = true;
|
|
181
|
+
else if (transformed.expression.text === 'elSplit')
|
|
182
|
+
usesElSplit = true;
|
|
183
|
+
}
|
|
184
|
+
if (hasPos)
|
|
185
|
+
edits.push({ start: origStart, end: origEnd, replacement: '' });
|
|
186
|
+
return ts.visitEachChild(transformed, visitor, undefined);
|
|
187
|
+
}
|
|
188
|
+
// Pass 2: Inject mask into text() calls
|
|
189
|
+
const textTransformed = tryInjectTextMask(node, lluiImport, viewHelperNames, viewHelperAliases, fieldBits, f);
|
|
190
|
+
if (textTransformed) {
|
|
191
|
+
if (hasPos)
|
|
192
|
+
edits.push({ start: origStart, end: origEnd, replacement: '' });
|
|
193
|
+
return textTransformed;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Pass 2: Inject __dirty and __msgSchema into component() calls
|
|
197
|
+
if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
|
|
198
|
+
let result = tryInjectDirty(node, fieldBits, f);
|
|
199
|
+
if (devMode) {
|
|
200
|
+
const schema = extractMsgSchema(source);
|
|
201
|
+
if (schema) {
|
|
202
|
+
result = injectMsgSchema(result ?? node, schema, f);
|
|
203
|
+
}
|
|
204
|
+
const stateSchema = extractStateSchema(source);
|
|
205
|
+
if (stateSchema) {
|
|
206
|
+
result = injectStateSchema(result ?? node, stateSchema.fields, f);
|
|
207
|
+
}
|
|
208
|
+
const effectSchema = extractEffectSchema(source);
|
|
209
|
+
if (effectSchema) {
|
|
210
|
+
result = injectEffectSchema(result ?? node, effectSchema, f);
|
|
211
|
+
}
|
|
212
|
+
result = injectComponentMeta(result ?? node, node, sourceFile, _filename, f);
|
|
213
|
+
}
|
|
214
|
+
if (result) {
|
|
215
|
+
if (hasPos)
|
|
216
|
+
edits.push({ start: origStart, end: origEnd, replacement: '' });
|
|
217
|
+
return ts.visitEachChild(result, visitor, undefined);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return ts.visitEachChild(node, visitor, undefined);
|
|
221
|
+
}
|
|
222
|
+
let transformed = ts.visitNode(sourceFile, visitor);
|
|
223
|
+
// Pass 3: Clean up imports — use the old cleanupImports approach
|
|
224
|
+
// which operates on the transformed SourceFile safely
|
|
225
|
+
const safeToRemove = new Set([...compiledHelpers].filter((h) => !bailedHelpers.has(h)));
|
|
226
|
+
transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, f);
|
|
227
|
+
if (edits.length === 0)
|
|
228
|
+
return null;
|
|
229
|
+
// Find component declarations for HMR
|
|
230
|
+
const componentDecls = devMode ? findComponentDeclarations(sourceFile, lluiImport) : [];
|
|
231
|
+
// Build per-statement edits by comparing original vs transformed.
|
|
232
|
+
// Only emit edits for statements that actually changed.
|
|
233
|
+
// Untouched code keeps its original positions → accurate source maps.
|
|
234
|
+
const finalEdits = [];
|
|
235
|
+
const origStmts = sourceFile.statements;
|
|
236
|
+
const xfStmts = transformed.statements;
|
|
237
|
+
for (let i = 0; i < origStmts.length && i < xfStmts.length; i++) {
|
|
238
|
+
const origStart = origStmts[i].getStart(sourceFile);
|
|
239
|
+
const origEnd = origStmts[i].getEnd();
|
|
240
|
+
const origText = source.slice(origStart, origEnd);
|
|
241
|
+
let xfText;
|
|
242
|
+
try {
|
|
243
|
+
xfText = printer.printNode(ts.EmitHint.Unspecified, xfStmts[i], transformed);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Synthetic nodes may fail to print individually — fall back to full reprint
|
|
247
|
+
const { top: _top, bottom: _bottom } = devMode
|
|
248
|
+
? generateDevCode(componentDecls, mcpPort)
|
|
249
|
+
: { top: '', bottom: '' };
|
|
250
|
+
const output = (_top ? _top + '\n' : '') + printer.printFile(transformed) + (_bottom ? '\n' + _bottom : '');
|
|
251
|
+
return { output, edits: [{ start: 0, end: source.length, replacement: output }] };
|
|
252
|
+
}
|
|
253
|
+
// Compare ignoring trailing semicolons and whitespace (printer adds them)
|
|
254
|
+
const origNorm = origText.trim().replace(/;$/, '');
|
|
255
|
+
const xfNorm = xfText.trim().replace(/;$/, '');
|
|
256
|
+
if (origNorm !== xfNorm) {
|
|
257
|
+
// Match the original style: if the original didn't end with a semicolon,
|
|
258
|
+
// strip the one the printer added
|
|
259
|
+
const origHasSemi = origText.trimEnd().endsWith(';');
|
|
260
|
+
const replacement = origHasSemi ? xfText : xfText.replace(/;(\s*)$/, '$1');
|
|
261
|
+
finalEdits.push({ start: origStart, end: origEnd, replacement });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Dev setup: enable* must run BEFORE user's mountApp (top of file),
|
|
265
|
+
// but import.meta.hot.accept needs to reference user's component vars
|
|
266
|
+
// (bottom of file). So split the injection.
|
|
267
|
+
if (devMode) {
|
|
268
|
+
const { top, bottom } = generateDevCode(componentDecls, mcpPort);
|
|
269
|
+
if (top)
|
|
270
|
+
finalEdits.push({ start: 0, end: 0, replacement: top + '\n' });
|
|
271
|
+
if (bottom)
|
|
272
|
+
finalEdits.push({ start: source.length, end: source.length, replacement: '\n' + bottom });
|
|
273
|
+
}
|
|
274
|
+
if (finalEdits.length === 0)
|
|
275
|
+
return null;
|
|
276
|
+
// Build the full output by applying edits (for backward compat)
|
|
277
|
+
const sorted = [...finalEdits].sort((a, b) => b.start - a.start);
|
|
278
|
+
let output = source;
|
|
279
|
+
for (const edit of sorted) {
|
|
280
|
+
output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
|
|
281
|
+
}
|
|
282
|
+
return { output, edits: finalEdits };
|
|
283
|
+
}
|
|
284
|
+
// ── HMR ──────────────────────────────────────────────────────────
|
|
285
|
+
function generateDevCode(components, mcpPort) {
|
|
286
|
+
if (components.length === 0) {
|
|
287
|
+
return {
|
|
288
|
+
top: '',
|
|
289
|
+
bottom: `if (import.meta.hot) {\n import.meta.hot.accept()\n}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const relayImport = mcpPort !== null ? ', startRelay as __startRelay' : '';
|
|
293
|
+
const relayCall = mcpPort !== null ? `\n__startRelay(${mcpPort})` : '';
|
|
294
|
+
const top = `
|
|
295
|
+
import { enableHmr as __enableHmr, replaceComponent as __replaceComponent } from '@llui/dom/hmr'
|
|
296
|
+
import { enableDevTools as __enableDevTools${relayImport} } from '@llui/dom/devtools'
|
|
297
|
+
__enableHmr()
|
|
298
|
+
__enableDevTools()${relayCall}
|
|
299
|
+
`.trim();
|
|
300
|
+
const replaceCalls = components
|
|
301
|
+
.map(({ varName, componentName }) => ` __replaceComponent("${componentName}", ${varName})`)
|
|
302
|
+
.join('\n');
|
|
303
|
+
const bottom = `
|
|
304
|
+
if (import.meta.hot) {
|
|
305
|
+
import.meta.hot.accept(() => {
|
|
306
|
+
${replaceCalls}
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
`.trim();
|
|
310
|
+
return { top, bottom };
|
|
311
|
+
}
|
|
312
|
+
/** Find all component() calls and extract the variable name and component name */
|
|
313
|
+
function findComponentDeclarations(sf, lluiImport) {
|
|
314
|
+
const result = [];
|
|
315
|
+
function visit(node) {
|
|
316
|
+
// Match: const Foo = component({ name: 'Foo', ... })
|
|
317
|
+
if (ts.isVariableDeclaration(node) &&
|
|
318
|
+
ts.isIdentifier(node.name) &&
|
|
319
|
+
node.initializer &&
|
|
320
|
+
ts.isCallExpression(node.initializer) &&
|
|
321
|
+
isComponentCall(node.initializer, lluiImport)) {
|
|
322
|
+
const varName = node.name.text;
|
|
323
|
+
const config = node.initializer.arguments[0];
|
|
324
|
+
if (config && ts.isObjectLiteralExpression(config)) {
|
|
325
|
+
for (const prop of config.properties) {
|
|
326
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
327
|
+
ts.isIdentifier(prop.name) &&
|
|
328
|
+
prop.name.text === 'name' &&
|
|
329
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
330
|
+
result.push({ varName, componentName: prop.initializer.text });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
ts.forEachChild(node, visit);
|
|
336
|
+
}
|
|
337
|
+
visit(sf);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
341
|
+
function findLluiImport(sf) {
|
|
342
|
+
for (const stmt of sf.statements) {
|
|
343
|
+
if (ts.isImportDeclaration(stmt) &&
|
|
344
|
+
ts.isStringLiteral(stmt.moduleSpecifier) &&
|
|
345
|
+
stmt.moduleSpecifier.text === '@llui/dom') {
|
|
346
|
+
return stmt;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
function getImportedHelpers(imp) {
|
|
352
|
+
const map = new Map();
|
|
353
|
+
const clause = imp.importClause;
|
|
354
|
+
if (!clause || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
355
|
+
return map;
|
|
356
|
+
for (const spec of clause.namedBindings.elements) {
|
|
357
|
+
const original = (spec.propertyName ?? spec.name).text;
|
|
358
|
+
const local = spec.name.text;
|
|
359
|
+
if (ELEMENT_HELPERS.has(original)) {
|
|
360
|
+
map.set(local, original);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return map;
|
|
364
|
+
}
|
|
365
|
+
function hasReactiveAccessors(sf) {
|
|
366
|
+
let found = false;
|
|
367
|
+
function visit(node) {
|
|
368
|
+
if (found)
|
|
369
|
+
return;
|
|
370
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
371
|
+
if (node.expression.text === 'text' || node.expression.text === 'component') {
|
|
372
|
+
found = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
ts.forEachChild(node, visit);
|
|
376
|
+
}
|
|
377
|
+
visit(sf);
|
|
378
|
+
return found;
|
|
379
|
+
}
|
|
380
|
+
function hasComponentDef(sf, lluiImport) {
|
|
381
|
+
let found = false;
|
|
382
|
+
function visit(node) {
|
|
383
|
+
if (found)
|
|
384
|
+
return;
|
|
385
|
+
if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
|
|
386
|
+
found = true;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
ts.forEachChild(node, visit);
|
|
390
|
+
}
|
|
391
|
+
visit(sf);
|
|
392
|
+
return found;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Scan for `component({ view: (h) => ... })` arrow functions and collect
|
|
396
|
+
* the identifier name used as the View-bundle parameter. When the user
|
|
397
|
+
* writes `h.show(...)` / `h.text(...)` inside the view, the compiler treats
|
|
398
|
+
* it the same as bare `show(...)` / `text(...)` for mask injection.
|
|
399
|
+
*/
|
|
400
|
+
function collectViewHelperNames(sf, lluiImport) {
|
|
401
|
+
const names = new Set();
|
|
402
|
+
function visit(node) {
|
|
403
|
+
if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
|
|
404
|
+
const arg = node.arguments[0];
|
|
405
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
406
|
+
for (const prop of arg.properties) {
|
|
407
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
408
|
+
ts.isIdentifier(prop.name) &&
|
|
409
|
+
prop.name.text === 'view' &&
|
|
410
|
+
(ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
|
|
411
|
+
const params = prop.initializer.parameters;
|
|
412
|
+
if (params.length >= 1) {
|
|
413
|
+
const first = params[0];
|
|
414
|
+
if (ts.isIdentifier(first.name)) {
|
|
415
|
+
names.add(first.name.text);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Also: any function parameter annotated as `View<...>` — covers extracted
|
|
423
|
+
// view-functions like `function repoPage(h: View<State, Msg>, ...)`.
|
|
424
|
+
if (ts.isParameter(node) &&
|
|
425
|
+
node.type &&
|
|
426
|
+
isViewTypeReference(node.type) &&
|
|
427
|
+
ts.isIdentifier(node.name)) {
|
|
428
|
+
names.add(node.name.text);
|
|
429
|
+
}
|
|
430
|
+
ts.forEachChild(node, visit);
|
|
431
|
+
}
|
|
432
|
+
visit(sf);
|
|
433
|
+
return names;
|
|
434
|
+
}
|
|
435
|
+
function isViewTypeReference(t) {
|
|
436
|
+
return ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && t.typeName.text === 'View';
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Scan for `component({ view: ({ show, each, text, ... }) => ... })`
|
|
440
|
+
* destructured parameters and return a map from the locally-bound name to
|
|
441
|
+
* the primitive name it aliases. This lets users write the bare `show(...)` /
|
|
442
|
+
* `text(...)` forms without importing them, while the compiler still
|
|
443
|
+
* applies mask injection etc.
|
|
444
|
+
*
|
|
445
|
+
* view: ({ show, text: t }) => [...]
|
|
446
|
+
* // returns { show → "show", t → "text" }
|
|
447
|
+
*/
|
|
448
|
+
const VIEW_HELPER_PRIMITIVES = new Set([
|
|
449
|
+
'show',
|
|
450
|
+
'branch',
|
|
451
|
+
'each',
|
|
452
|
+
'text',
|
|
453
|
+
'memo',
|
|
454
|
+
'selector',
|
|
455
|
+
'ctx',
|
|
456
|
+
'slice',
|
|
457
|
+
'send',
|
|
458
|
+
]);
|
|
459
|
+
function collectViewHelperAliases(sf, lluiImport, helperNames) {
|
|
460
|
+
const aliases = new Map();
|
|
461
|
+
function addFromBindingPattern(pattern) {
|
|
462
|
+
for (const elem of pattern.elements) {
|
|
463
|
+
// { show } → propertyName=undefined, name=show
|
|
464
|
+
// { show: mySh } → propertyName=show, name=mySh
|
|
465
|
+
const sourceName = elem.propertyName && ts.isIdentifier(elem.propertyName)
|
|
466
|
+
? elem.propertyName.text
|
|
467
|
+
: ts.isIdentifier(elem.name)
|
|
468
|
+
? elem.name.text
|
|
469
|
+
: null;
|
|
470
|
+
const localName = ts.isIdentifier(elem.name) ? elem.name.text : null;
|
|
471
|
+
if (sourceName && localName && VIEW_HELPER_PRIMITIVES.has(sourceName)) {
|
|
472
|
+
aliases.set(localName, sourceName);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function visit(node) {
|
|
477
|
+
if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
|
|
478
|
+
const arg = node.arguments[0];
|
|
479
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
480
|
+
for (const prop of arg.properties) {
|
|
481
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
482
|
+
ts.isIdentifier(prop.name) &&
|
|
483
|
+
prop.name.text === 'view' &&
|
|
484
|
+
(ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
|
|
485
|
+
const params = prop.initializer.parameters;
|
|
486
|
+
if (params.length >= 1) {
|
|
487
|
+
const first = params[0];
|
|
488
|
+
if (ts.isObjectBindingPattern(first.name)) {
|
|
489
|
+
addFromBindingPattern(first.name);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Also: function parameters like `(…, { show, text }: View<State, Msg>) => …`
|
|
497
|
+
// on extracted helpers — allow the same destructuring ergonomics.
|
|
498
|
+
if (ts.isParameter(node) &&
|
|
499
|
+
node.type &&
|
|
500
|
+
isViewTypeReference(node.type) &&
|
|
501
|
+
ts.isObjectBindingPattern(node.name)) {
|
|
502
|
+
addFromBindingPattern(node.name);
|
|
503
|
+
}
|
|
504
|
+
// Also: `const { show, text } = h` assignments where `h` is a known
|
|
505
|
+
// helper binding — lets helpers destructure once at the top of the
|
|
506
|
+
// function body.
|
|
507
|
+
if (ts.isVariableDeclaration(node) &&
|
|
508
|
+
ts.isObjectBindingPattern(node.name) &&
|
|
509
|
+
node.initializer &&
|
|
510
|
+
ts.isIdentifier(node.initializer) &&
|
|
511
|
+
helperNames.has(node.initializer.text)) {
|
|
512
|
+
addFromBindingPattern(node.name);
|
|
513
|
+
}
|
|
514
|
+
ts.forEachChild(node, visit);
|
|
515
|
+
}
|
|
516
|
+
visit(sf);
|
|
517
|
+
return aliases;
|
|
518
|
+
}
|
|
519
|
+
function isComponentCall(node, lluiImport) {
|
|
520
|
+
if (!ts.isIdentifier(node.expression))
|
|
521
|
+
return false;
|
|
522
|
+
const name = node.expression.text;
|
|
523
|
+
if (name !== 'component')
|
|
524
|
+
return false;
|
|
525
|
+
// Verify it's from the llui import
|
|
526
|
+
const clause = lluiImport.importClause;
|
|
527
|
+
if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
528
|
+
return false;
|
|
529
|
+
return clause.namedBindings.elements.some((s) => s.name.text === 'component' || (s.propertyName && s.propertyName.text === 'component'));
|
|
530
|
+
}
|
|
531
|
+
function emitStaticProp(staticProps, f, kind, resolvedKey, value) {
|
|
532
|
+
switch (kind) {
|
|
533
|
+
case 'class':
|
|
534
|
+
staticProps.push(f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('__e'), 'className'), ts.SyntaxKind.EqualsToken, value)));
|
|
535
|
+
break;
|
|
536
|
+
case 'prop':
|
|
537
|
+
staticProps.push(f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('__e'), resolvedKey), ts.SyntaxKind.EqualsToken, value)));
|
|
538
|
+
break;
|
|
539
|
+
case 'style':
|
|
540
|
+
staticProps.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier('__e'), 'style'), 'setProperty'), undefined, [f.createStringLiteral(resolvedKey), value])));
|
|
541
|
+
break;
|
|
542
|
+
default: // attr
|
|
543
|
+
staticProps.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('__e'), 'setAttribute'), undefined, [f.createStringLiteral(resolvedKey), value])));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// ── Pass 1: Element → elSplit ────────────────────────────────────
|
|
547
|
+
function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f) {
|
|
548
|
+
if (!ts.isIdentifier(node.expression))
|
|
549
|
+
return null;
|
|
550
|
+
const localName = node.expression.text;
|
|
551
|
+
const originalName = helpers.get(localName);
|
|
552
|
+
if (!originalName)
|
|
553
|
+
return null;
|
|
554
|
+
// Handle children-only overload: `div([...])` — first arg is the children array.
|
|
555
|
+
// Normalize to props=undefined, children=firstArg so downstream logic works.
|
|
556
|
+
const firstArg = node.arguments[0];
|
|
557
|
+
const usesChildrenOnlyOverload = firstArg && ts.isArrayLiteralExpression(firstArg);
|
|
558
|
+
const propsArg = usesChildrenOnlyOverload ? undefined : firstArg;
|
|
559
|
+
if (propsArg && !ts.isObjectLiteralExpression(propsArg)) {
|
|
560
|
+
bailed.add(localName);
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
// Bail on spread assignments (`...parts.root`) — the compiler cannot
|
|
564
|
+
// statically classify spread contents, and silently dropping them would
|
|
565
|
+
// break consumers (e.g. @llui/components parts spreading). Fall back to
|
|
566
|
+
// the runtime element helper so spreads are applied normally.
|
|
567
|
+
if (propsArg &&
|
|
568
|
+
ts.isObjectLiteralExpression(propsArg) &&
|
|
569
|
+
propsArg.properties.some((p) => ts.isSpreadAssignment(p))) {
|
|
570
|
+
bailed.add(localName);
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const tag = f.createStringLiteral(originalName);
|
|
574
|
+
// Classify props
|
|
575
|
+
const staticProps = [];
|
|
576
|
+
const events = [];
|
|
577
|
+
const bindings = [];
|
|
578
|
+
if (propsArg && ts.isObjectLiteralExpression(propsArg)) {
|
|
579
|
+
for (const prop of propsArg.properties) {
|
|
580
|
+
// Handle both PropertyAssignment (key: value) and ShorthandPropertyAssignment ({ id })
|
|
581
|
+
let key;
|
|
582
|
+
let value;
|
|
583
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
584
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name))
|
|
585
|
+
continue;
|
|
586
|
+
key = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.text;
|
|
587
|
+
value = prop.initializer;
|
|
588
|
+
}
|
|
589
|
+
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
590
|
+
key = prop.name.text;
|
|
591
|
+
value = prop.name; // The identifier itself is the value
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (key === 'key')
|
|
597
|
+
continue;
|
|
598
|
+
// Event handler
|
|
599
|
+
if (/^on[A-Z]/.test(key)) {
|
|
600
|
+
const eventName = key.slice(2).toLowerCase();
|
|
601
|
+
events.push(f.createArrayLiteralExpression([f.createStringLiteral(eventName), value]));
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
// Reactive binding — value is an arrow function or function expression
|
|
605
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
606
|
+
const kind = classifyKind(key);
|
|
607
|
+
const resolvedKey = resolveKey(key, kind);
|
|
608
|
+
const { mask, readsState } = computeAccessorMask(value, fieldBits);
|
|
609
|
+
// Zero-mask constant folding: accessor doesn't read state → treat as static
|
|
610
|
+
if (mask === 0 && !readsState) {
|
|
611
|
+
emitStaticProp(staticProps, f, kind, resolvedKey, f.createCallExpression(value, undefined, []));
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
bindings.push(f.createArrayLiteralExpression([
|
|
615
|
+
createMaskLiteral(f, mask),
|
|
616
|
+
f.createStringLiteral(kind),
|
|
617
|
+
f.createStringLiteral(resolvedKey),
|
|
618
|
+
value,
|
|
619
|
+
]));
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
// Call expression — check if it's a per-item accessor: item(t => t.field)
|
|
623
|
+
if (ts.isCallExpression(value)) {
|
|
624
|
+
if (isPerItemCall(value)) {
|
|
625
|
+
// Emit as a binding with FULL_MASK — the accessor is the item() call itself
|
|
626
|
+
const kind = classifyKind(key);
|
|
627
|
+
const resolvedKey = resolveKey(key, kind);
|
|
628
|
+
bindings.push(f.createArrayLiteralExpression([
|
|
629
|
+
createMaskLiteral(f, 0xffffffff | 0),
|
|
630
|
+
f.createStringLiteral(kind),
|
|
631
|
+
f.createStringLiteral(resolvedKey),
|
|
632
|
+
value,
|
|
633
|
+
]));
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
// Unknown call expression — bail out
|
|
637
|
+
bailed.add(localName);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
// Per-item property access: item.field — equivalent to item(t => t.field)
|
|
641
|
+
// Also matches hoisted __a0/__a1/… identifiers produced by dedup pass.
|
|
642
|
+
if (isPerItemFieldAccess(value) || isHoistedPerItem(value)) {
|
|
643
|
+
const kind = classifyKind(key);
|
|
644
|
+
const resolvedKey = resolveKey(key, kind);
|
|
645
|
+
bindings.push(f.createArrayLiteralExpression([
|
|
646
|
+
createMaskLiteral(f, 0xffffffff | 0),
|
|
647
|
+
f.createStringLiteral(kind),
|
|
648
|
+
f.createStringLiteral(resolvedKey),
|
|
649
|
+
value,
|
|
650
|
+
]));
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
// Static prop
|
|
654
|
+
const kind = classifyKind(key);
|
|
655
|
+
const resolvedKey = resolveKey(key, kind);
|
|
656
|
+
emitStaticProp(staticProps, f, kind, resolvedKey, value);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Build elSplit args
|
|
660
|
+
const staticFn = staticProps.length > 0
|
|
661
|
+
? f.createArrowFunction(undefined, undefined, [f.createParameterDeclaration(undefined, undefined, '__e')], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(staticProps, true))
|
|
662
|
+
: f.createNull();
|
|
663
|
+
const eventsArr = events.length > 0 ? f.createArrayLiteralExpression(events) : f.createNull();
|
|
664
|
+
const bindingsArr = bindings.length > 0 ? f.createArrayLiteralExpression(bindings) : f.createNull();
|
|
665
|
+
const children = usesChildrenOnlyOverload
|
|
666
|
+
? node.arguments[0]
|
|
667
|
+
: (node.arguments[1] ?? f.createNull());
|
|
668
|
+
compiled.add(localName);
|
|
669
|
+
// Subtree collapse: if children contain nested element helpers,
|
|
670
|
+
// collapse the entire tree into a single elTemplate() call
|
|
671
|
+
const analyzed = analyzeSubtree(node, helpers, fieldBits, []);
|
|
672
|
+
if (analyzed && hasNestedElements(analyzed)) {
|
|
673
|
+
// Mark all descendant helpers as compiled for import cleanup
|
|
674
|
+
collectUsedHelpers(analyzed, compiled);
|
|
675
|
+
const templateCall = emitSubtreeTemplate(analyzed, fieldBits, f);
|
|
676
|
+
return templateCall;
|
|
677
|
+
}
|
|
678
|
+
// Static subtree prerendering: if no events, no bindings, and children
|
|
679
|
+
// are all static text, emit a <template> clone
|
|
680
|
+
if (events.length === 0 && bindings.length === 0 && isStaticChildren(children)) {
|
|
681
|
+
const html = buildStaticHTML(originalName, staticProps, children, f);
|
|
682
|
+
if (html) {
|
|
683
|
+
return emitTemplateClone(html, f);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const call = f.createCallExpression(f.createIdentifier('elSplit'), undefined, [
|
|
687
|
+
tag,
|
|
688
|
+
staticFn,
|
|
689
|
+
eventsArr,
|
|
690
|
+
bindingsArr,
|
|
691
|
+
children,
|
|
692
|
+
]);
|
|
693
|
+
ts.addSyntheticLeadingComment(call, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false);
|
|
694
|
+
return call;
|
|
695
|
+
}
|
|
696
|
+
// ── Pass 2: Mask injection ───────────────────────────────────────
|
|
697
|
+
/**
|
|
698
|
+
* Match a call expression against a primitive name across all three binding
|
|
699
|
+
* forms:
|
|
700
|
+
* - bare imported identifier: `name(...)` where `name` was imported from @llui/dom
|
|
701
|
+
* - destructured alias: `name(...)` where `name` is bound via
|
|
702
|
+
* `view: (_, { name }) => ...` (or `{ name: alias }`)
|
|
703
|
+
* - member call: `<h>.name(...)` where `<h>` is the 2nd view parameter
|
|
704
|
+
*
|
|
705
|
+
* The compiler treats all three identically for mask injection / each()
|
|
706
|
+
* optimization purposes.
|
|
707
|
+
*/
|
|
708
|
+
function isHelperCall(expr, name, helperNames, aliases) {
|
|
709
|
+
if (ts.isIdentifier(expr)) {
|
|
710
|
+
if (expr.text === name)
|
|
711
|
+
return true;
|
|
712
|
+
if (aliases && aliases.get(expr.text) === name)
|
|
713
|
+
return true;
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
if (ts.isPropertyAccessExpression(expr) &&
|
|
717
|
+
ts.isIdentifier(expr.expression) &&
|
|
718
|
+
helperNames.has(expr.expression.text) &&
|
|
719
|
+
ts.isIdentifier(expr.name) &&
|
|
720
|
+
expr.name.text === name) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
function tryInjectTextMask(node, lluiImport, viewHelperNames, viewHelperAliases, fieldBits, f) {
|
|
726
|
+
if (!isHelperCall(node.expression, 'text', viewHelperNames, viewHelperAliases)) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
// For a bare identifier `text`, verify it actually resolves to the @llui/dom
|
|
730
|
+
// import (otherwise a user-defined `text` in scope would be rewritten).
|
|
731
|
+
// Destructured-alias and member-expression forms are already provenance-safe.
|
|
732
|
+
if (ts.isIdentifier(node.expression) && !viewHelperAliases.has(node.expression.text)) {
|
|
733
|
+
const clause = lluiImport.importClause;
|
|
734
|
+
if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
735
|
+
return null;
|
|
736
|
+
const hasText = clause.namedBindings.elements.some((s) => s.name.text === 'text' || s.propertyName?.text === 'text');
|
|
737
|
+
if (!hasText)
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
const firstArg = node.arguments[0];
|
|
741
|
+
if (!firstArg)
|
|
742
|
+
return null;
|
|
743
|
+
// Only inject mask for accessor functions, not static strings
|
|
744
|
+
if (!ts.isArrowFunction(firstArg) && !ts.isFunctionExpression(firstArg))
|
|
745
|
+
return null;
|
|
746
|
+
// Don't inject if mask already provided
|
|
747
|
+
if (node.arguments.length >= 2)
|
|
748
|
+
return null;
|
|
749
|
+
const { mask } = computeAccessorMask(firstArg, fieldBits);
|
|
750
|
+
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
751
|
+
firstArg,
|
|
752
|
+
createMaskLiteral(f, mask === 0 ? 0xffffffff | 0 : mask),
|
|
753
|
+
]);
|
|
754
|
+
}
|
|
755
|
+
function tryInjectDirty(node, fieldBits, f) {
|
|
756
|
+
if (fieldBits.size === 0)
|
|
757
|
+
return null;
|
|
758
|
+
const configArg = node.arguments[0];
|
|
759
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg))
|
|
760
|
+
return null;
|
|
761
|
+
// Check if __dirty already exists
|
|
762
|
+
for (const prop of configArg.properties) {
|
|
763
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
764
|
+
ts.isIdentifier(prop.name) &&
|
|
765
|
+
prop.name.text === '__dirty') {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Build __dirty: (o, n) => (Object.is(o.field, n.field) ? 0 : bit) | ...
|
|
770
|
+
// Compare at top-level field (depth 1) — nested path changes within a
|
|
771
|
+
// field must trigger the bit even if the specific sub-path isn't tracked.
|
|
772
|
+
// e.g., route.page tracked but route.data changes → must fire.
|
|
773
|
+
const topLevelBits = new Map();
|
|
774
|
+
for (const [path, bit] of fieldBits) {
|
|
775
|
+
const topField = path.split('.')[0];
|
|
776
|
+
topLevelBits.set(topField, (topLevelBits.get(topField) ?? 0) | bit);
|
|
777
|
+
}
|
|
778
|
+
const comparisons = [];
|
|
779
|
+
for (const [field, bit] of topLevelBits) {
|
|
780
|
+
const oAccess = buildAccess(f, 'o', [field]);
|
|
781
|
+
const nAccess = buildAccess(f, 'n', [field]);
|
|
782
|
+
comparisons.push(f.createParenthesizedExpression(f.createConditionalExpression(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('Object'), 'is'), undefined, [oAccess, nAccess]), f.createToken(ts.SyntaxKind.QuestionToken), f.createNumericLiteral(0), f.createToken(ts.SyntaxKind.ColonToken), createMaskLiteral(f, bit))));
|
|
783
|
+
}
|
|
784
|
+
let dirtyBody = comparisons[0];
|
|
785
|
+
for (let i = 1; i < comparisons.length; i++) {
|
|
786
|
+
dirtyBody = f.createBinaryExpression(dirtyBody, ts.SyntaxKind.BarToken, comparisons[i]);
|
|
787
|
+
}
|
|
788
|
+
// Fallback: if no tracked bit fired but the state reference changed, some
|
|
789
|
+
// untracked field must have changed — return FULL_MASK so bindings whose
|
|
790
|
+
// accessors came from external modules (spread parts) still fire.
|
|
791
|
+
// tracked || (Object.is(o, n) ? 0 : FULL_MASK)
|
|
792
|
+
const fallback = f.createParenthesizedExpression(f.createConditionalExpression(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('Object'), 'is'), undefined, [f.createIdentifier('o'), f.createIdentifier('n')]), f.createToken(ts.SyntaxKind.QuestionToken), f.createNumericLiteral(0), f.createToken(ts.SyntaxKind.ColonToken), createMaskLiteral(f, -1)));
|
|
793
|
+
dirtyBody = f.createBinaryExpression(f.createParenthesizedExpression(dirtyBody), ts.SyntaxKind.BarBarToken, fallback);
|
|
794
|
+
const dirtyFn = f.createArrowFunction(undefined, undefined, [
|
|
795
|
+
f.createParameterDeclaration(undefined, undefined, 'o'),
|
|
796
|
+
f.createParameterDeclaration(undefined, undefined, 'n'),
|
|
797
|
+
], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), dirtyBody);
|
|
798
|
+
const dirtyProp = f.createPropertyAssignment('__dirty', dirtyFn);
|
|
799
|
+
// __maskLegend: maps each top-level state field to the bit(s) that fire when
|
|
800
|
+
// it changes. Lets introspection tools decode runtime dirty masks to field names.
|
|
801
|
+
const legendProps = [];
|
|
802
|
+
for (const [field, bit] of topLevelBits) {
|
|
803
|
+
legendProps.push(f.createPropertyAssignment(field, createMaskLiteral(f, bit)));
|
|
804
|
+
}
|
|
805
|
+
const legendProp = f.createPropertyAssignment('__maskLegend', f.createObjectLiteralExpression(legendProps, false));
|
|
806
|
+
const newConfig = f.createObjectLiteralExpression([...configArg.properties, dirtyProp, legendProp], true);
|
|
807
|
+
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
808
|
+
newConfig,
|
|
809
|
+
...node.arguments.slice(1),
|
|
810
|
+
]);
|
|
811
|
+
}
|
|
812
|
+
function buildAccess(f, root, parts) {
|
|
813
|
+
let expr = f.createIdentifier(root);
|
|
814
|
+
for (const part of parts) {
|
|
815
|
+
// Use optional chaining for nested paths
|
|
816
|
+
if (parts.length > 1) {
|
|
817
|
+
expr = f.createPropertyAccessChain(expr, f.createToken(ts.SyntaxKind.QuestionDotToken), part);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
expr = f.createPropertyAccessExpression(expr, part);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return expr;
|
|
824
|
+
}
|
|
825
|
+
// ── Pass 3: Import cleanup ───────────────────────────────────────
|
|
826
|
+
function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, f) {
|
|
827
|
+
if (compiled.size === 0 && !usesElTemplate && !usesElSplit && !usesMemo)
|
|
828
|
+
return sf;
|
|
829
|
+
const clause = lluiImport.importClause;
|
|
830
|
+
if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
831
|
+
return sf;
|
|
832
|
+
const remaining = clause.namedBindings.elements.filter((spec) => !compiled.has(spec.name.text));
|
|
833
|
+
const hasElSplit = clause.namedBindings.elements.some((s) => s.name.text === 'elSplit');
|
|
834
|
+
if (!hasElSplit && usesElSplit) {
|
|
835
|
+
remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elSplit')));
|
|
836
|
+
}
|
|
837
|
+
const hasElTemplate = clause.namedBindings.elements.some((s) => s.name.text === 'elTemplate');
|
|
838
|
+
if (!hasElTemplate && usesElTemplate) {
|
|
839
|
+
remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elTemplate')));
|
|
840
|
+
}
|
|
841
|
+
const hasMemo = clause.namedBindings.elements.some((s) => s.name.text === 'memo');
|
|
842
|
+
if (!hasMemo && usesMemo) {
|
|
843
|
+
remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('memo')));
|
|
844
|
+
}
|
|
845
|
+
const newBindings = f.createNamedImports(remaining);
|
|
846
|
+
const newClause = f.createImportClause(false, undefined, newBindings);
|
|
847
|
+
const newImportDecl = f.createImportDeclaration(undefined, newClause, lluiImport.moduleSpecifier);
|
|
848
|
+
let replaced = false;
|
|
849
|
+
const statements = sf.statements.map((stmt) => {
|
|
850
|
+
if (!replaced &&
|
|
851
|
+
ts.isImportDeclaration(stmt) &&
|
|
852
|
+
ts.isStringLiteral(stmt.moduleSpecifier) &&
|
|
853
|
+
stmt.moduleSpecifier.text === '@llui/dom' &&
|
|
854
|
+
!stmt.importClause?.isTypeOnly) {
|
|
855
|
+
replaced = true;
|
|
856
|
+
return newImportDecl;
|
|
857
|
+
}
|
|
858
|
+
return stmt;
|
|
859
|
+
});
|
|
860
|
+
return f.updateSourceFile(sf, statements);
|
|
861
|
+
}
|
|
862
|
+
// ── __msgSchema injection ────────────────────────────────────────
|
|
863
|
+
function injectStateSchema(node, fields, f) {
|
|
864
|
+
const configArg = node.arguments[0];
|
|
865
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg))
|
|
866
|
+
return node;
|
|
867
|
+
for (const prop of configArg.properties) {
|
|
868
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
869
|
+
ts.isIdentifier(prop.name) &&
|
|
870
|
+
prop.name.text === '__stateSchema') {
|
|
871
|
+
return node;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const schemaProp = f.createPropertyAssignment('__stateSchema', stateTypeToLiteral({ kind: 'object', fields }, f));
|
|
875
|
+
const newConfig = f.createObjectLiteralExpression([...configArg.properties, schemaProp], true);
|
|
876
|
+
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
877
|
+
newConfig,
|
|
878
|
+
...node.arguments.slice(1),
|
|
879
|
+
]);
|
|
880
|
+
}
|
|
881
|
+
function stateTypeToLiteral(t, f) {
|
|
882
|
+
if (typeof t === 'string')
|
|
883
|
+
return f.createStringLiteral(t);
|
|
884
|
+
if (t.kind === 'enum') {
|
|
885
|
+
return f.createObjectLiteralExpression([
|
|
886
|
+
f.createPropertyAssignment('kind', f.createStringLiteral('enum')),
|
|
887
|
+
f.createPropertyAssignment('values', f.createArrayLiteralExpression(t.values.map((v) => f.createStringLiteral(v)))),
|
|
888
|
+
]);
|
|
889
|
+
}
|
|
890
|
+
if (t.kind === 'array') {
|
|
891
|
+
return f.createObjectLiteralExpression([
|
|
892
|
+
f.createPropertyAssignment('kind', f.createStringLiteral('array')),
|
|
893
|
+
f.createPropertyAssignment('of', stateTypeToLiteral(t.of, f)),
|
|
894
|
+
]);
|
|
895
|
+
}
|
|
896
|
+
if (t.kind === 'optional') {
|
|
897
|
+
return f.createObjectLiteralExpression([
|
|
898
|
+
f.createPropertyAssignment('kind', f.createStringLiteral('optional')),
|
|
899
|
+
f.createPropertyAssignment('of', stateTypeToLiteral(t.of, f)),
|
|
900
|
+
]);
|
|
901
|
+
}
|
|
902
|
+
if (t.kind === 'union') {
|
|
903
|
+
return f.createObjectLiteralExpression([
|
|
904
|
+
f.createPropertyAssignment('kind', f.createStringLiteral('union')),
|
|
905
|
+
f.createPropertyAssignment('of', f.createArrayLiteralExpression(t.of.map((m) => stateTypeToLiteral(m, f)))),
|
|
906
|
+
]);
|
|
907
|
+
}
|
|
908
|
+
// object
|
|
909
|
+
const fieldProps = [];
|
|
910
|
+
for (const [k, v] of Object.entries(t.fields)) {
|
|
911
|
+
fieldProps.push(f.createPropertyAssignment(k, stateTypeToLiteral(v, f)));
|
|
912
|
+
}
|
|
913
|
+
return f.createObjectLiteralExpression([
|
|
914
|
+
f.createPropertyAssignment('kind', f.createStringLiteral('object')),
|
|
915
|
+
f.createPropertyAssignment('fields', f.createObjectLiteralExpression(fieldProps, true)),
|
|
916
|
+
]);
|
|
917
|
+
}
|
|
918
|
+
function injectComponentMeta(nodeWithMaybeEdits, originalNode, sourceFile, filename, f) {
|
|
919
|
+
const configArg = nodeWithMaybeEdits.arguments[0];
|
|
920
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg))
|
|
921
|
+
return nodeWithMaybeEdits;
|
|
922
|
+
// Don't inject if already present
|
|
923
|
+
for (const prop of configArg.properties) {
|
|
924
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
925
|
+
ts.isIdentifier(prop.name) &&
|
|
926
|
+
prop.name.text === '__componentMeta') {
|
|
927
|
+
return nodeWithMaybeEdits;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Line number from the original (real-position) node
|
|
931
|
+
const pos = originalNode.pos >= 0 ? originalNode.getStart(sourceFile) : 0;
|
|
932
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
933
|
+
const meta = f.createObjectLiteralExpression([
|
|
934
|
+
f.createPropertyAssignment('file', f.createStringLiteral(filename)),
|
|
935
|
+
f.createPropertyAssignment('line', f.createNumericLiteral(line + 1)),
|
|
936
|
+
], false);
|
|
937
|
+
const metaProp = f.createPropertyAssignment('__componentMeta', meta);
|
|
938
|
+
const newConfig = f.createObjectLiteralExpression([...configArg.properties, metaProp], true);
|
|
939
|
+
return f.createCallExpression(nodeWithMaybeEdits.expression, nodeWithMaybeEdits.typeArguments, [
|
|
940
|
+
newConfig,
|
|
941
|
+
...nodeWithMaybeEdits.arguments.slice(1),
|
|
942
|
+
]);
|
|
943
|
+
}
|
|
944
|
+
function injectMsgSchema(node, schema, f) {
|
|
945
|
+
const configArg = node.arguments[0];
|
|
946
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg))
|
|
947
|
+
return node;
|
|
948
|
+
// Don't inject if already present
|
|
949
|
+
for (const prop of configArg.properties) {
|
|
950
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
951
|
+
ts.isIdentifier(prop.name) &&
|
|
952
|
+
prop.name.text === '__msgSchema') {
|
|
953
|
+
return node;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// Build the schema object literal
|
|
957
|
+
const variantProps = [];
|
|
958
|
+
for (const [variant, fields] of Object.entries(schema.variants)) {
|
|
959
|
+
const fieldProps = [];
|
|
960
|
+
for (const [field, type] of Object.entries(fields)) {
|
|
961
|
+
if (typeof type === 'string') {
|
|
962
|
+
fieldProps.push(f.createPropertyAssignment(field, f.createStringLiteral(type)));
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
fieldProps.push(f.createPropertyAssignment(field, f.createObjectLiteralExpression([
|
|
966
|
+
f.createPropertyAssignment('enum', f.createArrayLiteralExpression(type.enum.map((v) => f.createStringLiteral(v)))),
|
|
967
|
+
])));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
variantProps.push(f.createPropertyAssignment(f.createStringLiteral(variant), f.createObjectLiteralExpression(fieldProps)));
|
|
971
|
+
}
|
|
972
|
+
const schemaObj = f.createObjectLiteralExpression([
|
|
973
|
+
f.createPropertyAssignment('discriminant', f.createStringLiteral(schema.discriminant)),
|
|
974
|
+
f.createPropertyAssignment('variants', f.createObjectLiteralExpression(variantProps, true)),
|
|
975
|
+
], true);
|
|
976
|
+
const schemaProp = f.createPropertyAssignment('__msgSchema', schemaObj);
|
|
977
|
+
const newConfig = f.createObjectLiteralExpression([...configArg.properties, schemaProp], true);
|
|
978
|
+
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
979
|
+
newConfig,
|
|
980
|
+
...node.arguments.slice(1),
|
|
981
|
+
]);
|
|
982
|
+
}
|
|
983
|
+
function injectEffectSchema(node, schema, f) {
|
|
984
|
+
const configArg = node.arguments[0];
|
|
985
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg))
|
|
986
|
+
return node;
|
|
987
|
+
for (const prop of configArg.properties) {
|
|
988
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
989
|
+
ts.isIdentifier(prop.name) &&
|
|
990
|
+
prop.name.text === '__effectSchema') {
|
|
991
|
+
return node;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const variantProps = [];
|
|
995
|
+
for (const [variant, fields] of Object.entries(schema.variants)) {
|
|
996
|
+
const fieldProps = [];
|
|
997
|
+
for (const [field, type] of Object.entries(fields)) {
|
|
998
|
+
if (typeof type === 'string') {
|
|
999
|
+
fieldProps.push(f.createPropertyAssignment(field, f.createStringLiteral(type)));
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
fieldProps.push(f.createPropertyAssignment(field, f.createObjectLiteralExpression([
|
|
1003
|
+
f.createPropertyAssignment('enum', f.createArrayLiteralExpression(type.enum.map((v) => f.createStringLiteral(v)))),
|
|
1004
|
+
])));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
variantProps.push(f.createPropertyAssignment(f.createStringLiteral(variant), f.createObjectLiteralExpression(fieldProps)));
|
|
1008
|
+
}
|
|
1009
|
+
const schemaObj = f.createObjectLiteralExpression([
|
|
1010
|
+
f.createPropertyAssignment('discriminant', f.createStringLiteral(schema.discriminant)),
|
|
1011
|
+
f.createPropertyAssignment('variants', f.createObjectLiteralExpression(variantProps, true)),
|
|
1012
|
+
], true);
|
|
1013
|
+
const schemaProp = f.createPropertyAssignment('__effectSchema', schemaObj);
|
|
1014
|
+
const newConfig = f.createObjectLiteralExpression([...configArg.properties, schemaProp], true);
|
|
1015
|
+
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
1016
|
+
newConfig,
|
|
1017
|
+
...node.arguments.slice(1),
|
|
1018
|
+
]);
|
|
1019
|
+
}
|
|
1020
|
+
// ── Per-item accessor detection ──────────────────────────────────
|
|
1021
|
+
// ── Item selector deduplication ──────────────────────────────────
|
|
1022
|
+
/**
|
|
1023
|
+
* In each() render callbacks, deduplicate repeated item(selector) calls.
|
|
1024
|
+
*
|
|
1025
|
+
* Before: item((r) => r.id) appears 4 times → 4 selector closures + 4 accessor closures
|
|
1026
|
+
* After: const __s0 = (r) => r.id; const __a0 = item(__s0); → 1 selector + 1 accessor
|
|
1027
|
+
*/
|
|
1028
|
+
function tryDeduplicateItemSelectors(eachCall, f, printer, sourceFile) {
|
|
1029
|
+
// each() takes a single object literal argument
|
|
1030
|
+
const arg = eachCall.arguments[0];
|
|
1031
|
+
if (!arg || !ts.isObjectLiteralExpression(arg))
|
|
1032
|
+
return null;
|
|
1033
|
+
// Find the render property
|
|
1034
|
+
let renderProp = null;
|
|
1035
|
+
for (const prop of arg.properties) {
|
|
1036
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
1037
|
+
ts.isIdentifier(prop.name) &&
|
|
1038
|
+
prop.name.text === 'render') {
|
|
1039
|
+
renderProp = prop;
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (!renderProp)
|
|
1044
|
+
return null;
|
|
1045
|
+
const renderFn = renderProp.initializer;
|
|
1046
|
+
if (!ts.isArrowFunction(renderFn) && !ts.isFunctionExpression(renderFn))
|
|
1047
|
+
return null;
|
|
1048
|
+
// Get the item parameter name from the options bag: ({ item, ... }) => ...
|
|
1049
|
+
const renderParam = renderFn.parameters[0];
|
|
1050
|
+
if (!renderParam)
|
|
1051
|
+
return null;
|
|
1052
|
+
let itemName = null;
|
|
1053
|
+
if (ts.isIdentifier(renderParam.name)) {
|
|
1054
|
+
// Old style: (item) => ... or (item, index) => ...
|
|
1055
|
+
itemName = renderParam.name.text;
|
|
1056
|
+
}
|
|
1057
|
+
else if (ts.isObjectBindingPattern(renderParam.name)) {
|
|
1058
|
+
// New style: ({ item, send, ... }) => ...
|
|
1059
|
+
for (const el of renderParam.name.elements) {
|
|
1060
|
+
if (ts.isBindingElement(el) && ts.isIdentifier(el.name) && el.name.text === 'item') {
|
|
1061
|
+
itemName = 'item';
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (!itemName)
|
|
1067
|
+
return null;
|
|
1068
|
+
const occurrences = [];
|
|
1069
|
+
// Try to extract a simple field name from an arrow selector: (t) => t.FIELD → "FIELD"
|
|
1070
|
+
function extractSimpleField(sel) {
|
|
1071
|
+
if (sel.parameters.length !== 1)
|
|
1072
|
+
return null;
|
|
1073
|
+
const paramName = sel.parameters[0].name;
|
|
1074
|
+
if (!ts.isIdentifier(paramName))
|
|
1075
|
+
return null;
|
|
1076
|
+
const body = ts.isArrowFunction(sel) ? sel.body : null;
|
|
1077
|
+
if (!body)
|
|
1078
|
+
return null;
|
|
1079
|
+
const expr = ts.isBlock(body) ? null : body;
|
|
1080
|
+
if (!expr || !ts.isPropertyAccessExpression(expr))
|
|
1081
|
+
return null;
|
|
1082
|
+
if (!ts.isIdentifier(expr.expression) || expr.expression.text !== paramName.text)
|
|
1083
|
+
return null;
|
|
1084
|
+
if (!ts.isIdentifier(expr.name))
|
|
1085
|
+
return null;
|
|
1086
|
+
return expr.name.text;
|
|
1087
|
+
}
|
|
1088
|
+
function collectItemCalls(node) {
|
|
1089
|
+
// item(selector) calls
|
|
1090
|
+
if (ts.isCallExpression(node) &&
|
|
1091
|
+
ts.isIdentifier(node.expression) &&
|
|
1092
|
+
node.expression.text === itemName &&
|
|
1093
|
+
node.arguments.length === 1) {
|
|
1094
|
+
const sel = node.arguments[0];
|
|
1095
|
+
if (ts.isArrowFunction(sel) || ts.isFunctionExpression(sel)) {
|
|
1096
|
+
const field = extractSimpleField(sel);
|
|
1097
|
+
const key = field !== null
|
|
1098
|
+
? `field:${field}`
|
|
1099
|
+
: `expr:${printer.printNode(ts.EmitHint.Expression, sel, sourceFile)}`;
|
|
1100
|
+
occurrences.push({ kind: 'call', node, selector: sel, key });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// item.FIELD property access — but NOT when it's the callee of a call expression
|
|
1104
|
+
// where we want the original to stay (e.g. item.id() we still replace, because the
|
|
1105
|
+
// accessor itself becomes __a0 and we keep the trailing ()).
|
|
1106
|
+
else if (ts.isPropertyAccessExpression(node) &&
|
|
1107
|
+
ts.isIdentifier(node.expression) &&
|
|
1108
|
+
node.expression.text === itemName &&
|
|
1109
|
+
ts.isIdentifier(node.name)) {
|
|
1110
|
+
const field = node.name.text;
|
|
1111
|
+
occurrences.push({ kind: 'access', node, field, key: `field:${field}` });
|
|
1112
|
+
}
|
|
1113
|
+
ts.forEachChild(node, collectItemCalls);
|
|
1114
|
+
}
|
|
1115
|
+
collectItemCalls(renderFn.body);
|
|
1116
|
+
if (occurrences.length < 2)
|
|
1117
|
+
return null; // nothing to deduplicate
|
|
1118
|
+
// Group by normalized key (field:name or expr:text)
|
|
1119
|
+
const groups = new Map();
|
|
1120
|
+
for (const occ of occurrences) {
|
|
1121
|
+
const existing = groups.get(occ.key);
|
|
1122
|
+
if (existing)
|
|
1123
|
+
existing.push(occ);
|
|
1124
|
+
else
|
|
1125
|
+
groups.set(occ.key, [occ]);
|
|
1126
|
+
}
|
|
1127
|
+
// Hoist ALL occurrences (even unique ones) so compiled code uses `acc()` (plain
|
|
1128
|
+
// function) instead of `item(fn)` (Proxy-wrapped) or `item.FIELD` (Proxy.get trap).
|
|
1129
|
+
// Unique accesses get their own __a* var; duplicates share one.
|
|
1130
|
+
const allGroups = [...groups.entries()];
|
|
1131
|
+
if (allGroups.length === 0)
|
|
1132
|
+
return null;
|
|
1133
|
+
// Build hoisted declarations and replacement map
|
|
1134
|
+
const hoistedStmts = [];
|
|
1135
|
+
const replacements = new Map();
|
|
1136
|
+
let sIdx = 0;
|
|
1137
|
+
for (const [key, occs] of allGroups) {
|
|
1138
|
+
const selVar = `__s${sIdx}`;
|
|
1139
|
+
const accVar = `__a${sIdx}`;
|
|
1140
|
+
sIdx++;
|
|
1141
|
+
// Build the selector expression.
|
|
1142
|
+
// For field:FIELD, synthesize (t) => t.FIELD (or reuse an existing call's selector).
|
|
1143
|
+
// For expr:..., reuse the existing selector expression.
|
|
1144
|
+
let selector;
|
|
1145
|
+
const callOccurrence = occs.find((o) => o.kind === 'call');
|
|
1146
|
+
if (callOccurrence && callOccurrence.kind === 'call') {
|
|
1147
|
+
selector = callOccurrence.selector;
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
// All occurrences are property-access form — synthesize (t) => t.FIELD
|
|
1151
|
+
const firstAccess = occs[0];
|
|
1152
|
+
if (firstAccess.kind !== 'access')
|
|
1153
|
+
throw new Error('unreachable');
|
|
1154
|
+
selector = f.createArrowFunction(undefined, undefined, [f.createParameterDeclaration(undefined, undefined, 't')], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createPropertyAccessExpression(f.createIdentifier('t'), firstAccess.field));
|
|
1155
|
+
}
|
|
1156
|
+
// const __s0 = (r) => r.id
|
|
1157
|
+
hoistedStmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(selVar, undefined, undefined, selector)], ts.NodeFlags.Const)));
|
|
1158
|
+
// const __a0 = acc(__s0) — use the plain-function `acc` instead of `item` (which
|
|
1159
|
+
// is a Proxy). Adds `acc` to the destructure binding below if not already present.
|
|
1160
|
+
hoistedStmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
|
|
1161
|
+
f.createVariableDeclaration(accVar, undefined, undefined, f.createCallExpression(f.createIdentifier('acc'), undefined, [
|
|
1162
|
+
f.createIdentifier(selVar),
|
|
1163
|
+
])),
|
|
1164
|
+
], ts.NodeFlags.Const)));
|
|
1165
|
+
// Map all occurrences to the cached accessor identifier
|
|
1166
|
+
void key; // silence unused
|
|
1167
|
+
for (const occ of occs) {
|
|
1168
|
+
replacements.set(occ.node, f.createIdentifier(accVar));
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// Rewrite the render function body to replace item(sel)/item.field with cached refs
|
|
1172
|
+
function replaceVisitor(node) {
|
|
1173
|
+
if (replacements.has(node)) {
|
|
1174
|
+
return replacements.get(node);
|
|
1175
|
+
}
|
|
1176
|
+
return ts.visitEachChild(node, replaceVisitor, undefined);
|
|
1177
|
+
}
|
|
1178
|
+
const newBody = ts.visitNode(renderFn.body, replaceVisitor);
|
|
1179
|
+
// Prepend hoisted declarations to the body
|
|
1180
|
+
let finalBody;
|
|
1181
|
+
if (ts.isBlock(newBody)) {
|
|
1182
|
+
finalBody = f.createBlock([...hoistedStmts, ...newBody.statements], true);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
// Arrow with expression body → convert to block with return
|
|
1186
|
+
finalBody = f.createBlock([...hoistedStmts, f.createReturnStatement(newBody)], true);
|
|
1187
|
+
}
|
|
1188
|
+
// Ensure `acc` is in the destructure binding pattern of the render param.
|
|
1189
|
+
// Hoisted code references it; if user didn't destructure it, add it.
|
|
1190
|
+
const newParameters = renderFn.parameters.map((p, idx) => {
|
|
1191
|
+
if (idx !== 0)
|
|
1192
|
+
return p;
|
|
1193
|
+
if (!ts.isObjectBindingPattern(p.name))
|
|
1194
|
+
return p;
|
|
1195
|
+
const hasAcc = p.name.elements.some((el) => ts.isBindingElement(el) && ts.isIdentifier(el.name) && el.name.text === 'acc');
|
|
1196
|
+
if (hasAcc)
|
|
1197
|
+
return p;
|
|
1198
|
+
const newBinding = f.createObjectBindingPattern([
|
|
1199
|
+
...p.name.elements,
|
|
1200
|
+
f.createBindingElement(undefined, undefined, f.createIdentifier('acc')),
|
|
1201
|
+
]);
|
|
1202
|
+
return f.createParameterDeclaration(p.modifiers, p.dotDotDotToken, newBinding, p.questionToken, p.type, p.initializer);
|
|
1203
|
+
});
|
|
1204
|
+
// Build new render function
|
|
1205
|
+
const newRenderFn = ts.isArrowFunction(renderFn)
|
|
1206
|
+
? f.createArrowFunction(renderFn.modifiers, renderFn.typeParameters, newParameters, renderFn.type, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), finalBody)
|
|
1207
|
+
: f.createFunctionExpression(renderFn.modifiers, renderFn.asteriskToken, renderFn.name, renderFn.typeParameters, newParameters, renderFn.type, finalBody);
|
|
1208
|
+
// Rebuild the each() call with the new render property
|
|
1209
|
+
const newProps = arg.properties.map((prop) => prop === renderProp ? f.createPropertyAssignment('render', newRenderFn) : prop);
|
|
1210
|
+
const newArg = f.createObjectLiteralExpression(newProps, true);
|
|
1211
|
+
return f.createCallExpression(eachCall.expression, eachCall.typeArguments, [
|
|
1212
|
+
newArg,
|
|
1213
|
+
...eachCall.arguments.slice(1),
|
|
1214
|
+
]);
|
|
1215
|
+
}
|
|
1216
|
+
// ── Auto-memoize each() items accessor ──────────────────────────
|
|
1217
|
+
const ALLOCATING_METHODS = new Set([
|
|
1218
|
+
'filter',
|
|
1219
|
+
'map',
|
|
1220
|
+
'slice',
|
|
1221
|
+
'sort',
|
|
1222
|
+
'reverse',
|
|
1223
|
+
'concat',
|
|
1224
|
+
'flat',
|
|
1225
|
+
'flatMap',
|
|
1226
|
+
'reduce',
|
|
1227
|
+
]);
|
|
1228
|
+
/**
|
|
1229
|
+
* Detect whether an expression body contains array-allocating operations
|
|
1230
|
+
* that would produce a new array on every call.
|
|
1231
|
+
*/
|
|
1232
|
+
function accessorAllocatesArray(body) {
|
|
1233
|
+
let found = false;
|
|
1234
|
+
function walk(n) {
|
|
1235
|
+
if (found)
|
|
1236
|
+
return;
|
|
1237
|
+
// .method() on something — check the method name
|
|
1238
|
+
if (ts.isCallExpression(n) &&
|
|
1239
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
1240
|
+
ts.isIdentifier(n.expression.name) &&
|
|
1241
|
+
ALLOCATING_METHODS.has(n.expression.name.text)) {
|
|
1242
|
+
found = true;
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
// Spread in array literal: [...x, y]
|
|
1246
|
+
if (ts.isArrayLiteralExpression(n) && n.elements.some((el) => ts.isSpreadElement(el))) {
|
|
1247
|
+
found = true;
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
// Array.from(...)
|
|
1251
|
+
if (ts.isCallExpression(n) &&
|
|
1252
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
1253
|
+
ts.isIdentifier(n.expression.expression) &&
|
|
1254
|
+
n.expression.expression.text === 'Array' &&
|
|
1255
|
+
ts.isIdentifier(n.expression.name) &&
|
|
1256
|
+
n.expression.name.text === 'from') {
|
|
1257
|
+
found = true;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
ts.forEachChild(n, walk);
|
|
1261
|
+
}
|
|
1262
|
+
walk(body);
|
|
1263
|
+
return found;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Wrap `each({ items: (s) => s.x.filter(...) })` in `memo()` with a bitmask,
|
|
1267
|
+
* so the filter is only re-run when its dependencies change. For items accessors
|
|
1268
|
+
* that don't allocate (e.g. `(s) => s.items`), each's built-in same-ref fast
|
|
1269
|
+
* path already suffices — no wrap needed.
|
|
1270
|
+
*
|
|
1271
|
+
* Returns null if no wrapping was applied.
|
|
1272
|
+
*/
|
|
1273
|
+
function tryWrapEachItemsWithMemo(eachCall, fieldBits, f) {
|
|
1274
|
+
const arg = eachCall.arguments[0];
|
|
1275
|
+
if (!arg || !ts.isObjectLiteralExpression(arg))
|
|
1276
|
+
return null;
|
|
1277
|
+
let itemsProp = null;
|
|
1278
|
+
for (const prop of arg.properties) {
|
|
1279
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'items') {
|
|
1280
|
+
itemsProp = prop;
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (!itemsProp)
|
|
1285
|
+
return null;
|
|
1286
|
+
const accessor = itemsProp.initializer;
|
|
1287
|
+
if (!ts.isArrowFunction(accessor) && !ts.isFunctionExpression(accessor))
|
|
1288
|
+
return null;
|
|
1289
|
+
// Don't wrap if it's already wrapped (call expression like memo(...) or similar)
|
|
1290
|
+
// We only wrap raw arrow functions.
|
|
1291
|
+
// Skip if the body doesn't allocate — each's own ref check handles those.
|
|
1292
|
+
const body = ts.isArrowFunction(accessor) ? accessor.body : accessor.body;
|
|
1293
|
+
if (!accessorAllocatesArray(body))
|
|
1294
|
+
return null;
|
|
1295
|
+
const { mask, readsState } = computeAccessorMask(accessor, fieldBits);
|
|
1296
|
+
if (mask === 0 && !readsState)
|
|
1297
|
+
return null; // constant, nothing to memoize
|
|
1298
|
+
const finalMask = mask === 0 && readsState ? 0xffffffff | 0 : mask;
|
|
1299
|
+
// Wrap: memo(accessor, mask)
|
|
1300
|
+
const wrapped = f.createCallExpression(f.createIdentifier('memo'), undefined, [
|
|
1301
|
+
accessor,
|
|
1302
|
+
createMaskLiteral(f, finalMask),
|
|
1303
|
+
]);
|
|
1304
|
+
const newProps = arg.properties.map((p) => p === itemsProp ? f.createPropertyAssignment('items', wrapped) : p);
|
|
1305
|
+
const newArg = f.createObjectLiteralExpression(newProps, true);
|
|
1306
|
+
return f.createCallExpression(eachCall.expression, eachCall.typeArguments, [
|
|
1307
|
+
newArg,
|
|
1308
|
+
...eachCall.arguments.slice(1),
|
|
1309
|
+
]);
|
|
1310
|
+
}
|
|
1311
|
+
// ── Subtree collapse: nested elements → elTemplate ──────────────
|
|
1312
|
+
const VOID_ELEMENTS = new Set([
|
|
1313
|
+
'area',
|
|
1314
|
+
'base',
|
|
1315
|
+
'br',
|
|
1316
|
+
'col',
|
|
1317
|
+
'embed',
|
|
1318
|
+
'hr',
|
|
1319
|
+
'img',
|
|
1320
|
+
'input',
|
|
1321
|
+
'link',
|
|
1322
|
+
'meta',
|
|
1323
|
+
'param',
|
|
1324
|
+
'source',
|
|
1325
|
+
'track',
|
|
1326
|
+
'wbr',
|
|
1327
|
+
]);
|
|
1328
|
+
/**
|
|
1329
|
+
* Try to analyze an element call and all its descendants as a collapsible subtree.
|
|
1330
|
+
* Returns null if any part of the tree is not eligible for collapse.
|
|
1331
|
+
*/
|
|
1332
|
+
function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
1333
|
+
if (!ts.isIdentifier(node.expression))
|
|
1334
|
+
return null;
|
|
1335
|
+
const localName = node.expression.text;
|
|
1336
|
+
const tag = helpers.get(localName);
|
|
1337
|
+
if (!tag)
|
|
1338
|
+
return null;
|
|
1339
|
+
// Handle children-only overload: `div([...])` — first arg is the children array.
|
|
1340
|
+
// In that case, treat it as no props + children=firstArg.
|
|
1341
|
+
const firstArg = node.arguments[0];
|
|
1342
|
+
const usesChildrenOnlyOverload = firstArg && ts.isArrayLiteralExpression(firstArg);
|
|
1343
|
+
const propsArg = usesChildrenOnlyOverload ? undefined : firstArg;
|
|
1344
|
+
const childrenArg = usesChildrenOnlyOverload ? firstArg : node.arguments[1];
|
|
1345
|
+
if (propsArg && !ts.isObjectLiteralExpression(propsArg))
|
|
1346
|
+
return null;
|
|
1347
|
+
const staticAttrs = [];
|
|
1348
|
+
const events = [];
|
|
1349
|
+
const bindings = [];
|
|
1350
|
+
if (propsArg && ts.isObjectLiteralExpression(propsArg)) {
|
|
1351
|
+
for (const prop of propsArg.properties) {
|
|
1352
|
+
let key;
|
|
1353
|
+
let value;
|
|
1354
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
1355
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name))
|
|
1356
|
+
return null;
|
|
1357
|
+
key = ts.isIdentifier(prop.name) ? prop.name.text : prop.name.text;
|
|
1358
|
+
value = prop.initializer;
|
|
1359
|
+
}
|
|
1360
|
+
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1361
|
+
key = prop.name.text;
|
|
1362
|
+
value = prop.name;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
if (key === 'key')
|
|
1368
|
+
continue;
|
|
1369
|
+
// Event handler
|
|
1370
|
+
if (/^on[A-Z]/.test(key)) {
|
|
1371
|
+
events.push([key.slice(2).toLowerCase(), value]);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
// Reactive binding
|
|
1375
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
1376
|
+
const kind = classifyKind(key);
|
|
1377
|
+
const resolvedKey = resolveKey(key, kind);
|
|
1378
|
+
const { mask, readsState } = computeAccessorMask(value, fieldBits);
|
|
1379
|
+
if (mask === 0 && !readsState) {
|
|
1380
|
+
// Constant fold — treat as static if we can extract a string
|
|
1381
|
+
const staticVal = tryExtractStaticString(value);
|
|
1382
|
+
if (staticVal !== null) {
|
|
1383
|
+
const attrKey = kind === 'class' ? 'class' : resolvedKey;
|
|
1384
|
+
staticAttrs.push([attrKey, staticVal]);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
bindings.push([mask === 0 && readsState ? 0xffffffff | 0 : mask, kind, resolvedKey, value]);
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
// Per-item accessor call
|
|
1392
|
+
if (ts.isCallExpression(value) && isPerItemCall(value)) {
|
|
1393
|
+
const kind = classifyKind(key);
|
|
1394
|
+
const resolvedKey = resolveKey(key, kind);
|
|
1395
|
+
bindings.push([0xffffffff | 0, kind, resolvedKey, value]);
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
// Per-item property access: item.field (or hoisted __a0/__a1/…)
|
|
1399
|
+
if (isPerItemFieldAccess(value) || isHoistedPerItem(value)) {
|
|
1400
|
+
const kind = classifyKind(key);
|
|
1401
|
+
const resolvedKey = resolveKey(key, kind);
|
|
1402
|
+
bindings.push([0xffffffff | 0, kind, resolvedKey, value]);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
// Static literal prop
|
|
1406
|
+
if (ts.isStringLiteral(value)) {
|
|
1407
|
+
const kind = classifyKind(key);
|
|
1408
|
+
const attrKey = kind === 'class' ? 'class' : resolveKey(key, kind);
|
|
1409
|
+
staticAttrs.push([attrKey, value.text]);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (ts.isNumericLiteral(value)) {
|
|
1413
|
+
const kind = classifyKind(key);
|
|
1414
|
+
const attrKey = kind === 'class' ? 'class' : resolveKey(key, kind);
|
|
1415
|
+
staticAttrs.push([attrKey, value.text]);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1419
|
+
const kind = classifyKind(key);
|
|
1420
|
+
const attrKey = kind === 'class' ? 'class' : resolveKey(key, kind);
|
|
1421
|
+
staticAttrs.push([attrKey, '']);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
// Non-literal prop — can't collapse
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
// Analyze children
|
|
1429
|
+
const children = [];
|
|
1430
|
+
if (childrenArg && ts.isArrayLiteralExpression(childrenArg)) {
|
|
1431
|
+
let childIdx = 0;
|
|
1432
|
+
for (const child of childrenArg.elements) {
|
|
1433
|
+
// String literal child — static text node
|
|
1434
|
+
if (ts.isStringLiteral(child) || ts.isNoSubstitutionTemplateLiteral(child)) {
|
|
1435
|
+
children.push({ type: 'staticText', value: child.text });
|
|
1436
|
+
childIdx++;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
// text('literal') — static text
|
|
1440
|
+
if (ts.isCallExpression(child) &&
|
|
1441
|
+
ts.isIdentifier(child.expression) &&
|
|
1442
|
+
child.expression.text === 'text') {
|
|
1443
|
+
if (child.arguments.length >= 1 && ts.isStringLiteral(child.arguments[0])) {
|
|
1444
|
+
children.push({ type: 'staticText', value: child.arguments[0].text });
|
|
1445
|
+
childIdx++; // static text creates a text node in the template DOM
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
// Reactive text — accessor is first arg
|
|
1449
|
+
const accessor = child.arguments[0];
|
|
1450
|
+
if (ts.isArrowFunction(accessor) || ts.isFunctionExpression(accessor)) {
|
|
1451
|
+
const { mask, readsState } = computeAccessorMask(accessor, fieldBits);
|
|
1452
|
+
children.push({
|
|
1453
|
+
type: 'reactiveText',
|
|
1454
|
+
accessor,
|
|
1455
|
+
mask: mask === 0 && readsState ? 0xffffffff | 0 : mask,
|
|
1456
|
+
childIdx,
|
|
1457
|
+
});
|
|
1458
|
+
childIdx++; // placeholder text node in template
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
// Per-item text: text(item(t => t.label))
|
|
1462
|
+
if (ts.isCallExpression(accessor) && isPerItemCall(accessor)) {
|
|
1463
|
+
children.push({
|
|
1464
|
+
type: 'reactiveText',
|
|
1465
|
+
accessor,
|
|
1466
|
+
mask: 0xffffffff | 0,
|
|
1467
|
+
childIdx,
|
|
1468
|
+
});
|
|
1469
|
+
childIdx++; // placeholder text node in template
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
// Per-item text via property access: text(item.label)
|
|
1473
|
+
// Also matches hoisted __a0/__a1/… identifiers produced by dedup.
|
|
1474
|
+
if (isPerItemFieldAccess(accessor) || isHoistedPerItem(accessor)) {
|
|
1475
|
+
children.push({
|
|
1476
|
+
type: 'reactiveText',
|
|
1477
|
+
accessor,
|
|
1478
|
+
mask: 0xffffffff | 0,
|
|
1479
|
+
childIdx,
|
|
1480
|
+
});
|
|
1481
|
+
childIdx++;
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
return null; // unsupported text() form
|
|
1485
|
+
}
|
|
1486
|
+
// Element helper call — recurse
|
|
1487
|
+
if (ts.isCallExpression(child) &&
|
|
1488
|
+
ts.isIdentifier(child.expression) &&
|
|
1489
|
+
helpers.has(child.expression.text)) {
|
|
1490
|
+
const childNode = analyzeSubtree(child, helpers, fieldBits, [...path, childIdx]);
|
|
1491
|
+
if (!childNode)
|
|
1492
|
+
return null;
|
|
1493
|
+
children.push({ type: 'element', node: childNode });
|
|
1494
|
+
childIdx++;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
// Anything else (each, branch, show, arbitrary expressions) — bail
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
// Note: mixed static + reactive text in the same parent is now supported
|
|
1501
|
+
// because reactive text uses <!--$--> comment placeholders that break
|
|
1502
|
+
// text-node merging at parse time.
|
|
1503
|
+
}
|
|
1504
|
+
else if (childrenArg && childrenArg.kind !== ts.SyntaxKind.NullKeyword) {
|
|
1505
|
+
// Non-array children (e.g., spread, variable) — bail
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
return { tag, localName, staticAttrs, events, bindings, children, path };
|
|
1509
|
+
}
|
|
1510
|
+
function tryExtractStaticString(accessor) {
|
|
1511
|
+
const body = ts.isArrowFunction(accessor) ? accessor.body : null;
|
|
1512
|
+
if (body && ts.isStringLiteral(body))
|
|
1513
|
+
return body.text;
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Check if a subtree has any nested element children (worth collapsing).
|
|
1518
|
+
*/
|
|
1519
|
+
function hasNestedElements(node) {
|
|
1520
|
+
return node.children.some((c) => c.type === 'element');
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Collect all local helper names used in the subtree for import cleanup.
|
|
1524
|
+
*/
|
|
1525
|
+
function collectUsedHelpers(node, out) {
|
|
1526
|
+
out.add(node.localName);
|
|
1527
|
+
for (const child of node.children) {
|
|
1528
|
+
if (child.type === 'element')
|
|
1529
|
+
collectUsedHelpers(child.node, out);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Build the static HTML string from an analyzed subtree.
|
|
1534
|
+
*/
|
|
1535
|
+
function buildTemplateHTML(node) {
|
|
1536
|
+
let html = `<${node.tag}`;
|
|
1537
|
+
for (const [key, value] of node.staticAttrs) {
|
|
1538
|
+
html += ` ${key}="${escapeAttr(value)}"`;
|
|
1539
|
+
}
|
|
1540
|
+
html += '>';
|
|
1541
|
+
if (VOID_ELEMENTS.has(node.tag))
|
|
1542
|
+
return html;
|
|
1543
|
+
for (let ci = 0; ci < node.children.length; ci++) {
|
|
1544
|
+
const child = node.children[ci];
|
|
1545
|
+
if (child.type === 'staticText') {
|
|
1546
|
+
html += escapeHTML(child.value);
|
|
1547
|
+
}
|
|
1548
|
+
else if (child.type === 'element') {
|
|
1549
|
+
html += buildTemplateHTML(child.node);
|
|
1550
|
+
}
|
|
1551
|
+
else if (child.type === 'reactiveText') {
|
|
1552
|
+
// When the reactive text is not adjacent to another text-type child,
|
|
1553
|
+
// we can use a literal text node placeholder instead of a comment.
|
|
1554
|
+
// The cloned text node is reused in the patch function — no
|
|
1555
|
+
// createTextNode + replaceChild needed. This saves 2 DOM operations
|
|
1556
|
+
// per text binding per row.
|
|
1557
|
+
//
|
|
1558
|
+
// When adjacent text WOULD cause HTML-parser merging (two text nodes
|
|
1559
|
+
// collapse into one), we fall back to the comment placeholder.
|
|
1560
|
+
const prev = ci > 0 ? node.children[ci - 1] : null;
|
|
1561
|
+
const next = ci < node.children.length - 1 ? node.children[ci + 1] : null;
|
|
1562
|
+
const adjText = prev?.type === 'staticText' ||
|
|
1563
|
+
prev?.type === 'reactiveText' ||
|
|
1564
|
+
next?.type === 'staticText' ||
|
|
1565
|
+
next?.type === 'reactiveText';
|
|
1566
|
+
if (adjText) {
|
|
1567
|
+
html += '<!--$-->';
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
// Space character becomes a Text node in the cloned template.
|
|
1571
|
+
// Mark the child so the patch codegen knows to skip replaceChild.
|
|
1572
|
+
html += ' ';
|
|
1573
|
+
child.inlineText = true;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
html += `</${node.tag}>`;
|
|
1578
|
+
return html;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Collect all patch operations from an analyzed subtree.
|
|
1582
|
+
*/
|
|
1583
|
+
function collectPatchOps(node, f, rootExpr, ops, counter) {
|
|
1584
|
+
const hasDynamic = node.events.length > 0 ||
|
|
1585
|
+
node.bindings.length > 0 ||
|
|
1586
|
+
node.children.some((c) => c.type === 'reactiveText');
|
|
1587
|
+
let nodeExpr = rootExpr;
|
|
1588
|
+
if (hasDynamic) {
|
|
1589
|
+
const varName = `__n${counter.n++}`;
|
|
1590
|
+
// Build walk expression: root.childNodes[i].childNodes[j]...
|
|
1591
|
+
nodeExpr = f.createIdentifier(varName);
|
|
1592
|
+
ops.push({
|
|
1593
|
+
varName,
|
|
1594
|
+
walkExpr: buildWalkExpr(node.path, f),
|
|
1595
|
+
events: node.events,
|
|
1596
|
+
bindings: node.bindings,
|
|
1597
|
+
reactiveTexts: node.children.filter((c) => c.type === 'reactiveText'),
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
// Recurse into element children
|
|
1601
|
+
for (const child of node.children) {
|
|
1602
|
+
if (child.type === 'element') {
|
|
1603
|
+
collectPatchOps(child.node, f, nodeExpr, ops, counter);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function buildWalkExpr(path, f) {
|
|
1608
|
+
let expr = f.createIdentifier('root');
|
|
1609
|
+
for (const idx of path) {
|
|
1610
|
+
// Use firstChild + nextSibling chain instead of childNodes[n]
|
|
1611
|
+
// firstChild/nextSibling are direct pointer lookups, childNodes is a live NodeList
|
|
1612
|
+
expr = f.createPropertyAccessExpression(expr, 'firstChild');
|
|
1613
|
+
for (let i = 0; i < idx; i++) {
|
|
1614
|
+
expr = f.createPropertyAccessExpression(expr, 'nextSibling');
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return expr;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Emit elTemplate(htmlString, (root, __bind) => { ... }) call.
|
|
1621
|
+
*/
|
|
1622
|
+
function emitSubtreeTemplate(analyzed, fieldBits, f) {
|
|
1623
|
+
const html = buildTemplateHTML(analyzed);
|
|
1624
|
+
const ops = [];
|
|
1625
|
+
const counter = { n: 0, t: 0 };
|
|
1626
|
+
// Collect root-level patches
|
|
1627
|
+
const rootHasDynamic = analyzed.events.length > 0 ||
|
|
1628
|
+
analyzed.bindings.length > 0 ||
|
|
1629
|
+
analyzed.children.some((c) => c.type === 'reactiveText');
|
|
1630
|
+
if (rootHasDynamic) {
|
|
1631
|
+
ops.push({
|
|
1632
|
+
varName: '', // use 'root' directly
|
|
1633
|
+
walkExpr: f.createIdentifier('root'),
|
|
1634
|
+
events: analyzed.events,
|
|
1635
|
+
bindings: analyzed.bindings,
|
|
1636
|
+
reactiveTexts: analyzed.children.filter((c) => c.type === 'reactiveText'),
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
// Collect child patches
|
|
1640
|
+
for (const child of analyzed.children) {
|
|
1641
|
+
if (child.type === 'element') {
|
|
1642
|
+
collectPatchOps(child.node, f, f.createIdentifier('root'), ops, counter);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
// Collect delegatable events: group by event type across all ops
|
|
1646
|
+
// Events on child nodes with the same type are delegated to the root
|
|
1647
|
+
const delegatableEvents = new Map();
|
|
1648
|
+
for (const op of ops) {
|
|
1649
|
+
for (const [eventName, handler] of op.events) {
|
|
1650
|
+
if (!op.varName) {
|
|
1651
|
+
// Root-level events — can't delegate further up
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
const list = delegatableEvents.get(eventName);
|
|
1655
|
+
if (list)
|
|
1656
|
+
list.push({ nodeVar: op.varName, handler });
|
|
1657
|
+
else
|
|
1658
|
+
delegatableEvents.set(eventName, [{ nodeVar: op.varName, handler }]);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// Build patch function body
|
|
1662
|
+
const stmts = [];
|
|
1663
|
+
for (const op of ops) {
|
|
1664
|
+
const nodeRef = op.varName ? f.createIdentifier(op.varName) : f.createIdentifier('root');
|
|
1665
|
+
// Variable declaration for walking to node
|
|
1666
|
+
if (op.varName) {
|
|
1667
|
+
stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(op.varName, undefined, undefined, op.walkExpr)], ts.NodeFlags.Const)));
|
|
1668
|
+
}
|
|
1669
|
+
// Non-delegatable events (root-level or single-use event types)
|
|
1670
|
+
for (const [eventName, handler] of op.events) {
|
|
1671
|
+
const delegated = delegatableEvents.get(eventName);
|
|
1672
|
+
if (op.varName && delegated && delegated.length >= 2)
|
|
1673
|
+
continue; // handled below
|
|
1674
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(nodeRef, 'addEventListener'), undefined, [f.createStringLiteral(eventName), handler])));
|
|
1675
|
+
}
|
|
1676
|
+
// Reactive text children — walk to placeholder, create text node, bind
|
|
1677
|
+
for (const rt of op.reactiveTexts) {
|
|
1678
|
+
const tVar = `__t${counter.t++}`;
|
|
1679
|
+
const isInline = !!rt.inlineText;
|
|
1680
|
+
if (isInline) {
|
|
1681
|
+
// Inline text placeholder: the template HTML has a space character
|
|
1682
|
+
// that cloneNode already created as a Text node. Walk to it and
|
|
1683
|
+
// bind directly — no createTextNode, no replaceChild.
|
|
1684
|
+
let walk = f.createPropertyAccessExpression(nodeRef, 'firstChild');
|
|
1685
|
+
for (let i = 0; i < rt.childIdx; i++) {
|
|
1686
|
+
walk = f.createPropertyAccessExpression(walk, 'nextSibling');
|
|
1687
|
+
}
|
|
1688
|
+
stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(tVar, undefined, undefined, walk)], ts.NodeFlags.Const)));
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
// Comment placeholder: create a new text node and replace the comment.
|
|
1692
|
+
const cVar = `__c${counter.t - 1}`;
|
|
1693
|
+
let walk = f.createPropertyAccessExpression(nodeRef, 'firstChild');
|
|
1694
|
+
for (let i = 0; i < rt.childIdx; i++) {
|
|
1695
|
+
walk = f.createPropertyAccessExpression(walk, 'nextSibling');
|
|
1696
|
+
}
|
|
1697
|
+
stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(cVar, undefined, undefined, walk)], ts.NodeFlags.Const)));
|
|
1698
|
+
stmts.push(f.createVariableStatement(undefined, f.createVariableDeclarationList([
|
|
1699
|
+
f.createVariableDeclaration(tVar, undefined, undefined, f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('document'), 'createTextNode'), undefined, [f.createStringLiteral('')])),
|
|
1700
|
+
], ts.NodeFlags.Const)));
|
|
1701
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier(cVar), 'parentNode'), 'replaceChild'), undefined, [f.createIdentifier(tVar), f.createIdentifier(cVar)])));
|
|
1702
|
+
}
|
|
1703
|
+
// __bind(__t0, mask, 'text', undefined, accessor)
|
|
1704
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, [
|
|
1705
|
+
f.createIdentifier(tVar),
|
|
1706
|
+
createMaskLiteral(f, rt.mask),
|
|
1707
|
+
f.createStringLiteral('text'),
|
|
1708
|
+
f.createIdentifier('undefined'),
|
|
1709
|
+
rt.accessor,
|
|
1710
|
+
])));
|
|
1711
|
+
}
|
|
1712
|
+
// Reactive bindings — __bind(node, mask, kind, key, accessor)
|
|
1713
|
+
for (const [mask, kind, key, accessor] of op.bindings) {
|
|
1714
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, [
|
|
1715
|
+
nodeRef,
|
|
1716
|
+
createMaskLiteral(f, mask),
|
|
1717
|
+
f.createStringLiteral(kind),
|
|
1718
|
+
key ? f.createStringLiteral(key) : f.createIdentifier('undefined'),
|
|
1719
|
+
accessor,
|
|
1720
|
+
])));
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
// Emit delegated event listeners on root
|
|
1724
|
+
for (const [eventName, entries] of delegatableEvents) {
|
|
1725
|
+
if (entries.length < 2)
|
|
1726
|
+
continue;
|
|
1727
|
+
// root.onclick = (e) => { if (n1.contains(e.target)) { h1(); return } if (n2.contains(e.target)) { h2(); return } }
|
|
1728
|
+
const eParam = f.createIdentifier('__e');
|
|
1729
|
+
const eTarget = f.createPropertyAccessExpression(eParam, 'target');
|
|
1730
|
+
const ifStmts = [];
|
|
1731
|
+
for (const { nodeVar, handler } of entries) {
|
|
1732
|
+
// if (nodeVar.contains(e.target)) { handler(e); return }
|
|
1733
|
+
ifStmts.push(f.createIfStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier(nodeVar), 'contains'), undefined, [eTarget]), f.createBlock([
|
|
1734
|
+
f.createExpressionStatement(f.createCallExpression(handler, undefined, [eParam])),
|
|
1735
|
+
f.createReturnStatement(),
|
|
1736
|
+
], true)));
|
|
1737
|
+
}
|
|
1738
|
+
const delegateHandler = f.createArrowFunction(undefined, undefined, [f.createParameterDeclaration(undefined, undefined, '__e')], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(ifStmts, true));
|
|
1739
|
+
// root.addEventListener(eventName, handler)
|
|
1740
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('root'), 'addEventListener'), undefined, [f.createStringLiteral(eventName), delegateHandler])));
|
|
1741
|
+
}
|
|
1742
|
+
// (root, __bind) => { ... }
|
|
1743
|
+
const patchFn = f.createArrowFunction(undefined, undefined, [
|
|
1744
|
+
f.createParameterDeclaration(undefined, undefined, 'root'),
|
|
1745
|
+
f.createParameterDeclaration(undefined, undefined, '__bind'),
|
|
1746
|
+
], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock(stmts, true));
|
|
1747
|
+
const call = f.createCallExpression(f.createIdentifier('elTemplate'), undefined, [
|
|
1748
|
+
f.createStringLiteral(html),
|
|
1749
|
+
patchFn,
|
|
1750
|
+
]);
|
|
1751
|
+
return call;
|
|
1752
|
+
}
|
|
1753
|
+
// ── Static subtree detection ─────────────────────────────────────
|
|
1754
|
+
function isStaticChildren(children) {
|
|
1755
|
+
if (children.kind === ts.SyntaxKind.NullKeyword)
|
|
1756
|
+
return true;
|
|
1757
|
+
if (!ts.isArrayLiteralExpression(children))
|
|
1758
|
+
return false;
|
|
1759
|
+
return children.elements.every((child) => {
|
|
1760
|
+
// text('literal') — static text
|
|
1761
|
+
if (ts.isCallExpression(child) &&
|
|
1762
|
+
ts.isIdentifier(child.expression) &&
|
|
1763
|
+
child.expression.text === 'text') {
|
|
1764
|
+
return child.arguments.length === 1 && ts.isStringLiteral(child.arguments[0]);
|
|
1765
|
+
}
|
|
1766
|
+
// Another elSplit or element helper that was already determined static
|
|
1767
|
+
// For now, only handle text() children
|
|
1768
|
+
return false;
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
function buildStaticHTML(tag, staticProps, children, _f) {
|
|
1772
|
+
// Extract static attributes from staticFn statements
|
|
1773
|
+
let attrs = '';
|
|
1774
|
+
for (const stmt of staticProps) {
|
|
1775
|
+
if (!ts.isExpressionStatement(stmt))
|
|
1776
|
+
return null;
|
|
1777
|
+
const expr = stmt.expression;
|
|
1778
|
+
// __e.className = 'value'
|
|
1779
|
+
if (ts.isBinaryExpression(expr) && ts.isPropertyAccessExpression(expr.left)) {
|
|
1780
|
+
const prop = expr.left.name.text;
|
|
1781
|
+
if (prop === 'className' && ts.isStringLiteral(expr.right)) {
|
|
1782
|
+
attrs += ` class="${escapeAttr(expr.right.text)}"`;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
// __e.setAttribute('key', 'value')
|
|
1786
|
+
if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) {
|
|
1787
|
+
if (expr.expression.name.text === 'setAttribute' && expr.arguments.length === 2) {
|
|
1788
|
+
const key = expr.arguments[0];
|
|
1789
|
+
const val = expr.arguments[1];
|
|
1790
|
+
if (key && val && ts.isStringLiteral(key) && ts.isStringLiteral(val)) {
|
|
1791
|
+
attrs += ` ${key.text}="${escapeAttr(val.text)}"`;
|
|
1792
|
+
}
|
|
1793
|
+
else {
|
|
1794
|
+
return null; // non-literal attribute
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
// Extract text children
|
|
1800
|
+
let inner = '';
|
|
1801
|
+
if (ts.isArrayLiteralExpression(children)) {
|
|
1802
|
+
for (const child of children.elements) {
|
|
1803
|
+
if (ts.isCallExpression(child) &&
|
|
1804
|
+
ts.isIdentifier(child.expression) &&
|
|
1805
|
+
child.expression.text === 'text') {
|
|
1806
|
+
if (ts.isStringLiteral(child.arguments[0])) {
|
|
1807
|
+
inner += escapeHTML(child.arguments[0].text);
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
1819
|
+
}
|
|
1820
|
+
function escapeHTML(s) {
|
|
1821
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1822
|
+
}
|
|
1823
|
+
function escapeAttr(s) {
|
|
1824
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
1825
|
+
}
|
|
1826
|
+
let templateCounter = 0;
|
|
1827
|
+
function emitTemplateClone(html, f) {
|
|
1828
|
+
const varName = `__tmpl${templateCounter++}`;
|
|
1829
|
+
// Emit: (() => { const t = document.createElement('template'); t.innerHTML = 'html'; return t.content.cloneNode(true).firstChild })()
|
|
1830
|
+
// Simplified: we use an IIFE that creates and caches a template
|
|
1831
|
+
// Actually, for simplicity, just emit the cloneNode inline
|
|
1832
|
+
const tmplCreate = f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('document'), 'createElement'), undefined, [f.createStringLiteral('template')]);
|
|
1833
|
+
const iife = f.createCallExpression(f.createParenthesizedExpression(f.createArrowFunction(undefined, undefined, [], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createBlock([
|
|
1834
|
+
f.createVariableStatement(undefined, f.createVariableDeclarationList([f.createVariableDeclaration(varName, undefined, undefined, tmplCreate)], ts.NodeFlags.Const)),
|
|
1835
|
+
f.createExpressionStatement(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier(varName), 'innerHTML'), ts.SyntaxKind.EqualsToken, f.createStringLiteral(html))),
|
|
1836
|
+
f.createReturnStatement(f.createPropertyAccessExpression(f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier(varName), 'content'), 'cloneNode'), undefined, [f.createTrue()]), 'firstChild')),
|
|
1837
|
+
], true))), undefined, []);
|
|
1838
|
+
return iife;
|
|
1839
|
+
}
|
|
1840
|
+
function isPerItemCall(node) {
|
|
1841
|
+
// Matches: item(t => t.field) or item(t => expr)
|
|
1842
|
+
// where item is an identifier (the scoped accessor from each() render)
|
|
1843
|
+
if (!ts.isIdentifier(node.expression))
|
|
1844
|
+
return false;
|
|
1845
|
+
// Check that the first argument is an arrow function (the selector)
|
|
1846
|
+
if (node.arguments.length !== 1)
|
|
1847
|
+
return false;
|
|
1848
|
+
const arg = node.arguments[0];
|
|
1849
|
+
return ts.isArrowFunction(arg) || ts.isFunctionExpression(arg);
|
|
1850
|
+
}
|
|
1851
|
+
// Matches: item.FIELD — the item-proxy shorthand equivalent of item(t => t.FIELD).
|
|
1852
|
+
// Loose heuristic: any `IDENT.IDENT` where the left side is the bare identifier `item`.
|
|
1853
|
+
// The runtime detects per-item via accessor.length === 0, so passing the property access
|
|
1854
|
+
// directly as a binding accessor works regardless of what the compiler assumes.
|
|
1855
|
+
function isPerItemFieldAccess(node) {
|
|
1856
|
+
if (!ts.isPropertyAccessExpression(node))
|
|
1857
|
+
return false;
|
|
1858
|
+
if (!ts.isIdentifier(node.expression))
|
|
1859
|
+
return false;
|
|
1860
|
+
if (node.expression.text !== 'item')
|
|
1861
|
+
return false;
|
|
1862
|
+
if (!ts.isIdentifier(node.name))
|
|
1863
|
+
return false;
|
|
1864
|
+
return true;
|
|
1865
|
+
}
|
|
1866
|
+
// Matches the hoisted identifiers produced by tryDeduplicateItemSelectors: __a0, __a1, …
|
|
1867
|
+
// These represent already-cached per-item accessors.
|
|
1868
|
+
function isHoistedPerItem(node) {
|
|
1869
|
+
if (!ts.isIdentifier(node))
|
|
1870
|
+
return false;
|
|
1871
|
+
return /^__a\d+$/.test(node.text);
|
|
1872
|
+
}
|
|
1873
|
+
// ── Mask computation ─────────────────────────────────────────────
|
|
1874
|
+
// Returns { mask, readsState }
|
|
1875
|
+
// mask = 0 + readsState = false → constant (can fold to static)
|
|
1876
|
+
// mask = 0 + readsState = true → unresolvable state access (FULL_MASK)
|
|
1877
|
+
// mask > 0 → precise mask
|
|
1878
|
+
function computeAccessorMask(accessor, fieldBits) {
|
|
1879
|
+
if (accessor.parameters.length === 0)
|
|
1880
|
+
return { mask: 0xffffffff | 0, readsState: false };
|
|
1881
|
+
const paramName = accessor.parameters[0].name;
|
|
1882
|
+
if (!ts.isIdentifier(paramName))
|
|
1883
|
+
return { mask: 0xffffffff | 0, readsState: false };
|
|
1884
|
+
const stateParam = paramName.text;
|
|
1885
|
+
let mask = 0;
|
|
1886
|
+
let readsState = false;
|
|
1887
|
+
function walk(node) {
|
|
1888
|
+
if (ts.isIdentifier(node) && node.text === stateParam && !ts.isParameter(node.parent)) {
|
|
1889
|
+
readsState = true;
|
|
1890
|
+
}
|
|
1891
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
1892
|
+
if (!ts.isPropertyAccessExpression(node.parent)) {
|
|
1893
|
+
const chain = resolveChain(node, stateParam);
|
|
1894
|
+
if (chain) {
|
|
1895
|
+
const bit = fieldBits.get(chain);
|
|
1896
|
+
if (bit !== undefined) {
|
|
1897
|
+
mask |= bit;
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
for (const [path, b] of fieldBits) {
|
|
1901
|
+
if (path.startsWith(chain + '.') || path === chain) {
|
|
1902
|
+
mask |= b;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
ts.forEachChild(node, walk);
|
|
1910
|
+
}
|
|
1911
|
+
walk(accessor.body);
|
|
1912
|
+
if (mask === 0 && readsState) {
|
|
1913
|
+
return { mask: 0xffffffff | 0, readsState: true };
|
|
1914
|
+
}
|
|
1915
|
+
return { mask, readsState };
|
|
1916
|
+
}
|
|
1917
|
+
function resolveChain(node, paramName) {
|
|
1918
|
+
const parts = [];
|
|
1919
|
+
let current = node;
|
|
1920
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
1921
|
+
parts.unshift(current.name.text);
|
|
1922
|
+
current = current.expression;
|
|
1923
|
+
}
|
|
1924
|
+
if (!ts.isIdentifier(current) || current.text !== paramName)
|
|
1925
|
+
return null;
|
|
1926
|
+
if (parts.length > 2)
|
|
1927
|
+
return parts.slice(0, 2).join('.');
|
|
1928
|
+
return parts.join('.');
|
|
1929
|
+
}
|
|
1930
|
+
//# sourceMappingURL=transform.js.map
|