@zenithbuild/cli 0.7.2 → 0.7.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.
Files changed (54) hide show
  1. package/README.md +14 -11
  2. package/dist/adapters/adapter-netlify.js +1 -0
  3. package/dist/adapters/adapter-node.js +8 -0
  4. package/dist/adapters/adapter-vercel.js +1 -0
  5. package/dist/build/compiler-runtime.d.ts +10 -9
  6. package/dist/build/compiler-runtime.js +51 -1
  7. package/dist/build/compiler-signal-expression.d.ts +1 -0
  8. package/dist/build/compiler-signal-expression.js +155 -0
  9. package/dist/build/expression-rewrites.d.ts +1 -6
  10. package/dist/build/expression-rewrites.js +61 -65
  11. package/dist/build/page-component-loop.d.ts +3 -13
  12. package/dist/build/page-component-loop.js +21 -46
  13. package/dist/build/page-ir-normalization.d.ts +0 -8
  14. package/dist/build/page-ir-normalization.js +13 -234
  15. package/dist/build/page-loop-state.d.ts +6 -9
  16. package/dist/build/page-loop-state.js +9 -8
  17. package/dist/build/page-loop.js +27 -22
  18. package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
  19. package/dist/build/scoped-identifier-rewrite.js +28 -128
  20. package/dist/build/server-script.d.ts +2 -1
  21. package/dist/build/server-script.js +29 -3
  22. package/dist/build.js +5 -3
  23. package/dist/component-instance-ir.js +158 -52
  24. package/dist/dev-build-session.js +20 -6
  25. package/dist/dev-server.js +82 -39
  26. package/dist/framework-components/Image.zen +1 -1
  27. package/dist/images/materialization-plan.d.ts +1 -0
  28. package/dist/images/materialization-plan.js +6 -0
  29. package/dist/images/materialize.d.ts +5 -3
  30. package/dist/images/materialize.js +24 -109
  31. package/dist/images/router-manifest.d.ts +1 -0
  32. package/dist/images/router-manifest.js +49 -0
  33. package/dist/index.js +8 -2
  34. package/dist/manifest.js +3 -2
  35. package/dist/preview.d.ts +4 -3
  36. package/dist/preview.js +87 -53
  37. package/dist/request-body.d.ts +2 -0
  38. package/dist/request-body.js +13 -0
  39. package/dist/request-origin.d.ts +2 -0
  40. package/dist/request-origin.js +45 -0
  41. package/dist/route-check-support.d.ts +1 -0
  42. package/dist/route-check-support.js +4 -0
  43. package/dist/server-contract.d.ts +15 -0
  44. package/dist/server-contract.js +102 -32
  45. package/dist/server-error.d.ts +4 -0
  46. package/dist/server-error.js +34 -0
  47. package/dist/server-output.d.ts +2 -0
  48. package/dist/server-output.js +13 -0
  49. package/dist/server-runtime/node-server.js +33 -27
  50. package/dist/server-runtime/route-render.d.ts +3 -3
  51. package/dist/server-runtime/route-render.js +20 -31
  52. package/dist/server-script-composition.d.ts +11 -5
  53. package/dist/server-script-composition.js +25 -10
  54. package/package.json +6 -2
@@ -3,83 +3,189 @@ function deepClone(value) {
3
3
  return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
4
4
  }
5
5
  const cloneMetadataCache = new WeakMap();
6
- function escapeIdentifier(identifier) {
7
- return identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
- }
9
- function replaceIdentifierRefs(input, renamePlan) {
10
- let output = String(input || '');
11
- for (const entry of renamePlan) {
12
- output = output.replace(entry.pattern, entry.to);
13
- }
14
- return output;
15
- }
16
- function replaceIdentifierRefsInStatementSource(input, renameEntries, renamePlan) {
17
- const source = String(input || '');
18
- if (!source.trim()) {
19
- return source;
20
- }
6
+ function loadCloneTypeScriptApi() {
21
7
  const ts = loadTypeScriptApi();
22
8
  if (!ts) {
23
- return replaceIdentifierRefs(source, renamePlan);
9
+ throw new Error('[Zenith:Build] Deterministic component instance cloning requires the TypeScript parser.');
24
10
  }
25
- let sourceFile;
26
- try {
27
- sourceFile = ts.createSourceFile('zenith-instance-clone.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
11
+ return ts;
12
+ }
13
+ function collectBindingNames(ts, name, target) {
14
+ if (ts.isIdentifier(name)) {
15
+ target.add(name.text);
16
+ return;
28
17
  }
29
- catch {
30
- return replaceIdentifierRefs(source, renamePlan);
18
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
19
+ for (const element of name.elements) {
20
+ if (ts.isBindingElement(element)) {
21
+ collectBindingNames(ts, element.name, target);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ function collectDirectBlockBindings(ts, block, target) {
27
+ const statements = Array.isArray(block?.statements) ? block.statements : [];
28
+ for (const statement of statements) {
29
+ if (ts.isVariableStatement(statement)) {
30
+ for (const declaration of statement.declarationList.declarations) {
31
+ collectBindingNames(ts, declaration.name, target);
32
+ }
33
+ continue;
34
+ }
35
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) {
36
+ target.add(statement.name.text);
37
+ }
31
38
  }
39
+ }
40
+ function isNestedBlockScope(ts, node) {
41
+ return (ts.isBlock(node) || ts.isModuleBlock(node)) && !ts.isSourceFile(node.parent);
42
+ }
43
+ function buildScopedIdentifierTransformer(ts, renameEntries, sourceLabel) {
32
44
  const renameMap = new Map(renameEntries);
33
- const shouldRenameIdentifier = (node) => {
45
+ const shouldSkipIdentifier = (node, localBindings) => {
46
+ if (localBindings.has(node.text)) {
47
+ return true;
48
+ }
34
49
  const parent = node.parent;
35
50
  if (!parent) {
36
- return true;
51
+ return false;
37
52
  }
38
53
  if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
39
- return false;
54
+ return true;
40
55
  }
41
56
  if (ts.isPropertyAssignment(parent) && parent.name === node) {
42
- return false;
57
+ return true;
43
58
  }
44
59
  if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
45
- return false;
60
+ return true;
46
61
  }
47
62
  if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent)) {
48
- return false;
63
+ return true;
64
+ }
65
+ if (ts.isBindingElement(parent) && parent.propertyName === node) {
66
+ return true;
49
67
  }
50
- return true;
68
+ if (ts.isLabeledStatement(parent) && parent.label === node) {
69
+ return true;
70
+ }
71
+ if ((ts.isBreakStatement(parent) || ts.isContinueStatement(parent)) && parent.label === node) {
72
+ return true;
73
+ }
74
+ return false;
51
75
  };
52
- const transformer = (context) => {
53
- const visit = (node) => {
76
+ const nextScopeBindings = (node, localBindings) => {
77
+ if (ts.isSourceFile(node)) {
78
+ return localBindings;
79
+ }
80
+ if (ts.isFunctionLike(node)) {
81
+ const next = new Set(localBindings);
82
+ if (node.name && ts.isIdentifier(node.name) && !ts.isSourceFile(node.parent)) {
83
+ next.add(node.name.text);
84
+ }
85
+ for (const param of node.parameters) {
86
+ collectBindingNames(ts, param.name, next);
87
+ }
88
+ return next;
89
+ }
90
+ if (isNestedBlockScope(ts, node)) {
91
+ const next = new Set(localBindings);
92
+ collectDirectBlockBindings(ts, node, next);
93
+ return next;
94
+ }
95
+ if (ts.isCatchClause(node) && node.variableDeclaration) {
96
+ const next = new Set(localBindings);
97
+ collectBindingNames(ts, node.variableDeclaration.name, next);
98
+ return next;
99
+ }
100
+ if ((ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node))
101
+ && node.initializer
102
+ && ts.isVariableDeclarationList(node.initializer)) {
103
+ const next = new Set(localBindings);
104
+ for (const declaration of node.initializer.declarations) {
105
+ collectBindingNames(ts, declaration.name, next);
106
+ }
107
+ return next;
108
+ }
109
+ return localBindings;
110
+ };
111
+ return (context) => {
112
+ const visit = (node, localBindings) => {
113
+ const scopeBindings = nextScopeBindings(node, localBindings);
54
114
  if (ts.isShorthandPropertyAssignment(node)) {
55
115
  const rewritten = renameMap.get(node.name.text);
56
- if (typeof rewritten === 'string' && rewritten.length > 0) {
116
+ if (typeof rewritten === 'string' &&
117
+ rewritten.length > 0 &&
118
+ rewritten !== node.name.text &&
119
+ !scopeBindings.has(node.name.text)) {
57
120
  return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(node.name.text), ts.factory.createIdentifier(rewritten));
58
121
  }
59
122
  }
60
- if (ts.isIdentifier(node) && shouldRenameIdentifier(node)) {
123
+ if (ts.isIdentifier(node) && !shouldSkipIdentifier(node, scopeBindings)) {
61
124
  const rewritten = renameMap.get(node.text);
62
125
  if (typeof rewritten === 'string' && rewritten.length > 0 && rewritten !== node.text) {
63
126
  return ts.factory.createIdentifier(rewritten);
64
127
  }
65
128
  }
66
- return ts.visitEachChild(node, visit, context);
129
+ return ts.visitEachChild(node, (child) => visit(child, scopeBindings), context);
67
130
  };
68
- return (node) => ts.visitNode(node, visit);
131
+ return (node) => ts.visitNode(node, (child) => visit(child, new Set()));
69
132
  };
133
+ }
134
+ function rewriteSourceFileWithIdentifiers(sourceFile, renameEntries, sourceLabel) {
135
+ const ts = loadCloneTypeScriptApi();
136
+ const transformer = buildScopedIdentifierTransformer(ts, renameEntries, sourceLabel);
70
137
  const result = ts.transform(sourceFile, [transformer]);
71
138
  try {
72
139
  return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
73
140
  .printFile(result.transformed[0])
74
141
  .trimEnd();
75
142
  }
76
- catch {
77
- return replaceIdentifierRefs(source, renamePlan);
78
- }
79
143
  finally {
80
144
  result.dispose();
81
145
  }
82
146
  }
147
+ function rewriteIdentifierRefsInExpressionSource(input, renameEntries, sourceLabel) {
148
+ const source = String(input || '');
149
+ if (!source.trim()) {
150
+ return source;
151
+ }
152
+ const ts = loadCloneTypeScriptApi();
153
+ let sourceFile;
154
+ try {
155
+ sourceFile = ts.createSourceFile('zenith-instance-clone-expression.ts', `const __zenith_expr__ = (${source});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
156
+ }
157
+ catch {
158
+ throw new Error(`[Zenith:Build] Failed to parse component instance expression for deterministic rewriting in ${sourceLabel}.`);
159
+ }
160
+ const rewritten = rewriteSourceFileWithIdentifiers(sourceFile, renameEntries, sourceLabel);
161
+ const parsed = ts.createSourceFile('zenith-instance-clone-expression.js', rewritten, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
162
+ const statement = parsed.statements.find(ts.isVariableStatement);
163
+ const declaration = statement?.declarationList?.declarations?.[0];
164
+ const initializer = declaration?.initializer;
165
+ if (!initializer) {
166
+ throw new Error(`[Zenith:Build] Failed to extract rewritten component instance expression in ${sourceLabel}.`);
167
+ }
168
+ const root = ts.isParenthesizedExpression(initializer) ? initializer.expression : initializer;
169
+ return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
170
+ .printNode(ts.EmitHint.Unspecified, root, parsed)
171
+ .trim();
172
+ }
173
+ function rewriteIdentifierRefsInStatementSource(input, renameEntries, sourceLabel) {
174
+ const source = String(input || '');
175
+ if (!source.trim()) {
176
+ return source;
177
+ }
178
+ const ts = loadCloneTypeScriptApi();
179
+ let sourceFile;
180
+ try {
181
+ sourceFile = ts.createSourceFile('zenith-instance-clone.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
182
+ }
183
+ catch {
184
+ throw new Error(`[Zenith:Build] Failed to parse component instance statement source for deterministic rewriting in ${sourceLabel}.`);
185
+ }
186
+ ;
187
+ return rewriteSourceFileWithIdentifiers(sourceFile, renameEntries, sourceLabel);
188
+ }
83
189
  function collectRenameTargets(compIr, extractDeclaredIdentifiers) {
84
190
  const targets = new Set();
85
191
  const stateBindings = Array.isArray(compIr?.hoisted?.state) ? compIr.hoisted.state : [];
@@ -163,20 +269,16 @@ function findNextRefIndex(refBindings, raw, startIndex) {
163
269
  return -1;
164
270
  }
165
271
  export function cloneComponentIrForInstance(compIr, instanceId, extractDeclaredIdentifiers, resolveStateKeyFromBindings) {
272
+ // The only permitted downstream clone rewrite is structural instance isolation of
273
+ // component-local symbols. No string-based semantic reinterpretation is allowed.
166
274
  const suffix = `__inst${instanceId}`;
275
+ const sourceLabel = `component instance ${instanceId}`;
167
276
  const cloned = deepClone(compIr);
168
277
  const cloneMetadata = getCloneMetadata(compIr, extractDeclaredIdentifiers, resolveStateKeyFromBindings);
169
278
  const renameEntries = cloneMetadata.renameTargets.map((name) => [name, `${name}${suffix}`]);
170
279
  const renameMap = new Map(renameEntries);
171
- const renamePlan = renameEntries
172
- .sort((left, right) => right[0].length - left[0].length)
173
- .map(([from, to]) => ({
174
- from,
175
- pattern: new RegExp(`\\b${escapeIdentifier(from)}\\b`, 'g'),
176
- to
177
- }));
178
280
  if (Array.isArray(cloned?.expressions)) {
179
- cloned.expressions = cloned.expressions.map((expr) => replaceIdentifierRefs(expr, renamePlan));
281
+ cloned.expressions = cloned.expressions.map((expr) => rewriteIdentifierRefsInExpressionSource(expr, renameEntries, sourceLabel));
180
282
  }
181
283
  if (Array.isArray(cloned?.expression_bindings)) {
182
284
  cloned.expression_bindings = cloned.expression_bindings.map((binding) => {
@@ -185,22 +287,24 @@ export function cloneComponentIrForInstance(compIr, instanceId, extractDeclaredI
185
287
  }
186
288
  return {
187
289
  ...binding,
188
- literal: typeof binding.literal === 'string' ? replaceIdentifierRefs(binding.literal, renamePlan) : binding.literal,
290
+ literal: typeof binding.literal === 'string'
291
+ ? rewriteIdentifierRefsInExpressionSource(binding.literal, renameEntries, sourceLabel)
292
+ : binding.literal,
189
293
  compiled_expr: typeof binding.compiled_expr === 'string'
190
- ? replaceIdentifierRefs(binding.compiled_expr, renamePlan)
294
+ ? rewriteIdentifierRefsInExpressionSource(binding.compiled_expr, renameEntries, sourceLabel)
191
295
  : binding.compiled_expr,
192
296
  component_instance: typeof binding.component_instance === 'string'
193
- ? replaceIdentifierRefs(binding.component_instance, renamePlan)
297
+ ? rewriteIdentifierRefsInExpressionSource(binding.component_instance, renameEntries, sourceLabel)
194
298
  : binding.component_instance,
195
299
  component_binding: typeof binding.component_binding === 'string'
196
- ? replaceIdentifierRefs(binding.component_binding, renamePlan)
300
+ ? rewriteIdentifierRefsInExpressionSource(binding.component_binding, renameEntries, sourceLabel)
197
301
  : binding.component_binding
198
302
  };
199
303
  });
200
304
  }
201
305
  if (cloned?.hoisted) {
202
306
  if (Array.isArray(cloned.hoisted.declarations)) {
203
- cloned.hoisted.declarations = cloned.hoisted.declarations.map((line) => replaceIdentifierRefsInStatementSource(line, renameEntries, renamePlan));
307
+ cloned.hoisted.declarations = cloned.hoisted.declarations.map((line) => rewriteIdentifierRefsInStatementSource(line, renameEntries, sourceLabel));
204
308
  }
205
309
  if (Array.isArray(cloned.hoisted.functions)) {
206
310
  cloned.hoisted.functions = cloned.hoisted.functions.map((name) => renameMap.get(name) || name);
@@ -214,12 +318,14 @@ export function cloneComponentIrForInstance(compIr, instanceId, extractDeclaredI
214
318
  return entry;
215
319
  }
216
320
  const key = typeof entry.key === 'string' ? (renameMap.get(entry.key) || entry.key) : entry.key;
217
- const value = typeof entry.value === 'string' ? replaceIdentifierRefs(entry.value, renamePlan) : entry.value;
321
+ const value = typeof entry.value === 'string'
322
+ ? rewriteIdentifierRefsInExpressionSource(entry.value, renameEntries, sourceLabel)
323
+ : entry.value;
218
324
  return { ...entry, key, value };
219
325
  });
220
326
  }
221
327
  if (Array.isArray(cloned.hoisted.code)) {
222
- cloned.hoisted.code = cloned.hoisted.code.map((line) => replaceIdentifierRefsInStatementSource(line, renameEntries, renamePlan));
328
+ cloned.hoisted.code = cloned.hoisted.code.map((line) => rewriteIdentifierRefsInStatementSource(line, renameEntries, sourceLabel));
223
329
  }
224
330
  }
225
331
  if (Array.isArray(cloned?.ref_bindings)) {
@@ -2,14 +2,19 @@ import { existsSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { resolve } from 'node:path';
4
4
  import { buildComponentRegistry } from './resolve-components.js';
5
+ import { normalizeBasePath } from './base-path.js';
5
6
  import { collectAssets, createCompilerWarningEmitter, runBundler } from './build/compiler-runtime.js';
6
7
  import { buildPageEnvelopes } from './build/page-loop.js';
7
8
  import { createPageLoopCaches } from './build/page-loop-state.js';
8
9
  import { deriveProjectRootFromPagesDir, ensureZenithTypeDeclarations } from './build/type-declarations.js';
10
+ import { injectImageMaterializationIntoRouterManifest } from './images/router-manifest.js';
9
11
  import { buildImageArtifacts } from './images/service.js';
10
- import { createImageRuntimePayload } from './images/payload.js';
12
+ import { materializeImageMarkupInHtmlFiles } from './images/materialize.js';
13
+ import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from './images/payload.js';
11
14
  import { createStartupProfiler } from './startup-profile.js';
12
15
  import { resolveBundlerBin } from './toolchain-paths.js';
16
+ import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
17
+ import { supportsTargetRouteCheck } from './route-check-support.js';
13
18
  import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate } from './toolchain-runner.js';
14
19
  import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
15
20
  import { generateManifest } from './manifest.js';
@@ -28,9 +33,8 @@ function createCompilerTotals() {
28
33
  function createExpressionRewriteMetrics() {
29
34
  return {
30
35
  calls: 0,
31
- cacheHits: 0,
32
- cacheMisses: 0,
33
- templateCompileMs: 0
36
+ compilerOwnedBindings: 0,
37
+ ambiguousBindings: 0
34
38
  };
35
39
  }
36
40
  function toManifestEntryMap(manifest, pagesDir) {
@@ -238,6 +242,9 @@ export function createDevBuildSession(options) {
238
242
  const compilerBin = createCompilerToolchain({ projectRoot, logger });
239
243
  const bundlerBin = createBundlerToolchain({ projectRoot, logger });
240
244
  const routerEnabled = config.router === true;
245
+ const { target } = resolveBuildAdapter(config);
246
+ const basePath = normalizeBasePath(config.basePath || '/');
247
+ const routeCheckEnabled = supportsTargetRouteCheck(target);
241
248
  const compilerOpts = {
242
249
  typescriptDefault: config.typescriptDefault === true,
243
250
  experimentalEmbeddedMarkup: config.embeddedMarkupExpressions === true,
@@ -255,7 +262,7 @@ export function createDevBuildSession(options) {
255
262
  pageLoopCaches: createPageLoopCaches(),
256
263
  hasSuccessfulBuild: false,
257
264
  imageManifest: {},
258
- imageRuntimePayload: createImageRuntimePayload(config.images, {}, 'endpoint')
265
+ imageRuntimePayload: createImageRuntimePayload(config.images, {}, 'passthrough', basePath)
259
266
  };
260
267
  async function syncImageState(startupProfile) {
261
268
  const { manifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
@@ -264,7 +271,12 @@ export function createDevBuildSession(options) {
264
271
  config: config.images
265
272
  }));
266
273
  state.imageManifest = manifest;
267
- state.imageRuntimePayload = createImageRuntimePayload(config.images, manifest, 'endpoint');
274
+ state.imageRuntimePayload = createImageRuntimePayload(config.images, manifest, 'passthrough', basePath);
275
+ await startupProfile.measureAsync('materialize_image_markup', () => materializeImageMarkupInHtmlFiles({
276
+ distDir: outDir,
277
+ payload: state.imageRuntimePayload
278
+ }));
279
+ await startupProfile.measureAsync('inject_image_runtime_payload', () => injectImageRuntimePayloadIntoHtmlFiles(outDir, state.imageRuntimePayload));
268
280
  }
269
281
  async function runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, bundlerOptions = {}) {
270
282
  const orderedEnvelopes = bundlerOptions.envelopesOverride
@@ -273,12 +285,14 @@ export function createDevBuildSession(options) {
273
285
  throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
274
286
  }
275
287
  await startupProfile.measureAsync('run_bundler', () => runBundler(orderedEnvelopes, outDir, projectRoot, activeLogger, showBundlerInfo, bundlerBin, {
288
+ routeCheck: routeCheckEnabled,
276
289
  devStableAssets: true,
277
290
  rebuildStrategy: bundlerOptions.rebuildStrategy || 'full',
278
291
  changedRoutes: bundlerOptions.changedRoutes || [],
279
292
  fastPath: bundlerOptions.fastPath === true,
280
293
  globalGraphHash: bundlerOptions.globalGraphHash || ''
281
294
  }), { envelopes: orderedEnvelopes.length });
295
+ await startupProfile.measureAsync('inject_image_materialization_manifest', () => injectImageMaterializationIntoRouterManifest(outDir, orderedEnvelopes), { envelopes: orderedEnvelopes.length });
282
296
  const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
283
297
  return { assets, envelopeCount: orderedEnvelopes.length };
284
298
  }