@zenithbuild/cli 0.6.2 → 0.6.3

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.
Files changed (2) hide show
  1. package/dist/build.js +617 -38
  2. package/package.json +1 -1
package/dist/build.js CHANGED
@@ -200,11 +200,31 @@ function stripStyleBlocks(source) {
200
200
  * @param {string} compPath
201
201
  * @param {string} componentSource
202
202
  * @param {object} compIr
203
- * @returns {{ map: Map<string, string>, ambiguous: Set<string> }}
203
+ * @returns {{
204
+ * map: Map<string, string>,
205
+ * bindings: Map<string, {
206
+ * compiled_expr: string | null,
207
+ * signal_index: number | null,
208
+ * signal_indices: number[],
209
+ * state_index: number | null,
210
+ * component_instance: string | null,
211
+ * component_binding: string | null
212
+ * }>,
213
+ * signals: Array<{ id?: number, kind?: string, state_index?: number }>,
214
+ * stateBindings: Array<{ key?: string, value?: string }>,
215
+ * ambiguous: Set<string>
216
+ * }}
204
217
  */
205
218
  function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts) {
206
- const out = { map: new Map(), ambiguous: new Set() };
219
+ const out = {
220
+ map: new Map(),
221
+ bindings: new Map(),
222
+ signals: Array.isArray(compIr?.signals) ? compIr.signals : [],
223
+ stateBindings: Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [],
224
+ ambiguous: new Set()
225
+ };
207
226
  const rewrittenExpressions = Array.isArray(compIr?.expressions) ? compIr.expressions : [];
227
+ const rewrittenBindings = Array.isArray(compIr?.expression_bindings) ? compIr.expression_bindings : [];
208
228
  if (rewrittenExpressions.length === 0) {
209
229
  return out;
210
230
  }
@@ -229,34 +249,243 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
229
249
  if (typeof raw !== 'string' || typeof rewritten !== 'string') {
230
250
  continue;
231
251
  }
232
- if (raw === rewritten) {
233
- continue;
252
+
253
+ const binding = rewrittenBindings[i];
254
+ const normalizedBinding = binding && typeof binding === 'object'
255
+ ? {
256
+ compiled_expr: typeof binding.compiled_expr === 'string' ? binding.compiled_expr : null,
257
+ signal_index: Number.isInteger(binding.signal_index) ? binding.signal_index : null,
258
+ signal_indices: Array.isArray(binding.signal_indices)
259
+ ? binding.signal_indices.filter((value) => Number.isInteger(value))
260
+ : [],
261
+ state_index: Number.isInteger(binding.state_index) ? binding.state_index : null,
262
+ component_instance: typeof binding.component_instance === 'string' ? binding.component_instance : null,
263
+ component_binding: typeof binding.component_binding === 'string' ? binding.component_binding : null
264
+ }
265
+ : null;
266
+
267
+ if (!out.ambiguous.has(raw) && normalizedBinding) {
268
+ const existingBinding = out.bindings.get(raw);
269
+ if (existingBinding) {
270
+ if (JSON.stringify(existingBinding) !== JSON.stringify(normalizedBinding)) {
271
+ out.bindings.delete(raw);
272
+ out.map.delete(raw);
273
+ out.ambiguous.add(raw);
274
+ continue;
275
+ }
276
+ } else {
277
+ out.bindings.set(raw, normalizedBinding);
278
+ }
234
279
  }
235
- const existing = out.map.get(raw);
236
- if (existing && existing !== rewritten) {
237
- out.map.delete(raw);
238
- out.ambiguous.add(raw);
280
+
281
+ if (raw !== rewritten) {
282
+ const existing = out.map.get(raw);
283
+ if (existing && existing !== rewritten) {
284
+ out.bindings.delete(raw);
285
+ out.map.delete(raw);
286
+ out.ambiguous.add(raw);
287
+ continue;
288
+ }
289
+ if (!out.ambiguous.has(raw)) {
290
+ out.map.set(raw, rewritten);
291
+ }
292
+ }
293
+ }
294
+
295
+ return out;
296
+ }
297
+
298
+ function remapCompiledExpressionSignals(compiledExpr, componentSignals, componentStateBindings, pageSignalIndexByStateKey) {
299
+ if (typeof compiledExpr !== 'string' || compiledExpr.length === 0) {
300
+ return null;
301
+ }
302
+
303
+ return compiledExpr.replace(/signalMap\.get\((\d+)\)/g, (full, rawIndex) => {
304
+ const localIndex = Number.parseInt(rawIndex, 10);
305
+ if (!Number.isInteger(localIndex)) {
306
+ return full;
307
+ }
308
+ const signal = componentSignals[localIndex];
309
+ if (!signal || !Number.isInteger(signal.state_index)) {
310
+ return full;
311
+ }
312
+ const stateKey = componentStateBindings[signal.state_index]?.key;
313
+ if (typeof stateKey !== 'string' || stateKey.length === 0) {
314
+ return full;
315
+ }
316
+ const pageIndex = pageSignalIndexByStateKey.get(stateKey);
317
+ if (!Number.isInteger(pageIndex)) {
318
+ return full;
319
+ }
320
+ return `signalMap.get(${pageIndex})`;
321
+ });
322
+ }
323
+
324
+ function resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding) {
325
+ if (!binding || typeof binding !== 'object') {
326
+ return null;
327
+ }
328
+
329
+ const pageStateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
330
+ const pageSignals = Array.isArray(pageIr?.hoisted?.signals) ? pageIr.hoisted.signals : [];
331
+ const pageStateIndexByKey = new Map();
332
+ const pageSignalIndexByStateKey = new Map();
333
+
334
+ for (let index = 0; index < pageStateBindings.length; index++) {
335
+ const key = pageStateBindings[index]?.key;
336
+ if (typeof key === 'string' && key.length > 0 && !pageStateIndexByKey.has(key)) {
337
+ pageStateIndexByKey.set(key, index);
338
+ }
339
+ }
340
+
341
+ for (let index = 0; index < pageSignals.length; index++) {
342
+ const stateIndex = pageSignals[index]?.state_index;
343
+ if (!Number.isInteger(stateIndex)) {
239
344
  continue;
240
345
  }
241
- if (!out.ambiguous.has(raw)) {
242
- out.map.set(raw, rewritten);
346
+ const stateKey = pageStateBindings[stateIndex]?.key;
347
+ if (typeof stateKey === 'string' && stateKey.length > 0 && !pageSignalIndexByStateKey.has(stateKey)) {
348
+ pageSignalIndexByStateKey.set(stateKey, index);
349
+ }
350
+ }
351
+
352
+ const componentSignals = Array.isArray(componentRewrite?.signals) ? componentRewrite.signals : [];
353
+ const componentStateBindings = Array.isArray(componentRewrite?.stateBindings) ? componentRewrite.stateBindings : [];
354
+
355
+ let signalIndices = Array.isArray(binding.signal_indices)
356
+ ? [...new Set(
357
+ binding.signal_indices
358
+ .map((signalIndex) => {
359
+ if (!Number.isInteger(signalIndex)) {
360
+ return null;
361
+ }
362
+ const signal = componentSignals[signalIndex];
363
+ if (!signal || !Number.isInteger(signal.state_index)) {
364
+ return null;
365
+ }
366
+ const stateKey = componentStateBindings[signal.state_index]?.key;
367
+ if (typeof stateKey !== 'string' || stateKey.length === 0) {
368
+ return null;
369
+ }
370
+ const pageIndex = pageSignalIndexByStateKey.get(stateKey);
371
+ return Number.isInteger(pageIndex) ? pageIndex : null;
372
+ })
373
+ .filter((value) => Number.isInteger(value))
374
+ )].sort((a, b) => a - b)
375
+ : [];
376
+
377
+ let signalIndex = null;
378
+ if (Number.isInteger(binding.signal_index)) {
379
+ const signal = componentSignals[binding.signal_index];
380
+ const stateKey = signal && Number.isInteger(signal.state_index)
381
+ ? componentStateBindings[signal.state_index]?.key
382
+ : null;
383
+ const pageIndex = typeof stateKey === 'string' ? pageSignalIndexByStateKey.get(stateKey) : null;
384
+ signalIndex = Number.isInteger(pageIndex) ? pageIndex : null;
385
+ }
386
+ if (signalIndex === null && signalIndices.length === 1) {
387
+ signalIndex = signalIndices[0];
388
+ }
389
+
390
+ let stateIndex = null;
391
+ if (Number.isInteger(binding.state_index)) {
392
+ const stateKey = componentStateBindings[binding.state_index]?.key;
393
+ const pageIndex = typeof stateKey === 'string' ? pageStateIndexByKey.get(stateKey) : null;
394
+ stateIndex = Number.isInteger(pageIndex) ? pageIndex : null;
395
+ }
396
+
397
+ if (Number.isInteger(stateIndex)) {
398
+ const fallbackSignalIndices = pageSignals
399
+ .map((signal, index) => signal?.state_index === stateIndex ? index : null)
400
+ .filter((value) => Number.isInteger(value));
401
+ const signalIndicesMatchState = signalIndices.every(
402
+ (index) => pageSignals[index]?.state_index === stateIndex
403
+ );
404
+ if ((!signalIndicesMatchState || signalIndices.length === 0) && fallbackSignalIndices.length > 0) {
405
+ signalIndices = fallbackSignalIndices;
406
+ }
407
+ if (
408
+ (signalIndex === null || pageSignals[signalIndex]?.state_index !== stateIndex) &&
409
+ fallbackSignalIndices.length === 1
410
+ ) {
411
+ signalIndex = fallbackSignalIndices[0];
243
412
  }
244
413
  }
245
414
 
246
- return out;
415
+ let compiledExpr = remapCompiledExpressionSignals(
416
+ binding.compiled_expr,
417
+ componentSignals,
418
+ componentStateBindings,
419
+ pageSignalIndexByStateKey
420
+ );
421
+ if (
422
+ typeof compiledExpr === 'string' &&
423
+ signalIndices.length === 1 &&
424
+ Array.isArray(binding.signal_indices) &&
425
+ binding.signal_indices.length <= 1
426
+ ) {
427
+ compiledExpr = compiledExpr.replace(/signalMap\.get\(\d+\)/g, `signalMap.get(${signalIndices[0]})`);
428
+ }
429
+
430
+ return {
431
+ compiled_expr: compiledExpr,
432
+ signal_index: signalIndex,
433
+ signal_indices: signalIndices,
434
+ state_index: stateIndex,
435
+ component_instance: typeof binding.component_instance === 'string' ? binding.component_instance : null,
436
+ component_binding: typeof binding.component_binding === 'string' ? binding.component_binding : null
437
+ };
247
438
  }
248
439
 
249
440
  /**
250
441
  * Merge a per-component rewrite table into the page-level rewrite table.
251
442
  *
252
443
  * @param {Map<string, string>} pageMap
444
+ * @param {Map<string, {
445
+ * compiled_expr: string | null,
446
+ * signal_index: number | null,
447
+ * signal_indices: number[],
448
+ * state_index: number | null,
449
+ * component_instance: string | null,
450
+ * component_binding: string | null
451
+ * }>} pageBindingMap
253
452
  * @param {Set<string>} pageAmbiguous
254
- * @param {{ map: Map<string, string>, ambiguous: Set<string> }} componentRewrite
453
+ * @param {{
454
+ * map: Map<string, string>,
455
+ * bindings: Map<string, {
456
+ * compiled_expr: string | null,
457
+ * signal_index: number | null,
458
+ * signal_indices: number[],
459
+ * state_index: number | null,
460
+ * component_instance: string | null,
461
+ * component_binding: string | null
462
+ * }>,
463
+ * signals: Array<{ id?: number, kind?: string, state_index?: number }>,
464
+ * stateBindings: Array<{ key?: string, value?: string }>,
465
+ * ambiguous: Set<string>
466
+ * }} componentRewrite
467
+ * @param {object} pageIr
255
468
  */
256
- function mergeExpressionRewriteMaps(pageMap, pageAmbiguous, componentRewrite) {
469
+ function mergeExpressionRewriteMaps(pageMap, pageBindingMap, pageAmbiguous, componentRewrite, pageIr) {
257
470
  for (const raw of componentRewrite.ambiguous) {
258
471
  pageAmbiguous.add(raw);
259
472
  pageMap.delete(raw);
473
+ pageBindingMap.delete(raw);
474
+ }
475
+
476
+ for (const [raw, binding] of componentRewrite.bindings.entries()) {
477
+ if (pageAmbiguous.has(raw)) {
478
+ continue;
479
+ }
480
+ const resolved = resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding);
481
+ const existing = pageBindingMap.get(raw);
482
+ if (existing && JSON.stringify(existing) !== JSON.stringify(resolved)) {
483
+ pageAmbiguous.add(raw);
484
+ pageMap.delete(raw);
485
+ pageBindingMap.delete(raw);
486
+ continue;
487
+ }
488
+ pageBindingMap.set(raw, resolved);
260
489
  }
261
490
 
262
491
  for (const [raw, rewritten] of componentRewrite.map.entries()) {
@@ -267,6 +496,7 @@ function mergeExpressionRewriteMaps(pageMap, pageAmbiguous, componentRewrite) {
267
496
  if (existing && existing !== rewritten) {
268
497
  pageAmbiguous.add(raw);
269
498
  pageMap.delete(raw);
499
+ pageBindingMap.delete(raw);
270
500
  continue;
271
501
  }
272
502
  pageMap.set(raw, rewritten);
@@ -329,9 +559,17 @@ function rewriteRefBindingIdentifiers(pageIr, preferredKeys = null) {
329
559
  *
330
560
  * @param {object} pageIr
331
561
  * @param {Map<string, string>} expressionMap
562
+ * @param {Map<string, {
563
+ * compiled_expr: string | null,
564
+ * signal_index: number | null,
565
+ * signal_indices: number[],
566
+ * state_index: number | null,
567
+ * component_instance: string | null,
568
+ * component_binding: string | null
569
+ * }>} bindingMap
332
570
  * @param {Set<string>} ambiguous
333
571
  */
334
- function applyExpressionRewrites(pageIr, expressionMap, ambiguous) {
572
+ function applyExpressionRewrites(pageIr, expressionMap, bindingMap, ambiguous) {
335
573
  if (!Array.isArray(pageIr?.expressions) || pageIr.expressions.length === 0) {
336
574
  return;
337
575
  }
@@ -345,21 +583,109 @@ function applyExpressionRewrites(pageIr, expressionMap, ambiguous) {
345
583
  if (ambiguous.has(current)) {
346
584
  continue;
347
585
  }
586
+
348
587
  const rewritten = expressionMap.get(current);
349
- if (!rewritten || rewritten === current) {
588
+ const rewrittenBinding = bindingMap.get(current);
589
+ if (rewritten && rewritten !== current) {
590
+ pageIr.expressions[index] = rewritten;
591
+ }
592
+
593
+ if (!bindings[index] || typeof bindings[index] !== 'object') {
350
594
  continue;
351
595
  }
352
- pageIr.expressions[index] = rewritten;
596
+
597
+ if (rewritten && rewritten !== current && bindings[index].literal === current) {
598
+ bindings[index].literal = rewritten;
599
+ }
600
+
601
+ if (rewrittenBinding) {
602
+ bindings[index].compiled_expr = rewrittenBinding.compiled_expr;
603
+ bindings[index].signal_index = rewrittenBinding.signal_index;
604
+ bindings[index].signal_indices = rewrittenBinding.signal_indices;
605
+ bindings[index].state_index = rewrittenBinding.state_index;
606
+ bindings[index].component_instance = rewrittenBinding.component_instance;
607
+ bindings[index].component_binding = rewrittenBinding.component_binding;
608
+ } else if (rewritten && rewritten !== current && bindings[index].compiled_expr === current) {
609
+ bindings[index].compiled_expr = rewritten;
610
+ }
611
+
353
612
  if (
354
- bindings[index] &&
355
- typeof bindings[index] === 'object' &&
356
- bindings[index].literal === current
613
+ !rewrittenBinding &&
614
+ (!rewritten || rewritten === current) &&
615
+ bindings[index].literal === current &&
616
+ bindings[index].compiled_expr === current
357
617
  ) {
358
- bindings[index].literal = rewritten;
359
- if (bindings[index].compiled_expr === current) {
360
- bindings[index].compiled_expr = rewritten;
618
+ bindings[index].compiled_expr = current;
619
+ }
620
+ }
621
+ }
622
+
623
+ function normalizeExpressionBindingDependencies(pageIr) {
624
+ if (!Array.isArray(pageIr?.expression_bindings) || pageIr.expression_bindings.length === 0) {
625
+ return;
626
+ }
627
+
628
+ const signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
629
+ const dependencyRe = /signalMap\.get\((\d+)\)/g;
630
+
631
+ for (const binding of pageIr.expression_bindings) {
632
+ if (!binding || typeof binding !== 'object' || typeof binding.compiled_expr !== 'string') {
633
+ continue;
634
+ }
635
+
636
+ const indices = [];
637
+ dependencyRe.lastIndex = 0;
638
+ let match;
639
+ while ((match = dependencyRe.exec(binding.compiled_expr)) !== null) {
640
+ const index = Number.parseInt(match[1], 10);
641
+ if (Number.isInteger(index)) {
642
+ indices.push(index);
643
+ }
644
+ }
645
+
646
+ if (indices.length === 0) {
647
+ continue;
648
+ }
649
+
650
+ let signalIndices = [...new Set(indices)].sort((a, b) => a - b);
651
+ if (Number.isInteger(binding.state_index)) {
652
+ const owningSignalIndices = signals
653
+ .map((signal, index) => signal?.state_index === binding.state_index ? index : null)
654
+ .filter((value) => Number.isInteger(value));
655
+ const extractedMatchState =
656
+ signalIndices.length > 0 &&
657
+ signalIndices.every((index) => signals[index]?.state_index === binding.state_index);
658
+ if (owningSignalIndices.length > 0 && !extractedMatchState) {
659
+ signalIndices = owningSignalIndices;
660
+ }
661
+ }
662
+
663
+ if (
664
+ !Array.isArray(binding.signal_indices) ||
665
+ binding.signal_indices.length === 0 ||
666
+ binding.signal_indices.some((index) => signals[index]?.state_index !== binding.state_index)
667
+ ) {
668
+ binding.signal_indices = signalIndices;
669
+ }
670
+ if (
671
+ (!Number.isInteger(binding.signal_index) ||
672
+ signals[binding.signal_index]?.state_index !== binding.state_index) &&
673
+ signalIndices.length === 1
674
+ ) {
675
+ binding.signal_index = signalIndices[0];
676
+ }
677
+ if (!Number.isInteger(binding.state_index) && Number.isInteger(binding.signal_index)) {
678
+ const stateIndex = signals[binding.signal_index]?.state_index;
679
+ if (Number.isInteger(stateIndex)) {
680
+ binding.state_index = stateIndex;
361
681
  }
362
682
  }
683
+ if (signalIndices.length === 1) {
684
+ binding.compiled_expr = binding.compiled_expr.replace(
685
+ /signalMap\.get\(\d+\)/g,
686
+ `signalMap.get(${signalIndices[0]})`
687
+ );
688
+ }
363
689
  }
364
690
  }
365
691
 
@@ -798,9 +1124,10 @@ const OPEN_COMPONENT_TAG_RE = /<([A-Z][a-zA-Z0-9]*)(\s[^<>]*?)?\s*(\/?)>/g;
798
1124
  *
799
1125
  * @param {string} source
800
1126
  * @param {Map<string, string>} registry
801
- * @returns {Map<string, string[]>}
1127
+ * @param {string | null} ownerPath
1128
+ * @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
802
1129
  */
803
- function collectComponentUsageAttrs(source, registry) {
1130
+ function collectComponentUsageAttrs(source, registry, ownerPath = null) {
804
1131
  const out = new Map();
805
1132
  OPEN_COMPONENT_TAG_RE.lastIndex = 0;
806
1133
  let match;
@@ -813,8 +1140,44 @@ function collectComponentUsageAttrs(source, registry) {
813
1140
  if (!out.has(name)) {
814
1141
  out.set(name, []);
815
1142
  }
816
- out.get(name).push(attrs);
1143
+ out.get(name).push({ attrs, ownerPath });
1144
+ }
1145
+ return out;
1146
+ }
1147
+
1148
+ /**
1149
+ * Collect component usage attrs recursively so nested component callsites
1150
+ * receive deterministic props preludes during page-hoist merging.
1151
+ *
1152
+ * Current Zenith architecture still resolves one attrs set per component type.
1153
+ * This helper preserves that model while ensuring nested usages are not lost.
1154
+ *
1155
+ * @param {string} source
1156
+ * @param {Map<string, string>} registry
1157
+ * @param {string | null} ownerPath
1158
+ * @param {Set<string>} visitedFiles
1159
+ * @param {Map<string, Array<{ attrs: string, ownerPath: string | null }>>} out
1160
+ * @returns {Map<string, Array<{ attrs: string, ownerPath: string | null }>>}
1161
+ */
1162
+ function collectRecursiveComponentUsageAttrs(source, registry, ownerPath = null, visitedFiles = new Set(), out = new Map()) {
1163
+ const local = collectComponentUsageAttrs(source, registry, ownerPath);
1164
+ for (const [name, attrsList] of local.entries()) {
1165
+ if (!out.has(name)) {
1166
+ out.set(name, []);
1167
+ }
1168
+ out.get(name).push(...attrsList);
817
1169
  }
1170
+
1171
+ for (const name of local.keys()) {
1172
+ const compPath = registry.get(name);
1173
+ if (!compPath || visitedFiles.has(compPath)) {
1174
+ continue;
1175
+ }
1176
+ visitedFiles.add(compPath);
1177
+ const componentSource = readFileSync(compPath, 'utf8');
1178
+ collectRecursiveComponentUsageAttrs(componentSource, registry, compPath, visitedFiles, out);
1179
+ }
1180
+
818
1181
  return out;
819
1182
  }
820
1183
 
@@ -828,7 +1191,16 @@ function collectComponentUsageAttrs(source, registry) {
828
1191
  * @param {object} compIr — the component's compiled IR
829
1192
  * @param {string} compPath — component file path
830
1193
  * @param {string} pageFile — page file path
831
- * @param {{ includeCode: boolean, cssImportsOnly: boolean, documentMode?: boolean, componentAttrs?: string }} options
1194
+ * @param {{
1195
+ * includeCode: boolean,
1196
+ * cssImportsOnly: boolean,
1197
+ * documentMode?: boolean,
1198
+ * componentAttrs?: string,
1199
+ * componentAttrsRewrite?: {
1200
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1201
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1202
+ * } | null
1203
+ * }} options
832
1204
  * @param {Set<string>} seenStaticImports
833
1205
  */
834
1206
  function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStaticImports, knownRefKeys = null) {
@@ -920,6 +1292,41 @@ function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStati
920
1292
  }
921
1293
  }
922
1294
 
1295
+ if (options.includeCode && Array.isArray(compIr.signals)) {
1296
+ pageIr.signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
1297
+ const existingSignalStateKeys = new Set(
1298
+ pageIr.signals
1299
+ .map((signal) => {
1300
+ const stateIndex = signal?.state_index;
1301
+ return Number.isInteger(stateIndex) ? pageIr.hoisted.state?.[stateIndex]?.key : null;
1302
+ })
1303
+ .filter(Boolean)
1304
+ );
1305
+
1306
+ for (const signal of compIr.signals) {
1307
+ if (!signal || !Number.isInteger(signal.state_index)) {
1308
+ continue;
1309
+ }
1310
+ const stateKey = compIr.hoisted?.state?.[signal.state_index]?.key;
1311
+ if (typeof stateKey !== 'string' || stateKey.length === 0) {
1312
+ continue;
1313
+ }
1314
+ const pageStateIndex = pageIr.hoisted.state.findIndex((entry) => entry?.key === stateKey);
1315
+ if (!Number.isInteger(pageStateIndex) || pageStateIndex < 0) {
1316
+ continue;
1317
+ }
1318
+ if (existingSignalStateKeys.has(stateKey)) {
1319
+ continue;
1320
+ }
1321
+ existingSignalStateKeys.add(stateKey);
1322
+ pageIr.signals.push({
1323
+ id: pageIr.signals.length,
1324
+ kind: typeof signal.kind === 'string' && signal.kind.length > 0 ? signal.kind : 'signal',
1325
+ state_index: pageStateIndex
1326
+ });
1327
+ }
1328
+ }
1329
+
923
1330
  // Merge hoisted code blocks (rebased to the page file path)
924
1331
  if (options.includeCode && compIr.hoisted?.code?.length) {
925
1332
  for (const block of compIr.hoisted.code) {
@@ -927,7 +1334,11 @@ function mergeComponentIr(pageIr, compIr, compPath, pageFile, options, seenStati
927
1334
  const filteredImports = options.cssImportsOnly
928
1335
  ? stripNonCssStaticImportsInSource(rebased)
929
1336
  : rebased;
930
- const withPropsPrelude = injectPropsPrelude(filteredImports, options.componentAttrs || '');
1337
+ const withPropsPrelude = injectPropsPrelude(
1338
+ filteredImports,
1339
+ options.componentAttrs || '',
1340
+ options.componentAttrsRewrite || null
1341
+ );
931
1342
  const transpiled = transpileTypeScriptToJs(withPropsPrelude, compPath);
932
1343
  const deduped = dedupeStaticImportsInSource(transpiled, seenStaticImports);
933
1344
  const deferred = deferComponentRuntimeBlock(deduped);
@@ -1228,11 +1639,123 @@ function renderObjectKey(key) {
1228
1639
  return JSON.stringify(key);
1229
1640
  }
1230
1641
 
1642
+ /**
1643
+ * @param {string} value
1644
+ * @returns {string | null}
1645
+ */
1646
+ function deriveScopedIdentifierAlias(value) {
1647
+ const ident = String(value || '').trim();
1648
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ident)) {
1649
+ return null;
1650
+ }
1651
+ const parts = ident.split('_').filter(Boolean);
1652
+ const candidate = parts.length > 1 ? parts[parts.length - 1] : ident;
1653
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate) ? candidate : ident;
1654
+ }
1655
+
1656
+ /**
1657
+ * @param {Map<string, string>} map
1658
+ * @param {Set<string>} ambiguous
1659
+ * @param {string | null} raw
1660
+ * @param {string | null} rewritten
1661
+ */
1662
+ function recordScopedIdentifierRewrite(map, ambiguous, raw, rewritten) {
1663
+ if (typeof raw !== 'string' || raw.length === 0 || typeof rewritten !== 'string' || rewritten.length === 0) {
1664
+ return;
1665
+ }
1666
+ const existing = map.get(raw);
1667
+ if (existing && existing !== rewritten) {
1668
+ map.delete(raw);
1669
+ ambiguous.add(raw);
1670
+ return;
1671
+ }
1672
+ if (!ambiguous.has(raw)) {
1673
+ map.set(raw, rewritten);
1674
+ }
1675
+ }
1676
+
1677
+ /**
1678
+ * @param {object | null | undefined} ir
1679
+ * @returns {{ map: Map<string, string>, ambiguous: Set<string> }}
1680
+ */
1681
+ function buildScopedIdentifierRewrite(ir) {
1682
+ const out = { map: new Map(), ambiguous: new Set() };
1683
+ if (!ir || typeof ir !== 'object') {
1684
+ return out;
1685
+ }
1686
+
1687
+ const stateBindings = Array.isArray(ir?.hoisted?.state) ? ir.hoisted.state : [];
1688
+ for (const stateEntry of stateBindings) {
1689
+ const key = typeof stateEntry?.key === 'string' ? stateEntry.key : null;
1690
+ recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(key), key);
1691
+ }
1692
+
1693
+ const functionBindings = Array.isArray(ir?.hoisted?.functions) ? ir.hoisted.functions : [];
1694
+ for (const fnName of functionBindings) {
1695
+ if (typeof fnName !== 'string') {
1696
+ continue;
1697
+ }
1698
+ recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(fnName), fnName);
1699
+ }
1700
+
1701
+ return out;
1702
+ }
1703
+
1704
+ /**
1705
+ * @param {string} expr
1706
+ * @param {{
1707
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1708
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1709
+ * } | null} rewriteContext
1710
+ * @returns {string}
1711
+ */
1712
+ function rewritePropsExpression(expr, rewriteContext = null) {
1713
+ const trimmed = String(expr || '').trim();
1714
+ if (!trimmed) {
1715
+ return trimmed;
1716
+ }
1717
+
1718
+ const expressionMap = rewriteContext?.expressionRewrite?.map;
1719
+ const expressionAmbiguous = rewriteContext?.expressionRewrite?.ambiguous;
1720
+ if (
1721
+ expressionMap instanceof Map &&
1722
+ !(expressionAmbiguous instanceof Set && expressionAmbiguous.has(trimmed))
1723
+ ) {
1724
+ const exact = expressionMap.get(trimmed);
1725
+ if (typeof exact === 'string' && exact.length > 0) {
1726
+ return exact;
1727
+ }
1728
+ }
1729
+
1730
+ const scopeMap = rewriteContext?.scopeRewrite?.map;
1731
+ const scopeAmbiguous = rewriteContext?.scopeRewrite?.ambiguous;
1732
+ const rootMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)([\s\S]*)$/);
1733
+ if (!rootMatch || !(scopeMap instanceof Map)) {
1734
+ return trimmed;
1735
+ }
1736
+
1737
+ const root = rootMatch[1];
1738
+ if (scopeAmbiguous instanceof Set && scopeAmbiguous.has(root)) {
1739
+ return trimmed;
1740
+ }
1741
+
1742
+ const rewrittenRoot = scopeMap.get(root);
1743
+ if (typeof rewrittenRoot !== 'string' || rewrittenRoot.length === 0 || rewrittenRoot === root) {
1744
+ return trimmed;
1745
+ }
1746
+
1747
+ return `${rewrittenRoot}${rootMatch[2]}`;
1748
+ }
1749
+
1231
1750
  /**
1232
1751
  * @param {string} attrs
1752
+ * @param {{
1753
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1754
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1755
+ * } | null} rewriteContext
1233
1756
  * @returns {string}
1234
1757
  */
1235
- function renderPropsLiteralFromAttrs(attrs) {
1758
+ function renderPropsLiteralFromAttrs(attrs, rewriteContext = null) {
1236
1759
  const src = String(attrs || '').trim();
1237
1760
  if (!src) {
1238
1761
  return '{}';
@@ -1257,7 +1780,7 @@ function renderPropsLiteralFromAttrs(attrs) {
1257
1780
  valueCode = JSON.stringify(singleQuoted);
1258
1781
  } else if (expressionValue !== undefined) {
1259
1782
  const trimmed = String(expressionValue).trim();
1260
- valueCode = trimmed.length > 0 ? trimmed : 'undefined';
1783
+ valueCode = trimmed.length > 0 ? rewritePropsExpression(trimmed, rewriteContext) : 'undefined';
1261
1784
  }
1262
1785
 
1263
1786
  entries.push(`${renderObjectKey(rawName)}: ${valueCode}`);
@@ -1273,9 +1796,13 @@ function renderPropsLiteralFromAttrs(attrs) {
1273
1796
  /**
1274
1797
  * @param {string} source
1275
1798
  * @param {string} attrs
1799
+ * @param {{
1800
+ * expressionRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null,
1801
+ * scopeRewrite?: { map?: Map<string, string>, ambiguous?: Set<string> } | null
1802
+ * } | null} rewriteContext
1276
1803
  * @returns {string}
1277
1804
  */
1278
- function injectPropsPrelude(source, attrs) {
1805
+ function injectPropsPrelude(source, attrs, rewriteContext = null) {
1279
1806
  if (typeof source !== 'string' || source.trim().length === 0) {
1280
1807
  return source;
1281
1808
  }
@@ -1286,7 +1813,7 @@ function injectPropsPrelude(source, attrs) {
1286
1813
  return source;
1287
1814
  }
1288
1815
 
1289
- const propsLiteral = renderPropsLiteralFromAttrs(attrs);
1816
+ const propsLiteral = renderPropsLiteralFromAttrs(attrs, rewriteContext);
1290
1817
  return `var props = ${propsLiteral};\n${source}`;
1291
1818
  }
1292
1819
 
@@ -1488,7 +2015,7 @@ export async function build(options) {
1488
2015
  for (const entry of manifest) {
1489
2016
  const sourceFile = join(pagesDir, entry.file);
1490
2017
  const rawSource = readFileSync(sourceFile, 'utf8');
1491
- const componentUsageAttrs = collectComponentUsageAttrs(rawSource, registry);
2018
+ const componentUsageAttrs = collectRecursiveComponentUsageAttrs(rawSource, registry, sourceFile);
1492
2019
 
1493
2020
  const baseName = sourceFile.slice(0, -extname(sourceFile).length);
1494
2021
  let adjacentGuard = null;
@@ -1541,6 +2068,7 @@ export async function build(options) {
1541
2068
  // Ensure IR has required array fields for merging
1542
2069
  pageIr.components_scripts = pageIr.components_scripts || {};
1543
2070
  pageIr.component_instances = pageIr.component_instances || [];
2071
+ pageIr.signals = Array.isArray(pageIr.signals) ? pageIr.signals : [];
1544
2072
  pageIr.hoisted = pageIr.hoisted || { imports: [], declarations: [], functions: [], signals: [], state: [], code: [] };
1545
2073
  pageIr.hoisted.imports = pageIr.hoisted.imports || [];
1546
2074
  pageIr.hoisted.declarations = pageIr.hoisted.declarations || [];
@@ -1550,8 +2078,12 @@ export async function build(options) {
1550
2078
  pageIr.hoisted.code = pageIr.hoisted.code || [];
1551
2079
  const seenStaticImports = new Set();
1552
2080
  const pageExpressionRewriteMap = new Map();
2081
+ const pageExpressionBindingMap = new Map();
1553
2082
  const pageAmbiguousExpressionMap = new Set();
1554
2083
  const knownRefKeys = new Set();
2084
+ const pageScopeRewrite = buildScopedIdentifierRewrite(pageIr);
2085
+ const pageSelfExpressionRewrite = buildComponentExpressionRewrite(sourceFile, compileSource, pageIr, compilerOpts);
2086
+ const componentScopeRewriteCache = new Map();
1555
2087
 
1556
2088
  // 2c. Compile each used component separately for its script IR
1557
2089
  for (const compName of usedComponents) {
@@ -1584,11 +2116,44 @@ export async function build(options) {
1584
2116
  expressionRewrite = buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts);
1585
2117
  componentExpressionRewriteCache.set(compPath, expressionRewrite);
1586
2118
  }
1587
- mergeExpressionRewriteMaps(
1588
- pageExpressionRewriteMap,
1589
- pageAmbiguousExpressionMap,
1590
- expressionRewrite
1591
- );
2119
+
2120
+ let usageEntry = (componentUsageAttrs.get(compName) || [])[0] || { attrs: '', ownerPath: sourceFile };
2121
+ if (!usageEntry || typeof usageEntry !== 'object') {
2122
+ usageEntry = { attrs: '', ownerPath: sourceFile };
2123
+ }
2124
+
2125
+ let attrExpressionRewrite = pageSelfExpressionRewrite;
2126
+ let attrScopeRewrite = pageScopeRewrite;
2127
+ const ownerPath = typeof usageEntry.ownerPath === 'string' && usageEntry.ownerPath.length > 0
2128
+ ? usageEntry.ownerPath
2129
+ : sourceFile;
2130
+
2131
+ if (ownerPath !== sourceFile) {
2132
+ let ownerIr = componentIrCache.get(ownerPath);
2133
+ if (!ownerIr) {
2134
+ const ownerSource = readFileSync(ownerPath, 'utf8');
2135
+ ownerIr = runCompiler(
2136
+ ownerPath,
2137
+ stripStyleBlocks(ownerSource),
2138
+ compilerOpts,
2139
+ { onWarning: emitCompilerWarning }
2140
+ );
2141
+ componentIrCache.set(ownerPath, ownerIr);
2142
+ }
2143
+
2144
+ attrExpressionRewrite = componentExpressionRewriteCache.get(ownerPath);
2145
+ if (!attrExpressionRewrite) {
2146
+ const ownerSource = readFileSync(ownerPath, 'utf8');
2147
+ attrExpressionRewrite = buildComponentExpressionRewrite(ownerPath, ownerSource, ownerIr, compilerOpts);
2148
+ componentExpressionRewriteCache.set(ownerPath, attrExpressionRewrite);
2149
+ }
2150
+
2151
+ attrScopeRewrite = componentScopeRewriteCache.get(ownerPath);
2152
+ if (!attrScopeRewrite) {
2153
+ attrScopeRewrite = buildScopedIdentifierRewrite(ownerIr);
2154
+ componentScopeRewriteCache.set(ownerPath, attrScopeRewrite);
2155
+ }
2156
+ }
1592
2157
 
1593
2158
  // 2d. Merge component IR into page IR
1594
2159
  mergeComponentIr(
@@ -1600,18 +2165,32 @@ export async function build(options) {
1600
2165
  includeCode: true,
1601
2166
  cssImportsOnly: isDocMode,
1602
2167
  documentMode: isDocMode,
1603
- componentAttrs: (componentUsageAttrs.get(compName) || [])[0] || ''
2168
+ componentAttrs: typeof usageEntry.attrs === 'string' ? usageEntry.attrs : '',
2169
+ componentAttrsRewrite: {
2170
+ expressionRewrite: attrExpressionRewrite,
2171
+ scopeRewrite: attrScopeRewrite
2172
+ }
1604
2173
  },
1605
2174
  seenStaticImports,
1606
2175
  knownRefKeys
1607
2176
  );
2177
+
2178
+ mergeExpressionRewriteMaps(
2179
+ pageExpressionRewriteMap,
2180
+ pageExpressionBindingMap,
2181
+ pageAmbiguousExpressionMap,
2182
+ expressionRewrite,
2183
+ pageIr
2184
+ );
1608
2185
  }
1609
2186
 
1610
2187
  applyExpressionRewrites(
1611
2188
  pageIr,
1612
2189
  pageExpressionRewriteMap,
2190
+ pageExpressionBindingMap,
1613
2191
  pageAmbiguousExpressionMap
1614
2192
  );
2193
+ normalizeExpressionBindingDependencies(pageIr);
1615
2194
 
1616
2195
  rewriteLegacyMarkupIdentifiers(pageIr);
1617
2196
  rewriteRefBindingIdentifiers(pageIr, knownRefKeys);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",