@zenithbuild/cli 0.6.3 → 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,6 +166,8 @@ function stripStyleBlocks(source) {
200
166
  * @param {string} compPath
201
167
  * @param {string} componentSource
202
168
  * @param {object} compIr
169
+ * @param {object} compilerOpts
170
+ * @param {string} compilerBin
203
171
  * @returns {{
204
172
  * map: Map<string, string>,
205
173
  * bindings: Map<string, {
@@ -215,7 +183,7 @@ function stripStyleBlocks(source) {
215
183
  * ambiguous: Set<string>
216
184
  * }}
217
185
  */
218
- function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts) {
186
+ function buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts, compilerBin) {
219
187
  const out = {
220
188
  map: new Map(),
221
189
  bindings: new Map(),
@@ -236,7 +204,10 @@ function buildComponentExpressionRewrite(compPath, componentSource, compIr, comp
236
204
 
237
205
  let templateIr;
238
206
  try {
239
- templateIr = runCompiler(compPath, templateOnly, compilerOpts, { suppressWarnings: true });
207
+ templateIr = runCompiler(compPath, templateOnly, compilerOpts, {
208
+ suppressWarnings: true,
209
+ compilerBin
210
+ });
240
211
  } catch {
241
212
  return out;
242
213
  }
@@ -327,7 +298,7 @@ function resolveRewrittenBindingMetadata(pageIr, componentRewrite, binding) {
327
298
  }
328
299
 
329
300
  const pageStateBindings = Array.isArray(pageIr?.hoisted?.state) ? pageIr.hoisted.state : [];
330
- const pageSignals = Array.isArray(pageIr?.hoisted?.signals) ? pageIr.hoisted.signals : [];
301
+ const pageSignals = Array.isArray(pageIr?.signals) ? pageIr.signals : [];
331
302
  const pageStateIndexByKey = new Map();
332
303
  const pageSignalIndexByStateKey = new Map();
333
304
 
@@ -620,6 +591,107 @@ function applyExpressionRewrites(pageIr, expressionMap, bindingMap, ambiguous) {
620
591
  }
621
592
  }
622
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;
690
+ }
691
+ }
692
+ }
693
+ }
694
+
623
695
  function normalizeExpressionBindingDependencies(pageIr) {
624
696
  if (!Array.isArray(pageIr?.expression_bindings) || pageIr.expression_bindings.length === 0) {
625
697
  return;
@@ -1653,6 +1725,62 @@ function deriveScopedIdentifierAlias(value) {
1653
1725
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(candidate) ? candidate : ident;
1654
1726
  }
1655
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
+
1656
1784
  /**
1657
1785
  * @param {Map<string, string>} map
1658
1786
  * @param {Set<string>} ambiguous
@@ -1698,9 +1826,131 @@ function buildScopedIdentifierRewrite(ir) {
1698
1826
  recordScopedIdentifierRewrite(out.map, out.ambiguous, deriveScopedIdentifierAlias(fnName), fnName);
1699
1827
  }
1700
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
+
1701
1839
  return out;
1702
1840
  }
1703
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
+
1704
1954
  /**
1705
1955
  * @param {string} expr
1706
1956
  * @param {{
@@ -1730,18 +1980,21 @@ function rewritePropsExpression(expr, rewriteContext = null) {
1730
1980
  const scopeMap = rewriteContext?.scopeRewrite?.map;
1731
1981
  const scopeAmbiguous = rewriteContext?.scopeRewrite?.ambiguous;
1732
1982
  const rootMatch = trimmed.match(/^([A-Za-z_$][A-Za-z0-9_$]*)([\s\S]*)$/);
1733
- if (!rootMatch || !(scopeMap instanceof Map)) {
1983
+ if (!(scopeMap instanceof Map)) {
1734
1984
  return trimmed;
1735
1985
  }
1986
+ if (!rootMatch) {
1987
+ return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1988
+ }
1736
1989
 
1737
1990
  const root = rootMatch[1];
1738
1991
  if (scopeAmbiguous instanceof Set && scopeAmbiguous.has(root)) {
1739
- return trimmed;
1992
+ return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1740
1993
  }
1741
1994
 
1742
1995
  const rewrittenRoot = scopeMap.get(root);
1743
1996
  if (typeof rewrittenRoot !== 'string' || rewrittenRoot.length === 0 || rewrittenRoot === root) {
1744
- return trimmed;
1997
+ return rewriteIdentifiersWithinExpression(trimmed, scopeMap, scopeAmbiguous);
1745
1998
  }
1746
1999
 
1747
2000
  return `${rewrittenRoot}${rootMatch[2]}`;
@@ -1874,13 +2127,21 @@ function deferComponentRuntimeBlock(source) {
1874
2127
  * @param {string} projectRoot
1875
2128
  * @param {object | null} [logger]
1876
2129
  * @param {boolean} [showInfo]
2130
+ * @param {string} [bundlerBin]
1877
2131
  * @returns {Promise<void>}
1878
2132
  */
1879
- 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
+ ) {
1880
2141
  return new Promise((resolvePromise, rejectPromise) => {
1881
2142
  const useStructuredLogger = Boolean(logger && typeof logger.childLine === 'function');
1882
2143
  const child = spawn(
1883
- getBundlerBin(),
2144
+ bundlerBin,
1884
2145
  ['--out-dir', outDir],
1885
2146
  {
1886
2147
  cwd: projectRoot,
@@ -1968,6 +2229,8 @@ async function collectAssets(rootDir) {
1968
2229
  export async function build(options) {
1969
2230
  const { pagesDir, outDir, config = {}, logger = null, showBundlerInfo = true } = options;
1970
2231
  const projectRoot = deriveProjectRootFromPagesDir(pagesDir);
2232
+ const compilerBin = resolveCompilerBin(projectRoot);
2233
+ const bundlerBin = resolveBundlerBin(projectRoot);
1971
2234
  const softNavigationEnabled = config.softNavigation === true || config.router === true;
1972
2235
  const compilerOpts = {
1973
2236
  typescriptDefault: config.typescriptDefault === true,
@@ -1978,6 +2241,15 @@ export async function build(options) {
1978
2241
  await rm(outDir, { recursive: true, force: true });
1979
2242
  await mkdir(outDir, { recursive: true });
1980
2243
 
2244
+ if (logger) {
2245
+ await maybeWarnAboutZenithVersionMismatch({
2246
+ projectRoot,
2247
+ logger,
2248
+ command: 'build',
2249
+ bundlerBinPath: bundlerBin
2250
+ });
2251
+ }
2252
+
1981
2253
  // Derive src/ directory from pages/ directory
1982
2254
  const srcDir = resolve(pagesDir, '..');
1983
2255
 
@@ -2037,7 +2309,10 @@ export async function build(options) {
2037
2309
  sourceFile,
2038
2310
  compileSource,
2039
2311
  compilerOpts,
2040
- { onWarning: emitCompilerWarning }
2312
+ {
2313
+ compilerBin,
2314
+ onWarning: emitCompilerWarning
2315
+ }
2041
2316
  );
2042
2317
 
2043
2318
  const hasGuard = (extractedServer.serverScript && extractedServer.serverScript.has_guard) || adjacentGuard !== null;
@@ -2082,7 +2357,20 @@ export async function build(options) {
2082
2357
  const pageAmbiguousExpressionMap = new Set();
2083
2358
  const knownRefKeys = new Set();
2084
2359
  const pageScopeRewrite = buildScopedIdentifierRewrite(pageIr);
2085
- const pageSelfExpressionRewrite = buildComponentExpressionRewrite(sourceFile, compileSource, pageIr, compilerOpts);
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
+ );
2086
2374
  const componentScopeRewriteCache = new Map();
2087
2375
 
2088
2376
  // 2c. Compile each used component separately for its script IR
@@ -2100,7 +2388,10 @@ export async function build(options) {
2100
2388
  compPath,
2101
2389
  componentCompileSource,
2102
2390
  compilerOpts,
2103
- { onWarning: emitCompilerWarning }
2391
+ {
2392
+ compilerBin,
2393
+ onWarning: emitCompilerWarning
2394
+ }
2104
2395
  );
2105
2396
  componentIrCache.set(compPath, compIr);
2106
2397
  }
@@ -2113,7 +2404,13 @@ export async function build(options) {
2113
2404
 
2114
2405
  let expressionRewrite = componentExpressionRewriteCache.get(compPath);
2115
2406
  if (!expressionRewrite) {
2116
- expressionRewrite = buildComponentExpressionRewrite(compPath, componentSource, compIr, compilerOpts);
2407
+ expressionRewrite = buildComponentExpressionRewrite(
2408
+ compPath,
2409
+ componentSource,
2410
+ compIr,
2411
+ compilerOpts,
2412
+ compilerBin
2413
+ );
2117
2414
  componentExpressionRewriteCache.set(compPath, expressionRewrite);
2118
2415
  }
2119
2416
 
@@ -2136,7 +2433,10 @@ export async function build(options) {
2136
2433
  ownerPath,
2137
2434
  stripStyleBlocks(ownerSource),
2138
2435
  compilerOpts,
2139
- { onWarning: emitCompilerWarning }
2436
+ {
2437
+ compilerBin,
2438
+ onWarning: emitCompilerWarning
2439
+ }
2140
2440
  );
2141
2441
  componentIrCache.set(ownerPath, ownerIr);
2142
2442
  }
@@ -2144,7 +2444,13 @@ export async function build(options) {
2144
2444
  attrExpressionRewrite = componentExpressionRewriteCache.get(ownerPath);
2145
2445
  if (!attrExpressionRewrite) {
2146
2446
  const ownerSource = readFileSync(ownerPath, 'utf8');
2147
- attrExpressionRewrite = buildComponentExpressionRewrite(ownerPath, ownerSource, ownerIr, compilerOpts);
2447
+ attrExpressionRewrite = buildComponentExpressionRewrite(
2448
+ ownerPath,
2449
+ ownerSource,
2450
+ ownerIr,
2451
+ compilerOpts,
2452
+ compilerBin
2453
+ );
2148
2454
  componentExpressionRewriteCache.set(ownerPath, attrExpressionRewrite);
2149
2455
  }
2150
2456
 
@@ -2190,6 +2496,8 @@ export async function build(options) {
2190
2496
  pageExpressionBindingMap,
2191
2497
  pageAmbiguousExpressionMap
2192
2498
  );
2499
+ applyScopedIdentifierRewrites(pageIr, buildScopedIdentifierRewrite(pageIr));
2500
+ synthesizeSignalBackedCompiledExpressions(pageIr);
2193
2501
  normalizeExpressionBindingDependencies(pageIr);
2194
2502
 
2195
2503
  rewriteLegacyMarkupIdentifiers(pageIr);
@@ -2204,7 +2512,7 @@ export async function build(options) {
2204
2512
  }
2205
2513
 
2206
2514
  if (envelopes.length > 0) {
2207
- await runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo);
2515
+ await runBundler(envelopes, outDir, projectRoot, logger, showBundlerInfo, bundlerBin);
2208
2516
  }
2209
2517
 
2210
2518
  const assets = await collectAssets(outDir);
package/dist/index.js CHANGED
@@ -116,6 +116,15 @@ export async function cli(args, cwd) {
116
116
  const outDir = join(projectRoot, 'dist');
117
117
  const config = await loadConfig(projectRoot);
118
118
 
119
+ if (command === 'build' || command === 'dev') {
120
+ const { maybeWarnAboutZenithVersionMismatch } = await import('./version-check.js');
121
+ await maybeWarnAboutZenithVersionMismatch({
122
+ projectRoot,
123
+ logger,
124
+ command
125
+ });
126
+ }
127
+
119
128
  if (command === 'build') {
120
129
  const { build } = await import('./build.js');
121
130
  logger.build('Building…');
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const CLI_ROOT = resolve(__dirname, '..');
9
+ const localRequire = createRequire(import.meta.url);
10
+
11
+ function safeCreateRequire(projectRoot) {
12
+ if (!projectRoot) {
13
+ return localRequire;
14
+ }
15
+ try {
16
+ return createRequire(resolve(projectRoot, 'package.json'));
17
+ } catch {
18
+ return localRequire;
19
+ }
20
+ }
21
+
22
+ function safeResolve(requireFn, specifier) {
23
+ try {
24
+ return requireFn.resolve(specifier);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function resolveBinary(candidates) {
31
+ for (const candidate of candidates) {
32
+ if (existsSync(candidate)) {
33
+ return candidate;
34
+ }
35
+ }
36
+ return candidates[0] || '';
37
+ }
38
+
39
+ export function resolvePackageRoot(packageName, projectRoot = null) {
40
+ const projectRequire = safeCreateRequire(projectRoot);
41
+ const projectPath = safeResolve(projectRequire, `${packageName}/package.json`);
42
+ if (projectPath) {
43
+ return dirname(projectPath);
44
+ }
45
+
46
+ const localPath = safeResolve(localRequire, `${packageName}/package.json`);
47
+ return localPath ? dirname(localPath) : null;
48
+ }
49
+
50
+ export function readInstalledPackageVersion(packageName, projectRoot = null) {
51
+ const packageRoot = resolvePackageRoot(packageName, projectRoot);
52
+ if (!packageRoot) {
53
+ return null;
54
+ }
55
+ try {
56
+ const pkg = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
57
+ return typeof pkg.version === 'string' ? pkg.version : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export function readCliPackageVersion() {
64
+ try {
65
+ const pkg = JSON.parse(readFileSync(resolve(CLI_ROOT, 'package.json'), 'utf8'));
66
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
67
+ } catch {
68
+ return '0.0.0';
69
+ }
70
+ }
71
+
72
+ export function compilerBinCandidates(projectRoot = null) {
73
+ const candidates = [
74
+ resolve(CLI_ROOT, '../compiler/target/release/zenith-compiler'),
75
+ resolve(CLI_ROOT, '../zenith-compiler/target/release/zenith-compiler')
76
+ ];
77
+ const installedRoot = resolvePackageRoot('@zenithbuild/compiler', projectRoot);
78
+ if (installedRoot) {
79
+ candidates.unshift(resolve(installedRoot, 'target/release/zenith-compiler'));
80
+ }
81
+ return candidates;
82
+ }
83
+
84
+ export function resolveCompilerBin(projectRoot = null) {
85
+ return resolveBinary(compilerBinCandidates(projectRoot));
86
+ }
87
+
88
+ export function bundlerBinCandidates(projectRoot = null, env = process.env) {
89
+ const candidates = [];
90
+ const envBin = env?.ZENITH_BUNDLER_BIN;
91
+ if (typeof envBin === 'string' && envBin.length > 0) {
92
+ candidates.push(envBin);
93
+ }
94
+
95
+ const installedRoot = resolvePackageRoot('@zenithbuild/bundler', projectRoot);
96
+ if (installedRoot) {
97
+ candidates.push(resolve(installedRoot, 'target/release/zenith-bundler'));
98
+ }
99
+
100
+ candidates.push(
101
+ resolve(CLI_ROOT, '../bundler/target/release/zenith-bundler'),
102
+ resolve(CLI_ROOT, '../zenith-bundler/target/release/zenith-bundler')
103
+ );
104
+
105
+ return candidates;
106
+ }
107
+
108
+ export function resolveBundlerBin(projectRoot = null, env = process.env) {
109
+ return resolveBinary(bundlerBinCandidates(projectRoot, env));
110
+ }
@@ -0,0 +1,378 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import {
5
+ readCliPackageVersion,
6
+ readInstalledPackageVersion,
7
+ resolveBundlerBin
8
+ } from './toolchain-paths.js';
9
+
10
+ const PACKAGE_KEYS = [
11
+ ['core', '@zenithbuild/core'],
12
+ ['compiler', '@zenithbuild/compiler'],
13
+ ['runtime', '@zenithbuild/runtime'],
14
+ ['router', '@zenithbuild/router'],
15
+ ['bundlerPackage', '@zenithbuild/bundler']
16
+ ];
17
+
18
+ function parseVersion(version) {
19
+ const raw = String(version || '').trim();
20
+ const match = raw.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
21
+ if (!match) {
22
+ return null;
23
+ }
24
+ return {
25
+ raw,
26
+ major: Number.parseInt(match[1], 10),
27
+ minor: Number.parseInt(match[2], 10),
28
+ patch: Number.parseInt(match[3], 10),
29
+ prerelease: match[4] || '',
30
+ prereleaseParts: match[4] ? match[4].split('.') : []
31
+ };
32
+ }
33
+
34
+ function compareIdentifiers(left, right) {
35
+ const leftNumeric = /^\d+$/.test(left);
36
+ const rightNumeric = /^\d+$/.test(right);
37
+ if (leftNumeric && rightNumeric) {
38
+ return Number(left) - Number(right);
39
+ }
40
+ if (leftNumeric) {
41
+ return -1;
42
+ }
43
+ if (rightNumeric) {
44
+ return 1;
45
+ }
46
+ return left.localeCompare(right);
47
+ }
48
+
49
+ export function compareVersions(leftVersion, rightVersion) {
50
+ const left = parseVersion(leftVersion);
51
+ const right = parseVersion(rightVersion);
52
+ if (!left && !right) return 0;
53
+ if (!left) return -1;
54
+ if (!right) return 1;
55
+
56
+ const numberDelta = (
57
+ (left.major - right.major)
58
+ || (left.minor - right.minor)
59
+ || (left.patch - right.patch)
60
+ );
61
+ if (numberDelta !== 0) {
62
+ return numberDelta;
63
+ }
64
+
65
+ if (!left.prerelease && !right.prerelease) {
66
+ return 0;
67
+ }
68
+ if (!left.prerelease) {
69
+ return 1;
70
+ }
71
+ if (!right.prerelease) {
72
+ return -1;
73
+ }
74
+
75
+ const len = Math.max(left.prereleaseParts.length, right.prereleaseParts.length);
76
+ for (let index = 0; index < len; index += 1) {
77
+ const leftPart = left.prereleaseParts[index];
78
+ const rightPart = right.prereleaseParts[index];
79
+ if (leftPart === undefined) {
80
+ return -1;
81
+ }
82
+ if (rightPart === undefined) {
83
+ return 1;
84
+ }
85
+ const delta = compareIdentifiers(leftPart, rightPart);
86
+ if (delta !== 0) {
87
+ return delta;
88
+ }
89
+ }
90
+
91
+ return 0;
92
+ }
93
+
94
+ function prereleaseChannel(parsed) {
95
+ if (!parsed || !parsed.prerelease) {
96
+ return 'stable';
97
+ }
98
+ const [label = 'prerelease', train] = parsed.prereleaseParts;
99
+ if (train && /^\d+$/.test(train)) {
100
+ return `${label}.${train}`;
101
+ }
102
+ return label;
103
+ }
104
+
105
+ function classifyDifference(expectedVersion, actualVersion) {
106
+ if (!expectedVersion || !actualVersion) {
107
+ return 'unknown';
108
+ }
109
+ if (expectedVersion === actualVersion) {
110
+ return 'exact';
111
+ }
112
+ const expected = parseVersion(expectedVersion);
113
+ const actual = parseVersion(actualVersion);
114
+ if (!expected || !actual) {
115
+ return 'unknown';
116
+ }
117
+ if (expected.major !== actual.major || expected.minor !== actual.minor) {
118
+ return 'hard';
119
+ }
120
+ if (prereleaseChannel(expected) !== prereleaseChannel(actual)) {
121
+ return 'hard';
122
+ }
123
+ return 'soft';
124
+ }
125
+
126
+ function readProjectPackage(projectRoot) {
127
+ if (!projectRoot) {
128
+ return null;
129
+ }
130
+ try {
131
+ const manifestPath = resolve(projectRoot, 'package.json');
132
+ if (!existsSync(manifestPath)) {
133
+ return null;
134
+ }
135
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function buildFixCommand(projectRoot, targetVersion) {
142
+ const manifest = readProjectPackage(projectRoot);
143
+ const dependencyNames = [
144
+ '@zenithbuild/core',
145
+ '@zenithbuild/cli',
146
+ '@zenithbuild/compiler',
147
+ '@zenithbuild/runtime',
148
+ '@zenithbuild/router',
149
+ '@zenithbuild/bundler'
150
+ ];
151
+
152
+ const deps = [];
153
+ const devDeps = [];
154
+ for (const name of dependencyNames) {
155
+ if (manifest?.dependencies && Object.prototype.hasOwnProperty.call(manifest.dependencies, name)) {
156
+ deps.push(`${name}@${targetVersion}`);
157
+ continue;
158
+ }
159
+ devDeps.push(`${name}@${targetVersion}`);
160
+ }
161
+
162
+ const commands = [];
163
+ if (deps.length > 0) {
164
+ commands.push(`npm i ${deps.join(' ')}`);
165
+ }
166
+ if (devDeps.length > 0) {
167
+ commands.push(`npm i -D ${devDeps.join(' ')}`);
168
+ }
169
+ if (commands.length === 0) {
170
+ commands.push(`npm i -D ${dependencyNames.map((name) => `${name}@${targetVersion}`).join(' ')}`);
171
+ }
172
+ return commands.join(' && ');
173
+ }
174
+
175
+ function describeVersions(versions) {
176
+ const entries = [
177
+ ['cli', versions.cli],
178
+ ['project cli', versions.projectCli],
179
+ ['core', versions.core],
180
+ ['compiler', versions.compiler],
181
+ ['runtime', versions.runtime],
182
+ ['router', versions.router],
183
+ ['bundler pkg', versions.bundlerPackage],
184
+ ['bundler bin', versions.bundlerBinary]
185
+ ];
186
+ return entries
187
+ .filter(([, version]) => typeof version === 'string' && version.length > 0)
188
+ .map(([label, version]) => `${label}=${version}`)
189
+ .join(' ');
190
+ }
191
+
192
+ function summarizeIssues(issues) {
193
+ const preview = issues.slice(0, 3).map((issue) => issue.summary);
194
+ const suffix = issues.length > 3 ? ` +${issues.length - 3} more` : '';
195
+ return `${preview.join('; ')}${suffix}`;
196
+ }
197
+
198
+ function determineTargetVersion(versions) {
199
+ const candidates = [
200
+ versions.projectCli,
201
+ versions.core,
202
+ versions.compiler,
203
+ versions.runtime,
204
+ versions.router,
205
+ versions.bundlerPackage,
206
+ versions.cli
207
+ ].filter((value) => typeof value === 'string' && value.length > 0);
208
+
209
+ if (candidates.length === 0) {
210
+ return '0.0.0';
211
+ }
212
+
213
+ let highest = candidates[0];
214
+ for (const candidate of candidates.slice(1)) {
215
+ if (compareVersions(candidate, highest) > 0) {
216
+ highest = candidate;
217
+ }
218
+ }
219
+ return highest;
220
+ }
221
+
222
+ export function getBundlerVersion(bundlerBinPath) {
223
+ const path = String(bundlerBinPath || '').trim();
224
+ if (!path) {
225
+ return { version: null, path: '', rawOutput: '', ok: false };
226
+ }
227
+ const result = spawnSync(path, ['--version'], { encoding: 'utf8' });
228
+ if (result.error) {
229
+ return {
230
+ version: null,
231
+ path,
232
+ rawOutput: result.error.message,
233
+ ok: false
234
+ };
235
+ }
236
+
237
+ const rawOutput = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
238
+ const versionMatch = rawOutput.match(/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
239
+ return {
240
+ version: versionMatch ? versionMatch[1] : null,
241
+ path,
242
+ rawOutput,
243
+ ok: result.status === 0 && Boolean(versionMatch)
244
+ };
245
+ }
246
+
247
+ export function getLocalZenithVersions({ projectRoot, bundlerBinPath } = {}) {
248
+ const resolvedBundlerBin = bundlerBinPath || resolveBundlerBin(projectRoot);
249
+ const bundlerVersion = getBundlerVersion(resolvedBundlerBin);
250
+ const versions = {
251
+ cli: readCliPackageVersion(),
252
+ projectCli: readInstalledPackageVersion('@zenithbuild/cli', projectRoot),
253
+ bundlerBinary: bundlerVersion.version,
254
+ bundlerBinPath: bundlerVersion.path,
255
+ bundlerBinRawOutput: bundlerVersion.rawOutput,
256
+ targetVersion: null
257
+ };
258
+
259
+ for (const [key, packageName] of PACKAGE_KEYS) {
260
+ versions[key] = readInstalledPackageVersion(packageName, projectRoot);
261
+ }
262
+
263
+ versions.targetVersion = determineTargetVersion(versions);
264
+ return versions;
265
+ }
266
+
267
+ export function checkCompatibility(versions) {
268
+ const targetVersion = versions?.targetVersion || determineTargetVersion(versions || {});
269
+ const issues = [];
270
+ const fixCommand = buildFixCommand(versions?.projectRoot, targetVersion);
271
+
272
+ const addIssue = (code, summary, message) => {
273
+ issues.push({
274
+ code,
275
+ summary,
276
+ message,
277
+ hint: `${fixCommand} (suppress with ZENITH_SKIP_VERSION_CHECK=1)`,
278
+ fixCommand
279
+ });
280
+ };
281
+
282
+ if (versions.projectCli && versions.cli && versions.projectCli !== versions.cli) {
283
+ const severity = classifyDifference(versions.projectCli, versions.cli);
284
+ addIssue(
285
+ severity === 'hard' ? 'CLI_TRAIN_MISMATCH' : 'CLI_OUTDATED',
286
+ `cli ${versions.cli} != project ${versions.projectCli}`,
287
+ `Version mismatch detected (may break HMR/refs): executing CLI ${versions.cli} does not match project CLI ${versions.projectCli}.`
288
+ );
289
+ }
290
+
291
+ for (const [key, label] of [
292
+ ['core', 'core'],
293
+ ['compiler', 'compiler'],
294
+ ['runtime', 'runtime'],
295
+ ['router', 'router'],
296
+ ['bundlerPackage', 'bundler package']
297
+ ]) {
298
+ const actual = versions[key];
299
+ const difference = classifyDifference(targetVersion, actual);
300
+ if (difference === 'hard') {
301
+ addIssue(
302
+ 'VERSION_TRAIN_MISMATCH',
303
+ `${label} ${actual} != ${targetVersion}`,
304
+ `Version mismatch detected (may break HMR/refs): ${label} ${actual} is on a different Zenith train than ${targetVersion}.`
305
+ );
306
+ } else if (difference === 'soft') {
307
+ addIssue(
308
+ 'VERSION_OUTDATED',
309
+ `${label} ${actual} != ${targetVersion}`,
310
+ `Version mismatch detected (may break HMR/refs): ${label} ${actual} is not aligned with ${targetVersion}.`
311
+ );
312
+ }
313
+ }
314
+
315
+ const bundlerExpected = versions.bundlerPackage || targetVersion;
316
+ const bundlerDifference = classifyDifference(bundlerExpected, versions.bundlerBinary);
317
+ if (bundlerDifference === 'hard') {
318
+ addIssue(
319
+ 'BUNDLER_BINARY_MISMATCH',
320
+ `bundler bin ${versions.bundlerBinary || 'missing'} != ${bundlerExpected}`,
321
+ `Version mismatch detected (may break build/IR contracts): bundler binary ${versions.bundlerBinary || 'missing'} does not match ${bundlerExpected}.`
322
+ );
323
+ } else if (bundlerDifference === 'soft') {
324
+ addIssue(
325
+ 'BUNDLER_BINARY_OUTDATED',
326
+ `bundler bin ${versions.bundlerBinary} != ${bundlerExpected}`,
327
+ `Version mismatch detected (may break build/IR contracts): bundler binary ${versions.bundlerBinary} is not aligned with ${bundlerExpected}.`
328
+ );
329
+ }
330
+
331
+ return {
332
+ status: issues.length === 0 ? 'ok' : 'warn',
333
+ issues,
334
+ details: {
335
+ targetVersion,
336
+ versions: {
337
+ ...versions
338
+ },
339
+ summary: describeVersions(versions)
340
+ }
341
+ };
342
+ }
343
+
344
+ export async function maybeWarnAboutZenithVersionMismatch({
345
+ projectRoot,
346
+ logger,
347
+ command = 'build',
348
+ bundlerBinPath = null
349
+ } = {}) {
350
+ if (!logger || process.env.ZENITH_SKIP_VERSION_CHECK === '1') {
351
+ return { status: 'ok', issues: [], details: {} };
352
+ }
353
+
354
+ const versions = getLocalZenithVersions({ projectRoot, bundlerBinPath });
355
+ versions.projectRoot = projectRoot;
356
+ const result = checkCompatibility(versions);
357
+ const onceKey = `zenith-version-check:${describeVersions(versions)}:${result.status}`;
358
+ const verboseTag = command === 'dev' ? 'DEV' : 'BUILD';
359
+
360
+ if (result.status === 'ok') {
361
+ logger.verbose(verboseTag, `toolchain versions ok ${result.details.summary}`);
362
+ return result;
363
+ }
364
+
365
+ const primary = result.issues[0];
366
+ logger.warn(
367
+ `${primary.message} ${summarizeIssues(result.issues)}`,
368
+ {
369
+ onceKey,
370
+ hint: primary.hint
371
+ }
372
+ );
373
+ logger.verbose(verboseTag, `toolchain versions ${result.details.summary}`);
374
+ if (versions.bundlerBinPath) {
375
+ logger.verbose(verboseTag, `bundler bin path=${versions.bundlerBinPath}`);
376
+ }
377
+ return result;
378
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "dependencies": {
27
- "@zenithbuild/compiler": "0.6.2",
27
+ "@zenithbuild/compiler": "0.6.4",
28
28
  "picocolors": "^1.1.1"
29
29
  },
30
30
  "devDependencies": {