@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.
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1822
+ }
1823
+ function escapeAttr(s) {
1824
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
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