@zenithbuild/cli 0.6.2 → 0.6.4

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/dist/build.js CHANGED
@@ -16,13 +16,11 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
16
16
  import { mkdir, readdir, rm, stat } from 'node:fs/promises';
17
17
  import { createRequire } from 'node:module';
18
18
  import { basename, dirname, extname, join, relative, resolve } from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
19
  import { generateManifest } from './manifest.js';
21
20
  import { buildComponentRegistry, expandComponents, extractTemplate, isDocumentMode } from './resolve-components.js';
21
+ import { resolveBundlerBin, resolveCompilerBin } from './toolchain-paths.js';
22
+ import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
22
23
 
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = dirname(__filename);
25
- const CLI_ROOT = resolve(__dirname, '..');
26
24
  const require = createRequire(import.meta.url);
27
25
  let cachedTypeScript = undefined;
28
26
 
@@ -40,40 +38,6 @@ function loadTypeScriptApi() {
40
38
  return cachedTypeScript;
41
39
  }
42
40
 
43
- /**
44
- * Resolve a binary path from deterministic candidates.
45
- *
46
- * Supports both repository layout (../zenith-*) and installed package layout
47
- * under node_modules/@zenithbuild (../compiler, ../bundler).
48
- *
49
- * @param {string[]} candidates
50
- * @returns {string}
51
- */
52
- function resolveBinary(candidates) {
53
- for (const candidate of candidates) {
54
- if (existsSync(candidate)) {
55
- return candidate;
56
- }
57
- }
58
- return candidates[0];
59
- }
60
-
61
- const COMPILER_BIN = resolveBinary([
62
- resolve(CLI_ROOT, '../compiler/target/release/zenith-compiler'),
63
- resolve(CLI_ROOT, '../zenith-compiler/target/release/zenith-compiler')
64
- ]);
65
-
66
- function getBundlerBin() {
67
- const envBin = process.env.ZENITH_BUNDLER_BIN;
68
- if (envBin && typeof envBin === 'string' && existsSync(envBin)) {
69
- return envBin;
70
- }
71
- return resolveBinary([
72
- resolve(CLI_ROOT, '../bundler/target/release/zenith-bundler'),
73
- resolve(CLI_ROOT, '../zenith-bundler/target/release/zenith-bundler')
74
- ]);
75
- }
76
-
77
41
  /**
78
42
  * Build a per-build warning emitter that deduplicates repeated compiler lines.
79
43
  *
@@ -133,9 +97,11 @@ function forwardStreamLines(stream, onLine) {
133
97
  * @param {object} compilerRunOptions
134
98
  * @param {(warning: string) => void} [compilerRunOptions.onWarning]
135
99
  * @param {boolean} [compilerRunOptions.suppressWarnings]
100
+ * @param {string} [compilerRunOptions.compilerBin]
136
101
  * @returns {object}
137
102
  */
138
103
  function runCompiler(filePath, stdinSource, compilerOpts = {}, compilerRunOptions = {}) {
104
+ const compilerBin = compilerRunOptions.compilerBin || resolveCompilerBin();
139
105
  const args = stdinSource !== undefined
140
106
  ? ['--stdin', filePath]
141
107
  : [filePath];
@@ -150,7 +116,7 @@ function runCompiler(filePath, stdinSource, compilerOpts = {}, compilerRunOption
150
116
  opts.input = stdinSource;
151
117
  }
152
118
 
153
- const result = spawnSync(COMPILER_BIN, args, opts);
119
+ const result = spawnSync(compilerBin, args, opts);
154
120
 
155
121
  if (result.error) {
156
122
  throw new Error(`Compiler spawn failed for ${filePath}: ${result.error.message}`);
@@ -200,11 +166,33 @@ function stripStyleBlocks(source) {
200
166
  * @param {string} compPath
201
167
  * @param {string} componentSource
202
168
  * @param {object} compIr
203
- * @returns {{ map: Map<string, string>, ambiguous: Set<string> }}
169
+ * @param {object} compilerOpts
170
+ * @param {string} compilerBin
171
+ * @returns {{
172
+ * map: Map<string, string>,
173
+ * bindings: Map<string, {
174
+ * compiled_expr: string | null,
175
+ * signal_index: number | null,
176
+ * signal_indices: number[],
177
+ * state_index: number | null,
178
+ * component_instance: string | null,
179
+ * component_binding: string | null
180
+ * }>,
181
+ * signals: Array<{ id?: number, kind?: string, state_index?: number }>,
182
+ * stateBindings: Array<{ key?: string, value?: string }>,
183
+ * ambiguous: Set<string>
184
+ * }}
204
185
  */
205
- function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts) {
206
- const out = { map: new Map(), ambiguous: new Set() };
186
+ function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts, compilerBin) {
187
+ const out = {
188
+ map: new Map(),
189
+ bindings: new Map(),
190
+ signals: Array.isArray(compIr?.signals) ? compIr.signals : [],
191
+ stateBindings: Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [],
192
+ ambiguous: new Set()
193
+ };
207
194
  const rewrittenExpressions = Array.isArray(compIr?.expressions) ? compIr.expressions : [];
195
+ const rewrittenBindings = Array.isArray(compIr?.expression_bindings) ? compIr.expression_bindings : [];
208
196
  if (rewrittenExpressions.length === 0) {
209
197
  return out;
210
198
  }
@@ -216,7 +204,10 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
216
204
 
217
205
  let templateIr;
218
206
  try {
219
- templateIr = runCompiler(compPath, templateOnly, compilerOpts, { suppressWarnings: true });
207
+ templateIr = runCompiler(compPath, templateOnly, compilerOpts, {
208
+ suppressWarnings: true,
209
+ compilerBin
210
+ });
220
211
  } catch {
221
212
  return out;
222
213
  }
@@ -229,34 +220,243 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
229
220
  if (typeof raw !== 'string' || typeof rewritten !== 'string') {
230
221
  continue;
231
222
  }
232
- if (raw === rewritten) {
233
- continue;
223
+
224
+ const binding = rewrittenBindings[i];
225
+ const normalizedBinding = binding && typeof binding === 'object'
226
+ ? {
227
+ compiled_expr: typeof binding.compiled_expr === 'string' ? binding.compiled_expr : null,
228
+ signal_index: Number.isInteger(binding.signal_index) ? binding.signal_index : null,
229
+ signal_indices: Array.isArray(binding.signal_indices)
230
+ ? binding.signal_indices.filter((value) => Number.isInteger(value))
231
+ : [],
232
+ state_index: Number.isInteger(binding.state_index) ? binding.state_index : null,
233
+ component_instance: typeof binding.component_instance === 'string' ? binding.component_instance : null,
234
+ component_binding: typeof binding.component_binding === 'string' ? binding.component_binding : null
235
+ }
236
+ : null;
237
+
238
+ if (!out.ambiguous.has(raw) && normalizedBinding) {
239
+ const existingBinding = out.bindings.get(raw);
240
+ if (existingBinding) {
241
+ if (JSON.stringify(existingBinding) !== JSON.stringify(normalizedBinding)) {
242
+ out.bindings.delete(raw);
243
+ out.map.delete(raw);
244
+ out.ambiguous.add(raw);
245
+ continue;
246
+ }
247
+ } else {
248
+ out.bindings.set(raw, normalizedBinding);
249
+ }
234
250
  }
235
- const existing = out.map.get(raw);
236
- if (existing && existing !== rewritten) {
237
- out.map.delete(raw);
238
- out.ambiguous.add(raw);
251
+
252
+ if (raw !== rewritten) {
253
+ const existing = out.map.get(raw);
254
+ if (existing && existing !== rewritten) {
255
+ out.bindings.delete(raw);
256
+ out.map.delete(raw);
257
+ out.ambiguous.add(raw);
258
+ continue;
259
+ }
260
+ if (!out.ambiguous.has(raw)) {
261
+ out.map.set(raw, rewritten);
262
+ }
263
+ }
264
+ }
265
+
266
+ return out;
267
+ }
268
+
269
+ function remapCompiledExpressionSignals(compiledExpr, componentSignals, componentStateBindings, pageSignalIndexByStateKey) {
270
+ if (typeof compiledExpr !== 'string' || compiledExpr.length === 0) {
271
+ return null;
272
+ }
273
+
274
+ return compiledExpr.replace(/signalMap\.get\((\d+)\)/g, (full, rawIndex) => {
275
+ const localIndex = Number.parseInt(rawIndex, 10);
276
+ if (!Number.isInteger(localIndex)) {
277
+ return full;
278
+ }
279
+ const signal = componentSignals[localIndex];
280
+ if (!signal || !Number.isInteger(signal.state_index)) {
281
+ return full;
282
+ }
283
+ const stateKey = componentStateBindings[signal.state_index]?.key;
284
+ if (typeof stateKey !== 'string' || stateKey.length === 0) {
285
+ return full;
286
+ }
287
+ const pageIndex = pageSignalIndexByStateKey.get(stateKey);
288
+ if (!Number.isInteger(pageIndex)) {
289
+ return full;
290
+ }
291
+ return `signalMap.get(${pageIndex})`;
292
+ });
293
+ }
294
+
295
+ function resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding) {
296
+ if (!binding || typeof binding !== 'object') {
297
+ return null;
298
+ }
299
+
300
+ const pageStateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
301
+ const pageSignals = Array.isArray(pageIr?.signals) ? pageIr.signals : [];
302
+ const pageStateIndexByKey = new Map();
303
+ const pageSignalIndexByStateKey = new Map();
304
+
305
+ for (let index = 0; index < pageStateBindings.length; index++) {
306
+ const key = pageStateBindings[index]?.key;
307
+ if (typeof key === 'string' && key.length > 0 && !pageStateIndexByKey.has(key)) {
308
+ pageStateIndexByKey.set(key, index);
309
+ }
310
+ }
311
+
312
+ for (let index = 0; index < pageSignals.length; index++) {
313
+ const stateIndex = pageSignals[index]?.state_index;
314
+ if (!Number.isInteger(stateIndex)) {
239
315
  continue;
240
316
  }
241
- if (!out.ambiguous.has(raw)) {
242
- out.map.set(raw, rewritten);
317
+ const stateKey = pageStateBindings[stateIndex]?.key;
318
+ if (typeof stateKey === 'string' && stateKey.length > 0 && !pageSignalIndexByStateKey.has(stateKey)) {
319
+ pageSignalIndexByStateKey.set(stateKey, index);
320
+ }
321
+ }
322
+
323
+ const componentSignals = Array.isArray(componentRewrite?.signals) ? componentRewrite.signals : [];
324
+ const componentStateBindings = Array.isArray(componentRewrite?.stateBindings) ? componentRewrite.stateBindings : [];
325
+
326
+ let signalIndices = Array.isArray(binding.signal_indices)
327
+ ? [...new Set(
328
+ binding.signal_indices
329
+ .map((signalIndex) => {
330
+ if (!Number.isInteger(signalIndex)) {
331
+ return null;
332
+ }
333
+ const signal = componentSignals[signalIndex];
334
+ if (!signal || !Number.isInteger(signal.state_index)) {
335
+ return null;
336
+ }
337
+ const stateKey = componentStateBindings[signal.state_index]?.key;
338
+ if (typeof stateKey !== 'string' || stateKey.length === 0) {
339
+ return null;
340
+ }
341
+ const pageIndex = pageSignalIndexByStateKey.get(stateKey);
342
+ return Number.isInteger(pageIndex) ? pageIndex : null;
343
+ })
344
+ .filter((value) => Number.isInteger(value))
345
+ )].sort((a, b) => a - b)
346
+ : [];
347
+
348
+ let signalIndex = null;
349
+ if (Number.isInteger(binding.signal_index)) {
350
+ const signal = componentSignals[binding.signal_index];
351
+ const stateKey = signal && Number.isInteger(signal.state_index)
352
+ ? componentStateBindings[signal.state_index]?.key
353
+ : null;
354
+ const pageIndex = typeof stateKey === 'string' ? pageSignalIndexByStateKey.get(stateKey) : null;
355
+ signalIndex = Number.isInteger(pageIndex) ? pageIndex : null;
356
+ }
357
+ if (signalIndex === null && signalIndices.length === 1) {
358
+ signalIndex = signalIndices[0];
359
+ }
360
+
361
+ let stateIndex = null;
362
+ if (Number.isInteger(binding.state_index)) {
363
+ const stateKey = componentStateBindings[binding.state_index]?.key;
364
+ const pageIndex = typeof stateKey === 'string' ? pageStateIndexByKey.get(stateKey) : null;
365
+ stateIndex = Number.isInteger(pageIndex) ? pageIndex : null;
366
+ }
367
+
368
+ if (Number.isInteger(stateIndex)) {
369
+ const fallbackSignalIndices = pageSignals
370
+ .map((signal, index) => signal?.state_index === stateIndex ? index : null)
371
+ .filter((value) => Number.isInteger(value));
372
+ const signalIndicesMatchState = signalIndices.every(
373
+ (index) => pageSignals[index]?.state_index === stateIndex
374
+ );
375
+ if ((!signalIndicesMatchState || signalIndices.length === 0) && fallbackSignalIndices.length > 0) {
376
+ signalIndices = fallbackSignalIndices;
377
+ }
378
+ if (
379
+ (signalIndex === null || pageSignals[signalIndex]?.state_index !== stateIndex) &&
380
+ fallbackSignalIndices.length === 1
381
+ ) {
382
+ signalIndex = fallbackSignalIndices[0];
243
383
  }
244
384
  }
245
385
 
246
- return out;
386
+ let compiledExpr = remapCompiledExpressionSignals(
387
+ binding.compiled_expr,
388
+ componentSignals,
389
+ componentStateBindings,
390
+ pageSignalIndexByStateKey
391
+ );
392
+ if (
393
+ typeof compiledExpr === 'string' &&
394
+ signalIndices.length === 1 &&
395
+ Array.isArray(binding.signal_indices) &&
396
+ binding.signal_indices.length <= 1
397
+ ) {
398
+ compiledExpr = compiledExpr.replace(/signalMap\.get\(\d+\)/g, `signalMap.get(${signalIndices[0]})`);
399
+ }
400
+
401
+ return {
402
+ compiled_expr: compiledExpr,
403
+ signal_index: signalIndex,
404
+ signal_indices: signalIndices,
405
+ state_index: stateIndex,
406
+ component_instance: typeof binding.component_instance === 'string' ? binding.component_instance : null,
407
+ component_binding: typeof binding.component_binding === 'string' ? binding.component_binding : null
408
+ };
247
409
  }
248
410
 
249
411
  /**
250
412
  * Merge a per-component rewrite table into the page-level rewrite table.
251
413
  *
252
414
  * @param {Map<string, string>} pageMap
415
+ * @param {Map<string, {
416
+ * compiled_expr: string | null,
417
+ * signal_index: number | null,
418
+ * signal_indices: number[],
419
+ * state_index: number | null,
420
+ * component_instance: string | null,
421
+ * component_binding: string | null
422
+ * }>} pageBindingMap
253
423
  * @param {Set<string>} pageAmbiguous
254
- * @param {{ map: Map<string, string>, ambiguous: Set<string> }} componentRewrite
424
+ * @param {{
425
+ * map: Map<string, string>,
426
+ * bindings: Map<string, {
427
+ * compiled_expr: string | null,
428
+ * signal_index: number | null,
429
+ * signal_indices: number[],
430
+ * state_index: number | null,
431
+ * component_instance: string | null,
432
+ * component_binding: string | null
433
+ * }>,
434
+ * signals: Array<{ id?: number, kind?: string, state_index?: number }>,
435
+ * stateBindings: Array<{ key?: string, value?: string }>,
436
+ * ambiguous: Set<string>
437
+ * }} componentRewrite
438
+ * @param {object} pageIr
255
439
  */
256
- function mergeExpressionRewriteMaps(pageMap, pageAmbiguous, componentRewrite) {
440
+ function mergeExpressionRewriteMaps(pageMap, pageBindingMap, pageAmbiguous, componentRewrite, pageIr) {
257
441
  for (const raw of componentRewrite.ambiguous) {
258
442
  pageAmbiguous.add(raw);
259
443
  pageMap.delete(raw);
444
+ pageBindingMap.delete(raw);
445
+ }
446
+
447
+ for (const [raw, binding] of componentRewrite.bindings.entries()) {
448
+ if (pageAmbiguous.has(raw)) {
449
+ continue;
450
+ }
451
+ const resolved = resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding);
452
+ const existing = pageBindingMap.get(raw);
453
+ if (existing && JSON.stringify(existing) !== JSON.stringify(resolved)) {
454
+ pageAmbiguous.add(raw);
455
+ pageMap.delete(raw);
456
+ pageBindingMap.delete(raw);
457
+ continue;
458
+ }
459
+ pageBindingMap.set(raw, resolved);
260
460
  }
261
461
 
262
462
  for (const [raw, rewritten] of componentRewrite.map.entries()) {
@@ -267,6 +467,7 @@ function mergeExpressionRewriteMaps(pageMap, pageAmbiguous, componentRewrite) {
267
467
  if (existing && existing !== rewritten) {
268
468
  pageAmbiguous.add(raw);
269
469
  pageMap.delete(raw);
470
+ pageBindingMap.delete(raw);
270
471
  continue;
271
472
  }
272
473
  pageMap.set(raw, rewritten);
@@ -329,9 +530,17 @@ function rewriteRefBindingIdentifiers(pageIr, preferredKeys = null) {
329
530
  *
330
531
  * @param {object} pageIr
331
532
  * @param {Map<string, string>} expressionMap
533
+ * @param {Map<string, {
534
+ * compiled_expr: string | null,
535
+ * signal_index: number | null,
536
+ * signal_indices: number[],
537
+ * state_index: number | null,
538
+ * component_instance: string | null,
539
+ * component_binding: string | null
540
+ * }>} bindingMap
332
541
  * @param {Set<string>} ambiguous
333
542
  */
334
- function applyExpressionRewrites(pageIr, expressionMap, ambiguous) {
543
+ function applyExpressionRewrites(pageIr, expressionMap, bindingMap, ambiguous) {
335
544
  if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
336
545
  return;
337
546
  }
@@ -345,24 +554,213 @@ function applyExpressionRewrites(pageIr, expressionMap, ambiguous) {
345
554
  if (ambiguous.has(current)) {
346
555
  continue;
347
556
  }
557
+
348
558
  const rewritten = expressionMap.get(current);
349
- if (!rewritten || rewritten === current) {
559
+ const rewrittenBinding = bindingMap.get(current);
560
+ if (rewritten && rewritten !== current) {
561
+ pageIr.expressions[index] = rewritten;
562
+ }
563
+
564
+ if (!bindings[index] || typeof bindings[index] !== 'object') {
350
565
  continue;
351
566
  }
352
- pageIr.expressions[index] = rewritten;
567
+
568
+ if (rewritten && rewritten !== current && bindings[index].literal === current) {
569
+ bindings[index].literal = rewritten;
570
+ }
571
+
572
+ if (rewrittenBinding) {
573
+ bindings[index].compiled_expr = rewrittenBinding.compiled_expr;
574
+ bindings[index].signal_index = rewrittenBinding.signal_index;
575
+ bindings[index].signal_indices = rewrittenBinding.signal_indices;
576
+ bindings[index].state_index = rewrittenBinding.state_index;
577
+ bindings[index].component_instance = rewrittenBinding.component_instance;
578
+ bindings[index].component_binding = rewrittenBinding.component_binding;
579
+ } else if (rewritten && rewritten !== current && bindings[index].compiled_expr === current) {
580
+ bindings[index].compiled_expr = rewritten;
581
+ }
582
+
353
583
  if (
354
- bindings[index] &&
355
- typeof bindings[index] === 'object' &&
356
- bindings[index].literal === current
584
+ !rewrittenBinding &&
585
+ (!rewritten || rewritten === current) &&
586
+ bindings[index].literal === current &&
587
+ bindings[index].compiled_expr === current
357
588
  ) {
358
- bindings[index].literal = rewritten;
359
- if (bindings[index].compiled_expr === current) {
360
- bindings[index].compiled_expr = rewritten;
589
+ bindings[index].compiled_expr = current;
590
+ }
591
+ }
592
+ }
593
+
594
+ function applyScopedIdentifierRewrites(pageIr, scopeRewrite) {
595
+ if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
596
+ return;
597
+ }
598
+ const bindings = Array.isArray(pageIr.expression_bindings) ? pageIr.expression_bindings : [];
599
+ const rewriteContext = {
600
+ scopeRewrite
601
+ };
602
+
603
+ for (let index = 0; index < pageIr.expressions.length; index++) {
604
+ const current = pageIr.expressions[index];
605
+ if (typeof current === 'string') {
606
+ pageIr.expressions[index] = rewritePropsExpression(current, rewriteContext);
607
+ }
608
+
609
+ if (!bindings[index] || typeof bindings[index] !== 'object') {
610
+ continue;
611
+ }
612
+
613
+ if (typeof bindings[index].literal === 'string') {
614
+ bindings[index].literal = rewritePropsExpression(bindings[index].literal, rewriteContext);
615
+ }
616
+ if (typeof bindings[index].compiled_expr === 'string') {
617
+ bindings[index].compiled_expr = rewritePropsExpression(bindings[index].compiled_expr, rewriteContext);
618
+ }
619
+ }
620
+ }
621
+
622
+ function synthesizeSignalBackedCompiledExpressions(pageIr) {
623
+ if (!Array.isArray(pageIr?.expression_bindings) || pageIr.expression_bindings.length === 0) {
624
+ return;
625
+ }
626
+
627
+ const stateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
628
+ const signals = Array.isArray(pageIr?.signals) ? pageIr.signals : [];
629
+ if (stateBindings.length === 0 || signals.length === 0) {
630
+ return;
631
+ }
632
+
633
+ const signalIndexByStateKey = new Map();
634
+ for (let index = 0; index < signals.length; index++) {
635
+ const stateIndex = signals[index]?.state_index;
636
+ const stateKey = Number.isInteger(stateIndex) ? stateBindings[stateIndex]?.key : null;
637
+ if (typeof stateKey === 'string' && stateKey.length > 0) {
638
+ signalIndexByStateKey.set(stateKey, index);
639
+ }
640
+ }
641
+ if (signalIndexByStateKey.size === 0) {
642
+ return;
643
+ }
644
+
645
+ for (let index = 0; index < pageIr.expression_bindings.length; index++) {
646
+ const binding = pageIr.expression_bindings[index];
647
+ if (!binding || typeof binding !== 'object') {
648
+ continue;
649
+ }
650
+ if (typeof binding.compiled_expr === 'string' && binding.compiled_expr.includes('signalMap.get(')) {
651
+ continue;
652
+ }
653
+
654
+ const candidate = typeof binding.literal === 'string' && binding.literal.trim().length > 0
655
+ ? binding.literal
656
+ : typeof pageIr.expressions?.[index] === 'string'
657
+ ? pageIr.expressions[index]
658
+ : null;
659
+ if (typeof candidate !== 'string' || candidate.trim().length === 0) {
660
+ continue;
661
+ }
662
+
663
+ let rewritten = candidate;
664
+ const signalIndices = [];
665
+ for (const [stateKey, signalIndex] of signalIndexByStateKey.entries()) {
666
+ if (!rewritten.includes(stateKey)) {
667
+ continue;
668
+ }
669
+ const escaped = stateKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
670
+ const pattern = new RegExp(`\\b${escaped}\\b`, 'g');
671
+ if (!pattern.test(rewritten)) {
672
+ continue;
673
+ }
674
+ rewritten = rewritten.replace(pattern, `signalMap.get(${signalIndex}).get()`);
675
+ signalIndices.push(signalIndex);
676
+ }
677
+
678
+ if (rewritten === candidate || signalIndices.length === 0) {
679
+ continue;
680
+ }
681
+
682
+ const uniqueSignalIndices = [...new Set(signalIndices)].sort((a, b) => a - b);
683
+ binding.compiled_expr = rewritten;
684
+ binding.signal_indices = uniqueSignalIndices;
685
+ if (uniqueSignalIndices.length === 1) {
686
+ binding.signal_index = uniqueSignalIndices[0];
687
+ const stateIndex = signals[uniqueSignalIndices[0]]?.state_index;
688
+ if (Number.isInteger(stateIndex)) {
689
+ binding.state_index = stateIndex;
361
690
  }
362
691
  }
363
692
  }
364
693
  }
365
694
 
695
+ function normalizeExpressionBindingDependencies(pageIr) {
696
+ if (!Array.isArray(pageIr?.expression_bindings) || pageIr.expression_bindings.length === 0) {
697
+ return;
698
+ }
699
+
700
+ const signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
701
+ const dependencyRe = /signalMap\.get\((\d+)\)/g;
702
+
703
+ for (const binding of pageIr.expression_bindings) {
704
+ if (!binding || typeof binding !== 'object' || typeof binding.compiled_expr !== 'string') {
705
+ continue;
706
+ }
707
+
708
+ const indices = [];
709
+ dependencyRe.lastIndex = 0;
710
+ let match;
711
+ while ((match = dependencyRe.exec(binding.compiled_expr)) !== null) {
712
+ const index = Number.parseInt(match[1], 10);
713
+ if (Number.isInteger(index)) {
714
+ indices.push(index);
715
+ }
716
+ }
717
+
718
+ if (indices.length === 0) {
719
+ continue;
720
+ }
721
+
722
+ let signalIndices = [...new Set(indices)].sort((a, b) => a - b);
723
+ if (Number.isInteger(binding.state_index)) {
724
+ const owningSignalIndices = signals
725
+ .map((signal, index) => signal?.state_index === binding.state_index ? index : null)
726
+ .filter((value) => Number.isInteger(value));
727
+ const extractedMatchState =
728
+ signalIndices.length > 0 &&
729
+ signalIndices.every((index) => signals[index]?.state_index === binding.state_index);
730
+ if (owningSignalIndices.length > 0 && !extractedMatchState) {
731
+ signalIndices = owningSignalIndices;
732
+ }
733
+ }
734
+
735
+ if (
736
+ !Array.isArray(binding.signal_indices) ||
737
+ binding.signal_indices.length === 0 ||
738
+ binding.signal_indices.some((index) => signals[index]?.state_index !== binding.state_index)
739
+ ) {
740
+ binding.signal_indices = signalIndices;
741
+ }
742
+ if (
743
+ (!Number.isInteger(binding.signal_index) ||
744
+ signals[binding.signal_index]?.state_index !== binding.state_index) &&
745
+ signalIndices.length === 1
746
+ ) {
747
+ binding.signal_index = signalIndices[0];
748
+ }
749
+ if (!Number.isInteger(binding.state_index) && Number.isInteger(binding.signal_index)) {
750
+ const stateIndex = signals[binding.signal_index]?.state_index;
751
+ if (Number.isInteger(stateIndex)) {
752
+ binding.state_index = stateIndex;
753
+ }
754
+ }
755
+ if (signalIndices.length === 1) {
756
+ binding.compiled_expr = binding.compiled_expr.replace(
757
+ /signalMap\.get\(\d+\)/g,
758
+ `signalMap.get(${signalIndices[0]})`
759
+ );
760
+ }
761
+ }
762
+ }
763
+
366
764
  /**
367
765
  * Rewrite legacy markup-literal identifiers in expression literals to the
368
766
  * internal `__ZENITH_INTERNAL_ZENHTML` binding used by the runtime.
@@ -798,9 +1196,10 @@ const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
798
1196
  *
799
1197
  * @param {string} source
800
1198
  * @param {Map<string, string>} registry
801
- * @returns {Map<string, string[]>}
1199
+ * @param {string | null} ownerPath
1200
+ * @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
802
1201
  */
803
- function collectComponentUsageAttrs(source, registry) {
1202
+ function collectComponentUsageAttrs(source, registry, ownerPath = null) {
804
1203
  const out = new Map();
805
1204
  OPEN_COMPONENT_TAG_RE.lastIndex = 0;
806
1205
  let match;
@@ -813,11 +1212,47 @@ function collectComponentUsageAttrs(source, registry) {
813
1212
  if (!out.has(name)) {
814
1213
  out.set(name, []);
815
1214
  }
816
- out.get(name).push(attrs);
1215
+ out.get(name).push({ attrs, ownerPath });
817
1216
  }
818
1217
  return out;
819
1218
  }
820
1219
 
1220
+ /**
1221
+ * Collect component usage attrs recursively so nested component callsites
1222
+ * receive deterministic props preludes during page-hoist merging.
1223
+ *
1224
+ * Current Zenith architecture still resolves one attrs set per component type.
1225
+ * This helper preserves that model while ensuring nested usages are not lost.
1226
+ *
1227
+ * @param {string} source
1228
+ * @param {Map<string, string>} registry
1229
+ * @param {string | null} ownerPath
1230
+ * @param {Set<string>} visitedFiles
1231
+ * @param {Map<string, Array<{ attrs: string, ownerPath: string | null }>>} out
1232
+ * @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
1233
+ */
1234
+ function collectRecursiveComponentUsageAttrs(source, registry, ownerPath = null, visitedFiles = new Set(), out = new Map()) {
1235
+ const local = collectComponentUsageAttrs(source, registry, ownerPath);
1236
+ for (const [name, attrsList] of local.entries()) {
1237
+ if (!out.has(name)) {
1238
+ out.set(name, []);
1239
+ }
1240
+ out.get(name).push(...attrsList);
1241
+ }
1242
+
1243
+ for (const name of local.keys()) {
1244
+ const compPath = registry.get(name);
1245
+ if (!compPath || visitedFiles.has(compPath)) {
1246
+ continue;
1247
+ }
1248
+ visitedFiles.add(compPath);
1249
+ const componentSource = readFileSync(compPath, 'utf8');
1250
+ collectRecursiveComponentUsageAttrs(componentSource, registry, compPath, visitedFiles, out);
1251
+ }
1252
+
1253
+ return out;
1254
+ }
1255
+
821
1256
  /**
822
1257
  * Merge a component's IR into the page IR.
823
1258
  *
@@ -828,7 +1263,16 @@ function collectComponentUsageAttrs(source, registry) {
828
1263
  * @param {object} compIr — the component's compiled IR
829
1264
  * @param {string} compPath — component file path
830
1265
  * @param {string} pageFile — page file path
831
- * @param {{ includeCode: boolean, cssImportsOnly: boolean, documentMode?: boolean, componentAttrs?: string }} options
1266
+ * @param {{
1267
+ * includeCode: boolean,
1268
+ * cssImportsOnly: boolean,
1269
+ * documentMode?: boolean,
1270
+ * componentAttrs?: string,
1271
+ * componentAttrsRewrite?: {
1272
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1273
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1274
+ * } | null
1275
+ * }} options
832
1276
  * @param {Set<string>} seenStaticImports
833
1277
  */
834
1278
  function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStaticImports, knownRefKeys = null) {
@@ -920,6 +1364,41 @@ function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStati
920
1364
  }
921
1365
  }
922
1366
 
1367
+ if (options.includeCode && Array.isArray(compIr.signals)) {
1368
+ pageIr.signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
1369
+ const existingSignalStateKeys = new Set(
1370
+ pageIr.signals
1371
+ .map((signal) => {
1372
+ const stateIndex = signal?.state_index;
1373
+ return Number.isInteger(stateIndex) ? pageIr.hoisted.state?.[stateIndex]?.key : null;
1374
+ })
1375
+ .filter(Boolean)
1376
+ );
1377
+
1378
+ for (const signal of compIr.signals) {
1379
+ if (!signal || !Number.isInteger(signal.state_index)) {
1380
+ continue;
1381
+ }
1382
+ const stateKey = compIr.hoisted?.state?.[signal.state_index]?.key;
1383
+ if (typeof stateKey !== 'string' || stateKey.length === 0) {
1384
+ continue;
1385
+ }
1386
+ const pageStateIndex = pageIr.hoisted.state.findIndex((entry) => entry?.key === stateKey);
1387
+ if (!Number.isInteger(pageStateIndex) || pageStateIndex < 0) {
1388
+ continue;
1389
+ }
1390
+ if (existingSignalStateKeys.has(stateKey)) {
1391
+ continue;
1392
+ }
1393
+ existingSignalStateKeys.add(stateKey);
1394
+ pageIr.signals.push({
1395
+ id: pageIr.signals.length,
1396
+ kind: typeof signal.kind === 'string' && signal.kind.length > 0 ? signal.kind : 'signal',
1397
+ state_index: pageStateIndex
1398
+ });
1399
+ }
1400
+ }
1401
+
923
1402
  // Merge hoisted code blocks (rebased to the page file path)
924
1403
  if (options.includeCode && compIr.hoisted?.code?.length) {
925
1404
  for (const block of compIr.hoisted.code) {
@@ -927,7 +1406,11 @@ function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStati
927
1406
  const filteredImports = options.cssImportsOnly
928
1407
  ? stripNonCssStaticImportsInSource(rebased)
929
1408
  : rebased;
930
- const withPropsPrelude = injectPropsPrelude(filteredImports, options.componentAttrs || '');
1409
+ const withPropsPrelude = injectPropsPrelude(
1410
+ filteredImports,
1411
+ options.componentAttrs || '',
1412
+ options.componentAttrsRewrite || null
1413
+ );
931
1414
  const transpiled = transpileTypeScriptToJs(withPropsPrelude, compPath);
932
1415
  const deduped = dedupeStaticImportsInSource(transpiled, seenStaticImports);
933
1416
  const deferred = deferComponentRuntimeBlock(deduped);
@@ -1228,11 +1711,304 @@ function renderObjectKey(key) {
1228
1711
  return JSON.stringify(key);
1229
1712
  }
1230
1713
 
1714
+ /**
1715
+ * @param {string} value
1716
+ * @returns {string | null}
1717
+ */
1718
+ function deriveScopedIdentifierAlias(value) {
1719
+ const ident = String(value || '').trim();
1720
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ident)) {
1721
+ return null;
1722
+ }
1723
+ const parts = ident.split('_').filter(Boolean);
1724
+ const candidate = parts.length > 1 ? parts[parts.length - 1] : ident;
1725
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate) ? candidate : ident;
1726
+ }
1727
+
1728
+ /**
1729
+ * @param {string} source
1730
+ * @returns {string[]}
1731
+ */
1732
+ function extractDeclaredIdentifiers(source) {
1733
+ const text = String(source || '').trim();
1734
+ if (!text) {
1735
+ return [];
1736
+ }
1737
+
1738
+ const ts = loadTypeScriptApi();
1739
+ if (ts) {
1740
+ const sourceFile = ts.createSourceFile('zenith-hoisted-declaration.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1741
+ const identifiers = [];
1742
+ const collectBindingNames = (name) => {
1743
+ if (ts.isIdentifier(name)) {
1744
+ identifiers.push(name.text);
1745
+ return;
1746
+ }
1747
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
1748
+ for (const element of name.elements) {
1749
+ if (ts.isBindingElement(element)) {
1750
+ collectBindingNames(element.name);
1751
+ }
1752
+ }
1753
+ }
1754
+ };
1755
+
1756
+ for (const statement of sourceFile.statements) {
1757
+ if (!ts.isVariableStatement(statement)) {
1758
+ continue;
1759
+ }
1760
+ for (const declaration of statement.declarationList.declarations) {
1761
+ collectBindingNames(declaration.name);
1762
+ }
1763
+ }
1764
+
1765
+ if (identifiers.length > 0) {
1766
+ return identifiers;
1767
+ }
1768
+ }
1769
+
1770
+ const fallback = [];
1771
+ const match = text.match(/^\s*(?:const|let|var)\s+([\s\S]+?);?\s*$/);
1772
+ if (!match) {
1773
+ return fallback;
1774
+ }
1775
+ const declarationList = match[1];
1776
+ const identifierRe = /(?:^|,)\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=,]+)?=/g;
1777
+ let found;
1778
+ while ((found = identifierRe.exec(declarationList)) !== null) {
1779
+ fallback.push(found[1]);
1780
+ }
1781
+ return fallback;
1782
+ }
1783
+
1784
+ /**
1785
+ * @param {Map<string, string>} map
1786
+ * @param {Set<string>} ambiguous
1787
+ * @param {string | null} raw
1788
+ * @param {string | null} rewritten
1789
+ */
1790
+ function recordScopedIdentifierRewrite(map, ambiguous, raw, rewritten) {
1791
+ if (typeof raw !== 'string' || raw.length === 0 || typeof rewritten !== 'string' || rewritten.length === 0) {
1792
+ return;
1793
+ }
1794
+ const existing = map.get(raw);
1795
+ if (existing && existing !== rewritten) {
1796
+ map.delete(raw);
1797
+ ambiguous.add(raw);
1798
+ return;
1799
+ }
1800
+ if (!ambiguous.has(raw)) {
1801
+ map.set(raw, rewritten);
1802
+ }
1803
+ }
1804
+
1805
+ /**
1806
+ * @param {object | null | undefined} ir
1807
+ * @returns {{ map: Map<string, string>, ambiguous: Set<string> }}
1808
+ */
1809
+ function buildScopedIdentifierRewrite(ir) {
1810
+ const out = { map: new Map(), ambiguous: new Set() };
1811
+ if (!ir || typeof ir !== 'object') {
1812
+ return out;
1813
+ }
1814
+
1815
+ const stateBindings = Array.isArray(ir?.hoisted?.state) ? ir.hoisted.state : [];
1816
+ for (const stateEntry of stateBindings) {
1817
+ const key = typeof stateEntry?.key === 'string' ? stateEntry.key : null;
1818
+ recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(key), key);
1819
+ }
1820
+
1821
+ const functionBindings = Array.isArray(ir?.hoisted?.functions) ? ir.hoisted.functions : [];
1822
+ for (const fnName of functionBindings) {
1823
+ if (typeof fnName !== 'string') {
1824
+ continue;
1825
+ }
1826
+ recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(fnName), fnName);
1827
+ }
1828
+
1829
+ const declarations = Array.isArray(ir?.hoisted?.declarations) ? ir.hoisted.declarations : [];
1830
+ for (const declaration of declarations) {
1831
+ if (typeof declaration !== 'string') {
1832
+ continue;
1833
+ }
1834
+ for (const identifier of extractDeclaredIdentifiers(declaration)) {
1835
+ recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(identifier), identifier);
1836
+ }
1837
+ }
1838
+
1839
+ return out;
1840
+ }
1841
+
1842
+ function rewriteIdentifiersWithinExpression(expr, scopeMap, scopeAmbiguous) {
1843
+ const ts = loadTypeScriptApi();
1844
+ if (!(scopeMap instanceof Map) || !ts) {
1845
+ return expr;
1846
+ }
1847
+
1848
+ const wrapped = `const __zenith_expr__ = (${expr});`;
1849
+ let sourceFile;
1850
+ try {
1851
+ sourceFile = ts.createSourceFile('zenith-expression.ts', wrapped, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
1852
+ } catch {
1853
+ return expr;
1854
+ }
1855
+
1856
+ const statement = sourceFile.statements[0];
1857
+ if (!statement || !ts.isVariableStatement(statement)) {
1858
+ return expr;
1859
+ }
1860
+ const initializer = statement.declarationList.declarations[0]?.initializer;
1861
+ const root = initializer && ts.isParenthesizedExpression(initializer) ? initializer.expression : initializer;
1862
+ if (!root) {
1863
+ return expr;
1864
+ }
1865
+
1866
+ const replacements = [];
1867
+ const collectBoundNames = (name, target) => {
1868
+ if (ts.isIdentifier(name)) {
1869
+ target.add(name.text);
1870
+ return;
1871
+ }
1872
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
1873
+ for (const element of name.elements) {
1874
+ if (ts.isBindingElement(element)) {
1875
+ collectBoundNames(element.name, target);
1876
+ }
1877
+ }
1878
+ }
1879
+ };
1880
+ const shouldSkipIdentifier = (node, localBindings) => {
1881
+ if (localBindings.has(node.text)) {
1882
+ return true;
1883
+ }
1884
+ const parent = node.parent;
1885
+ if (!parent) {
1886
+ return false;
1887
+ }
1888
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
1889
+ return true;
1890
+ }
1891
+ if (ts.isPropertyAssignment(parent) && parent.name === node) {
1892
+ return true;
1893
+ }
1894
+ if (ts.isShorthandPropertyAssignment(parent)) {
1895
+ return true;
1896
+ }
1897
+ if (ts.isBindingElement(parent) && parent.name === node) {
1898
+ return true;
1899
+ }
1900
+ if (ts.isParameter(parent) && parent.name === node) {
1901
+ return true;
1902
+ }
1903
+ return false;
1904
+ };
1905
+ const visit = (node, localBindings) => {
1906
+ let nextBindings = localBindings;
1907
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1908
+ nextBindings = new Set(localBindings);
1909
+ if (node.name && ts.isIdentifier(node.name)) {
1910
+ nextBindings.add(node.name.text);
1911
+ }
1912
+ for (const param of node.parameters) {
1913
+ collectBoundNames(param.name, nextBindings);
1914
+ }
1915
+ }
1916
+
1917
+ if (ts.isIdentifier(node) && !shouldSkipIdentifier(node, nextBindings)) {
1918
+ const rewritten = scopeMap.get(node.text);
1919
+ if (
1920
+ typeof rewritten === 'string' &&
1921
+ rewritten.length > 0 &&
1922
+ rewritten !== node.text &&
1923
+ !(scopeAmbiguous instanceof Set && scopeAmbiguous.has(node.text))
1924
+ ) {
1925
+ replacements.push({
1926
+ start: node.getStart(sourceFile),
1927
+ end: node.getEnd(),
1928
+ text: rewritten
1929
+ });
1930
+ }
1931
+ }
1932
+
1933
+ ts.forEachChild(node, (child) => visit(child, nextBindings));
1934
+ };
1935
+
1936
+ visit(root, new Set());
1937
+ if (replacements.length === 0) {
1938
+ return expr;
1939
+ }
1940
+
1941
+ let rewritten = wrapped;
1942
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
1943
+ rewritten = `${rewritten.slice(0, replacement.start)}${replacement.text}${rewritten.slice(replacement.end)}`;
1944
+ }
1945
+
1946
+ const prefix = 'const __zenith_expr__ = (';
1947
+ const suffix = ');';
1948
+ if (!rewritten.startsWith(prefix) || !rewritten.endsWith(suffix)) {
1949
+ return expr;
1950
+ }
1951
+ return rewritten.slice(prefix.length, rewritten.length - suffix.length);
1952
+ }
1953
+
1954
+ /**
1955
+ * @param {string} expr
1956
+ * @param {{
1957
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1958
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1959
+ * } | null} rewriteContext
1960
+ * @returns {string}
1961
+ */
1962
+ function rewritePropsExpression(expr, rewriteContext = null) {
1963
+ const trimmed = String(expr || '').trim();
1964
+ if (!trimmed) {
1965
+ return trimmed;
1966
+ }
1967
+
1968
+ const expressionMap = rewriteContext?.expressionRewrite?.map;
1969
+ const expressionAmbiguous = rewriteContext?.expressionRewrite?.ambiguous;
1970
+ if (
1971
+ expressionMap instanceof Map &&
1972
+ !(expressionAmbiguous instanceof Set && expressionAmbiguous.has(trimmed))
1973
+ ) {
1974
+ const exact = expressionMap.get(trimmed);
1975
+ if (typeof exact === 'string' && exact.length > 0) {
1976
+ return exact;
1977
+ }
1978
+ }
1979
+
1980
+ const scopeMap = rewriteContext?.scopeRewrite?.map;
1981
+ const scopeAmbiguous = rewriteContext?.scopeRewrite?.ambiguous;
1982
+ const rootMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)([\s\S]*)$/);
1983
+ if (!(scopeMap instanceof Map)) {
1984
+ return trimmed;
1985
+ }
1986
+ if (!rootMatch) {
1987
+ return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1988
+ }
1989
+
1990
+ const root = rootMatch[1];
1991
+ if (scopeAmbiguous instanceof Set && scopeAmbiguous.has(root)) {
1992
+ return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1993
+ }
1994
+
1995
+ const rewrittenRoot = scopeMap.get(root);
1996
+ if (typeof rewrittenRoot !== 'string' || rewrittenRoot.length === 0 || rewrittenRoot === root) {
1997
+ return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1998
+ }
1999
+
2000
+ return `${rewrittenRoot}${rootMatch[2]}`;
2001
+ }
2002
+
1231
2003
  /**
1232
2004
  * @param {string} attrs
2005
+ * @param {{
2006
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
2007
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
2008
+ * } | null} rewriteContext
1233
2009
  * @returns {string}
1234
2010
  */
1235
- function renderPropsLiteralFromAttrs(attrs) {
2011
+ function renderPropsLiteralFromAttrs(attrs, rewriteContext = null) {
1236
2012
  const src = String(attrs || '').trim();
1237
2013
  if (!src) {
1238
2014
  return '{}';
@@ -1257,7 +2033,7 @@ function renderPropsLiteralFromAttrs(attrs) {
1257
2033
  valueCode = JSON.stringify(singleQuoted);
1258
2034
  } else if (expressionValue !== undefined) {
1259
2035
  const trimmed = String(expressionValue).trim();
1260
- valueCode = trimmed.length > 0 ? trimmed : 'undefined';
2036
+ valueCode = trimmed.length > 0 ? rewritePropsExpression(trimmed, rewriteContext) : 'undefined';
1261
2037
  }
1262
2038
 
1263
2039
  entries.push(`${renderObjectKey(rawName)}: ${valueCode}`);
@@ -1273,9 +2049,13 @@ function renderPropsLiteralFromAttrs(attrs) {
1273
2049
  /**
1274
2050
  * @param {string} source
1275
2051
  * @param {string} attrs
2052
+ * @param {{
2053
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
2054
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
2055
+ * } | null} rewriteContext
1276
2056
  * @returns {string}
1277
2057
  */
1278
- function injectPropsPrelude(source, attrs) {
2058
+ function injectPropsPrelude(source, attrs, rewriteContext = null) {
1279
2059
  if (typeof source !== 'string' || source.trim().length === 0) {
1280
2060
  return source;
1281
2061
  }
@@ -1286,7 +2066,7 @@ function injectPropsPrelude(source, attrs) {
1286
2066
  return source;
1287
2067
  }
1288
2068
 
1289
- const propsLiteral = renderPropsLiteralFromAttrs(attrs);
2069
+ const propsLiteral = renderPropsLiteralFromAttrs(attrs, rewriteContext);
1290
2070
  return `var props = ${propsLiteral};\n${source}`;
1291
2071
  }
1292
2072
 
@@ -1347,13 +2127,21 @@ function deferComponentRuntimeBlock(source) {
1347
2127
  * @param {string} projectRoot
1348
2128
  * @param {object | null} [logger]
1349
2129
  * @param {boolean} [showInfo]
2130
+ * @param {string} [bundlerBin]
1350
2131
  * @returns {Promise<void>}
1351
2132
  */
1352
- function runBundler(envelope, outDir, projectRoot, logger = null, showInfo = true) {
2133
+ function runBundler(
2134
+ envelope,
2135
+ outDir,
2136
+ projectRoot,
2137
+ logger = null,
2138
+ showInfo = true,
2139
+ bundlerBin = resolveBundlerBin(projectRoot)
2140
+ ) {
1353
2141
  return new Promise((resolvePromise, rejectPromise) => {
1354
2142
  const useStructuredLogger = Boolean(logger && typeof logger.childLine === 'function');
1355
2143
  const child = spawn(
1356
- getBundlerBin(),
2144
+ bundlerBin,
1357
2145
  ['--out-dir', outDir],
1358
2146
  {
1359
2147
  cwd: projectRoot,
@@ -1441,6 +2229,8 @@ async function collectAssets(rootDir) {
1441
2229
  export async function build(options) {
1442
2230
  const { pagesDir, outDir, config = {}, logger = null, showBundlerInfo = true } = options;
1443
2231
  const projectRoot = deriveProjectRootFromPagesDir(pagesDir);
2232
+ const compilerBin = resolveCompilerBin(projectRoot);
2233
+ const bundlerBin = resolveBundlerBin(projectRoot);
1444
2234
  const softNavigationEnabled = config.softNavigation === true || config.router === true;
1445
2235
  const compilerOpts = {
1446
2236
  typescriptDefault: config.typescriptDefault === true,
@@ -1451,6 +2241,15 @@ export async function build(options) {
1451
2241
  await rm(outDir, { recursive: true, force: true });
1452
2242
  await mkdir(outDir, { recursive: true });
1453
2243
 
2244
+ if (logger) {
2245
+ await maybeWarnAboutZenithVersionMismatch({
2246
+ projectRoot,
2247
+ logger,
2248
+ command: 'build',
2249
+ bundlerBinPath: bundlerBin
2250
+ });
2251
+ }
2252
+
1454
2253
  // Derive src/ directory from pages/ directory
1455
2254
  const srcDir = resolve(pagesDir, '..');
1456
2255
 
@@ -1488,7 +2287,7 @@ export async function build(options) {
1488
2287
  for (const entry of manifest) {
1489
2288
  const sourceFile = join(pagesDir, entry.file);
1490
2289
  const rawSource = readFileSync(sourceFile, 'utf8');
1491
- const componentUsageAttrs = collectComponentUsageAttrs(rawSource, registry);
2290
+ const componentUsageAttrs = collectRecursiveComponentUsageAttrs(rawSource, registry, sourceFile);
1492
2291
 
1493
2292
  const baseName = sourceFile.slice(0, -extname(sourceFile).length);
1494
2293
  let adjacentGuard = null;
@@ -1510,7 +2309,10 @@ export async function build(options) {
1510
2309
  sourceFile,
1511
2310
  compileSource,
1512
2311
  compilerOpts,
1513
- { onWarning: emitCompilerWarning }
2312
+ {
2313
+ compilerBin,
2314
+ onWarning: emitCompilerWarning
2315
+ }
1514
2316
  );
1515
2317
 
1516
2318
  const hasGuard = (extractedServer.serverScript && extractedServer.serverScript.has_guard) || adjacentGuard !== null;
@@ -1541,6 +2343,7 @@ export async function build(options) {
1541
2343
  // Ensure IR has required array fields for merging
1542
2344
  pageIr.components_scripts = pageIr.components_scripts || {};
1543
2345
  pageIr.component_instances = pageIr.component_instances || [];
2346
+ pageIr.signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
1544
2347
  pageIr.hoisted = pageIr.hoisted || { imports: [], declarations: [], functions: [], signals: [], state: [], code: [] };
1545
2348
  pageIr.hoisted.imports = pageIr.hoisted.imports || [];
1546
2349
  pageIr.hoisted.declarations = pageIr.hoisted.declarations || [];
@@ -1550,8 +2353,25 @@ export async function build(options) {
1550
2353
  pageIr.hoisted.code = pageIr.hoisted.code || [];
1551
2354
  const seenStaticImports = new Set();
1552
2355
  const pageExpressionRewriteMap = new Map();
2356
+ const pageExpressionBindingMap = new Map();
1553
2357
  const pageAmbiguousExpressionMap = new Set();
1554
2358
  const knownRefKeys = new Set();
2359
+ const pageScopeRewrite = buildScopedIdentifierRewrite(pageIr);
2360
+ const pageSelfExpressionRewrite = buildComponentExpressionRewrite(
2361
+ sourceFile,
2362
+ compileSource,
2363
+ pageIr,
2364
+ compilerOpts,
2365
+ compilerBin
2366
+ );
2367
+ mergeExpressionRewriteMaps(
2368
+ pageExpressionRewriteMap,
2369
+ pageExpressionBindingMap,
2370
+ pageAmbiguousExpressionMap,
2371
+ pageSelfExpressionRewrite,
2372
+ pageIr
2373
+ );
2374
+ const componentScopeRewriteCache = new Map();
1555
2375
 
1556
2376
  // 2c. Compile each used component separately for its script IR
1557
2377
  for (const compName of usedComponents) {
@@ -1568,7 +2388,10 @@ export async function build(options) {
1568
2388
  compPath,
1569
2389
  componentCompileSource,
1570
2390
  compilerOpts,
1571
- { onWarning: emitCompilerWarning }
2391
+ {
2392
+ compilerBin,
2393
+ onWarning: emitCompilerWarning
2394
+ }
1572
2395
  );
1573
2396
  componentIrCache.set(compPath, compIr);
1574
2397
  }
@@ -1581,14 +2404,62 @@ export async function build(options) {
1581
2404
 
1582
2405
  let expressionRewrite = componentExpressionRewriteCache.get(compPath);
1583
2406
  if (!expressionRewrite) {
1584
- expressionRewrite = buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts);
2407
+ expressionRewrite = buildComponentExpressionRewrite(
2408
+ compPath,
2409
+ componentSource,
2410
+ compIr,
2411
+ compilerOpts,
2412
+ compilerBin
2413
+ );
1585
2414
  componentExpressionRewriteCache.set(compPath, expressionRewrite);
1586
2415
  }
1587
- mergeExpressionRewriteMaps(
1588
- pageExpressionRewriteMap,
1589
- pageAmbiguousExpressionMap,
1590
- expressionRewrite
1591
- );
2416
+
2417
+ let usageEntry = (componentUsageAttrs.get(compName) || [])[0] || { attrs: '', ownerPath: sourceFile };
2418
+ if (!usageEntry || typeof usageEntry !== 'object') {
2419
+ usageEntry = { attrs: '', ownerPath: sourceFile };
2420
+ }
2421
+
2422
+ let attrExpressionRewrite = pageSelfExpressionRewrite;
2423
+ let attrScopeRewrite = pageScopeRewrite;
2424
+ const ownerPath = typeof usageEntry.ownerPath === 'string' && usageEntry.ownerPath.length > 0
2425
+ ? usageEntry.ownerPath
2426
+ : sourceFile;
2427
+
2428
+ if (ownerPath !== sourceFile) {
2429
+ let ownerIr = componentIrCache.get(ownerPath);
2430
+ if (!ownerIr) {
2431
+ const ownerSource = readFileSync(ownerPath, 'utf8');
2432
+ ownerIr = runCompiler(
2433
+ ownerPath,
2434
+ stripStyleBlocks(ownerSource),
2435
+ compilerOpts,
2436
+ {
2437
+ compilerBin,
2438
+ onWarning: emitCompilerWarning
2439
+ }
2440
+ );
2441
+ componentIrCache.set(ownerPath, ownerIr);
2442
+ }
2443
+
2444
+ attrExpressionRewrite = componentExpressionRewriteCache.get(ownerPath);
2445
+ if (!attrExpressionRewrite) {
2446
+ const ownerSource = readFileSync(ownerPath, 'utf8');
2447
+ attrExpressionRewrite = buildComponentExpressionRewrite(
2448
+ ownerPath,
2449
+ ownerSource,
2450
+ ownerIr,
2451
+ compilerOpts,
2452
+ compilerBin
2453
+ );
2454
+ componentExpressionRewriteCache.set(ownerPath, attrExpressionRewrite);
2455
+ }
2456
+
2457
+ attrScopeRewrite = componentScopeRewriteCache.get(ownerPath);
2458
+ if (!attrScopeRewrite) {
2459
+ attrScopeRewrite = buildScopedIdentifierRewrite(ownerIr);
2460
+ componentScopeRewriteCache.set(ownerPath, attrScopeRewrite);
2461
+ }
2462
+ }
1592
2463
 
1593
2464
  // 2d. Merge component IR into page IR
1594
2465
  mergeComponentIr(
@@ -1600,18 +2471,34 @@ export async function build(options) {
1600
2471
  includeCode: true,
1601
2472
  cssImportsOnly: isDocMode,
1602
2473
  documentMode: isDocMode,
1603
- componentAttrs: (componentUsageAttrs.get(compName) || [])[0] || ''
2474
+ componentAttrs: typeof usageEntry.attrs === 'string' ? usageEntry.attrs : '',
2475
+ componentAttrsRewrite: {
2476
+ expressionRewrite: attrExpressionRewrite,
2477
+ scopeRewrite: attrScopeRewrite
2478
+ }
1604
2479
  },
1605
2480
  seenStaticImports,
1606
2481
  knownRefKeys
1607
2482
  );
2483
+
2484
+ mergeExpressionRewriteMaps(
2485
+ pageExpressionRewriteMap,
2486
+ pageExpressionBindingMap,
2487
+ pageAmbiguousExpressionMap,
2488
+ expressionRewrite,
2489
+ pageIr
2490
+ );
1608
2491
  }
1609
2492
 
1610
2493
  applyExpressionRewrites(
1611
2494
  pageIr,
1612
2495
  pageExpressionRewriteMap,
2496
+ pageExpressionBindingMap,
1613
2497
  pageAmbiguousExpressionMap
1614
2498
  );
2499
+ applyScopedIdentifierRewrites(pageIr, buildScopedIdentifierRewrite(pageIr));
2500
+ synthesizeSignalBackedCompiledExpressions(pageIr);
2501
+ normalizeExpressionBindingDependencies(pageIr);
1615
2502
 
1616
2503
  rewriteLegacyMarkupIdentifiers(pageIr);
1617
2504
  rewriteRefBindingIdentifiers(pageIr, knownRefKeys);
@@ -1625,7 +2512,7 @@ export async function build(options) {
1625
2512
  }
1626
2513
 
1627
2514
  if (envelopes.length > 0) {
1628
- await runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo);
2515
+ await runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo, bundlerBin);
1629
2516
  }
1630
2517
 
1631
2518
  const assets = await collectAssets(outDir);