@zenithbuild/cli 0.7.9 → 0.7.11

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 (37) hide show
  1. package/README.md +4 -1
  2. package/dist/build/compiler-runtime.js +3 -0
  3. package/dist/build/page-loop-state.d.ts +1 -4
  4. package/dist/build/page-loop-state.js +10 -9
  5. package/dist/build/page-loop.js +8 -7
  6. package/dist/build/server-script.js +13 -36
  7. package/dist/build/type-declarations.js +1 -54
  8. package/dist/dev-build-session/helpers.js +27 -7
  9. package/dist/dev-build-session/session.js +19 -10
  10. package/dist/dev-server/build-error-response.d.ts +21 -0
  11. package/dist/dev-server/build-error-response.js +48 -0
  12. package/dist/dev-server/port-fallback.d.ts +15 -0
  13. package/dist/dev-server/port-fallback.js +61 -0
  14. package/dist/dev-server/request-handler.js +12 -1
  15. package/dist/dev-server/watcher.js +15 -0
  16. package/dist/dev-server.d.ts +5 -2
  17. package/dist/dev-server.js +37 -45
  18. package/dist/images/remote-fetch.d.ts +12 -0
  19. package/dist/images/remote-fetch.js +257 -0
  20. package/dist/images/service.d.ts +10 -0
  21. package/dist/images/service.js +9 -46
  22. package/dist/index.js +12 -2
  23. package/dist/manifest.js +6 -1
  24. package/dist/resource-response.js +25 -8
  25. package/dist/resource-route-module.js +5 -22
  26. package/dist/route-classification.d.ts +10 -0
  27. package/dist/route-classification.js +17 -0
  28. package/dist/route-handler-export-analysis.d.ts +22 -0
  29. package/dist/route-handler-export-analysis.js +41 -0
  30. package/dist/server-output.js +6 -16
  31. package/dist/server-route-names.d.ts +2 -0
  32. package/dist/server-route-names.js +38 -0
  33. package/dist/server-runtime/node-server.js +5 -2
  34. package/dist/types/generate-env-dts.js +2 -44
  35. package/dist/types/zenith-env-dts.d.ts +4 -0
  36. package/dist/types/zenith-env-dts.js +96 -0
  37. package/package.json +3 -6
package/README.md CHANGED
@@ -83,7 +83,8 @@ Current limitations:
83
83
 
84
84
  - There is no separate `assetPrefix` knob. Assets intentionally follow `basePath`.
85
85
  - `static-export` does not expose deployed `/_zenith/image` or `/__zenith/route-check` endpoints. A plain static file server is the contract.
86
- - `vercel` and `netlify` do not yet emit a deployed `/_zenith/image` endpoint. The `node` target does.
86
+ - `node`, `vercel`, and `netlify` expose deployed `/_zenith/image` endpoints on the packaged image contract.
87
+ - Hosted `vercel` and `netlify` targets skip advisory `/__zenith/route-check`; direct HTML requests remain the server-side route boundary.
87
88
  - Image materialization is route-artifact-driven. Bundler owns final build/static HTML image materialization, while preview and server render still materialize at runtime from structured `image_materialization` metadata. No path executes page assets, and dynamic image props are currently unsupported until the compiler emits a dedicated image-props artifact.
88
89
  - There is no shipped plugin install/remove command surface in this CLI.
89
90
 
@@ -92,6 +93,8 @@ Current limitations:
92
93
  ### `zenith dev`
93
94
  Starts the development server on `localhost:3000`.
94
95
 
96
+ Changes to `zenith.config.js` or `zenith.config.ts` require restarting `zenith dev`. If the dev watcher observes a config-file edit, it reports the restart policy instead of rebuilding with stale config.
97
+
95
98
  ### `zenith build`
96
99
  Compiles and bundles your application for production.
97
100
 
@@ -91,6 +91,9 @@ export function runCompiler(filePath, stdinSource, compilerOpts = {}, compilerRu
91
91
  if (compilerOpts?.strictDomLints) {
92
92
  args.push('--strict-dom-lints');
93
93
  }
94
+ if (compilerOpts?.internalAllowUnboundMarkup) {
95
+ args.push('--internal-allow-unbound-markup');
96
+ }
94
97
  const opts = {
95
98
  encoding: 'utf8',
96
99
  maxBuffer: COMPILER_SPAWN_MAX_BUFFER
@@ -169,12 +169,9 @@ export function createPageLoopExecutionState(): {
169
169
  envelopes: never[];
170
170
  };
171
171
  export function preparePageIrForMerge(pageIr: any): void;
172
- export function applyServerEnvelopeToPageIr({ pageIr, composedServer, hasGuard, hasLoad, hasAction, entry, srcDir, sourceFile }: {
172
+ export function applyServerEnvelopeToPageIr({ pageIr, composedServer, entry, srcDir, sourceFile }: {
173
173
  pageIr: any;
174
174
  composedServer: any;
175
- hasGuard: any;
176
- hasLoad: any;
177
- hasAction: any;
178
175
  entry: any;
179
176
  srcDir: any;
180
177
  sourceFile: any;
@@ -1,4 +1,5 @@
1
1
  import { relative } from 'node:path';
2
+ import { classifyPageRoute } from '../route-classification.js';
2
3
  import { createBindingResolutionTotals, createComponentLoopPhaseTotals, createMergePhaseTotals, createOccurrenceApplyPhaseTotals, createPagePhaseTotals, createScopedRewritePhaseTotals } from './page-loop-metrics.js';
3
4
  export function createPageLoopState() {
4
5
  return {
@@ -46,22 +47,22 @@ export function preparePageIrForMerge(pageIr) {
46
47
  pageIr.hoisted.state = pageIr.hoisted.state || [];
47
48
  pageIr.hoisted.code = pageIr.hoisted.code || [];
48
49
  }
49
- export function applyServerEnvelopeToPageIr({ pageIr, composedServer, hasGuard, hasLoad, hasAction, entry, srcDir, sourceFile }) {
50
+ export function applyServerEnvelopeToPageIr({ pageIr, composedServer, entry, srcDir, sourceFile }) {
51
+ const classification = classifyPageRoute({
52
+ file: entry.file,
53
+ serverScript: composedServer.serverScript
54
+ });
50
55
  if (composedServer.serverScript) {
51
56
  const { has_action: _unusedHasAction, export_paths: _unusedExportPaths, ...serverScript } = composedServer.serverScript;
52
57
  pageIr.server_script = serverScript;
53
- pageIr.prerender = composedServer.serverScript.prerender === true;
58
+ pageIr.prerender = classification.prerender;
54
59
  if (pageIr.ssr_data === undefined) {
55
60
  pageIr.ssr_data = null;
56
61
  }
57
62
  }
58
- if (pageIr.prerender === true && (hasGuard || hasLoad || hasAction)) {
59
- throw new Error(`[zenith] Build failed for ${entry.file}: protected routes require SSR/runtime. ` +
60
- 'Cannot prerender a static route with a `guard`, `load`, or `action` function.');
61
- }
62
- pageIr.has_guard = hasGuard;
63
- pageIr.has_load = hasLoad;
64
- pageIr.has_action = hasAction;
63
+ pageIr.has_guard = classification.hasGuard;
64
+ pageIr.has_load = classification.hasLoad;
65
+ pageIr.has_action = classification.hasAction;
65
66
  pageIr.guard_module_ref = composedServer.guardPath ? relative(srcDir, composedServer.guardPath).replaceAll('\\', '/') : null;
66
67
  pageIr.load_module_ref = composedServer.loadPath ? relative(srcDir, composedServer.loadPath).replaceAll('\\', '/') : null;
67
68
  pageIr.action_module_ref = composedServer.actionPath ? relative(srcDir, composedServer.actionPath).replaceAll('\\', '/') : null;
@@ -56,13 +56,20 @@ export async function buildPageEnvelopes(input) {
56
56
  const expandedStartedAt = performance.now();
57
57
  const { expandedSource } = expandComponents(rawSource, registry, sourceFile);
58
58
  pageExpandMs = startupProfile.roundMs(performance.now() - expandedStartedAt);
59
+ const usesInternalExpandedSource = expandedSource !== rawSource;
60
+ if (usesInternalExpandedSource) {
61
+ const rawServerExtracted = extractServerScript(rawSource, sourceFile, compilerOpts);
62
+ timedRunCompiler('page', sourceFile, rawServerExtracted.source, compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
63
+ }
59
64
  const expandedServerExtractStartedAt = performance.now();
60
65
  const extractedServer = extractServerScript(expandedSource, sourceFile, compilerOpts);
61
66
  pagePhase.serverExtractMs += startupProfile.roundMs(performance.now() - expandedServerExtractStartedAt);
62
67
  const compileSource = extractedServer.source;
63
68
  await cooperativeYield();
64
69
  const pageCompileStartedAt = performance.now();
65
- let pageIr = timedRunCompiler('page', sourceFile, compileSource, compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
70
+ let pageIr = timedRunCompiler('page', sourceFile, compileSource, usesInternalExpandedSource
71
+ ? { ...compilerOpts, internalAllowUnboundMarkup: true }
72
+ : compilerOpts, { compilerToolchain: compilerBin, onWarning: emitCompilerWarning });
66
73
  pageCompileMs = startupProfile.roundMs(performance.now() - pageCompileStartedAt);
67
74
  const composedServer = composeServerScriptEnvelope({
68
75
  sourceFile,
@@ -71,15 +78,9 @@ export async function buildPageEnvelopes(input) {
71
78
  adjacentLoadPath: adjacentLoad,
72
79
  adjacentActionPath: adjacentAction
73
80
  });
74
- const hasGuard = composedServer.serverScript?.has_guard === true;
75
- const hasLoad = composedServer.serverScript?.has_load === true;
76
- const hasAction = composedServer.serverScript?.has_action === true;
77
81
  applyServerEnvelopeToPageIr({
78
82
  pageIr,
79
83
  composedServer,
80
- hasGuard,
81
- hasLoad,
82
- hasAction,
83
84
  entry,
84
85
  srcDir,
85
86
  sourceFile
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { findNextKnownComponentTag } from '../component-tag-parser.js';
3
+ import { readRouteHandlerExport } from '../route-handler-export-analysis.js';
3
4
  import { extractStaticExportPaths } from '../static-export-paths.js';
4
5
  /**
5
6
  * @param {string} source
@@ -56,40 +57,25 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
56
57
  ` Reason: <script server> block is empty\n` +
57
58
  ` Example: export const data = { ... }`);
58
59
  }
59
- const loadFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+load\s*\(([^)]*)\)/);
60
- const loadConstParenMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
61
- const loadConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+load\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
62
- const hasLoad = Boolean(loadFnMatch || loadConstParenMatch || loadConstSingleArgMatch);
63
- const loadMatchCount = Number(Boolean(loadFnMatch)) +
64
- Number(Boolean(loadConstParenMatch)) +
65
- Number(Boolean(loadConstSingleArgMatch));
66
- if (loadMatchCount > 1) {
60
+ const loadExport = readRouteHandlerExport(serverSource, 'load');
61
+ const hasLoad = loadExport.hasExport;
62
+ if (loadExport.matchCount > 1) {
67
63
  throw new Error(`Zenith server script contract violation:\n` +
68
64
  ` File: ${sourceFile}\n` +
69
65
  ` Reason: multiple load exports detected\n` +
70
66
  ` Example: keep exactly one export const load = async (ctx) => ({ ... })`);
71
67
  }
72
- const guardFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+guard\s*\(([^)]*)\)/);
73
- const guardConstParenMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
74
- const guardConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+guard\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
75
- const hasGuard = Boolean(guardFnMatch || guardConstParenMatch || guardConstSingleArgMatch);
76
- const guardMatchCount = Number(Boolean(guardFnMatch)) +
77
- Number(Boolean(guardConstParenMatch)) +
78
- Number(Boolean(guardConstSingleArgMatch));
79
- if (guardMatchCount > 1) {
68
+ const guardExport = readRouteHandlerExport(serverSource, 'guard');
69
+ const hasGuard = guardExport.hasExport;
70
+ if (guardExport.matchCount > 1) {
80
71
  throw new Error(`Zenith server script contract violation:\n` +
81
72
  ` File: ${sourceFile}\n` +
82
73
  ` Reason: multiple guard exports detected\n` +
83
74
  ` Example: keep exactly one export const guard = async (ctx) => ({ ... })`);
84
75
  }
85
- const actionFnMatch = serverSource.match(/\bexport\s+(?:async\s+)?function\s+action\s*\(([^)]*)\)/);
86
- const actionConstParenMatch = serverSource.match(/\bexport\s+const\s+action\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/);
87
- const actionConstSingleArgMatch = serverSource.match(/\bexport\s+const\s+action\s*=\s*(?:async\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
88
- const hasAction = Boolean(actionFnMatch || actionConstParenMatch || actionConstSingleArgMatch);
89
- const actionMatchCount = Number(Boolean(actionFnMatch)) +
90
- Number(Boolean(actionConstParenMatch)) +
91
- Number(Boolean(actionConstSingleArgMatch));
92
- if (actionMatchCount > 1) {
76
+ const actionExport = readRouteHandlerExport(serverSource, 'action');
77
+ const hasAction = actionExport.hasExport;
78
+ if (actionExport.matchCount > 1) {
93
79
  throw new Error(`Zenith server script contract violation:\n` +
94
80
  ` File: ${sourceFile}\n` +
95
81
  ` Reason: multiple action exports detected\n` +
@@ -112,10 +98,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
112
98
  ` Example: use only export const data or export const load`);
113
99
  }
114
100
  if (hasLoad) {
115
- const singleArg = String(loadConstSingleArgMatch?.[1] || '').trim();
116
- const paramsText = String((loadFnMatch || loadConstParenMatch)?.[1] || '').trim();
117
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
118
- if (arity !== 1) {
101
+ if (loadExport.arity !== null && loadExport.arity !== 1) {
119
102
  throw new Error(`Zenith server script contract violation:\n` +
120
103
  ` File: ${sourceFile}\n` +
121
104
  ` Reason: load(ctx) must accept exactly one argument\n` +
@@ -123,10 +106,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
123
106
  }
124
107
  }
125
108
  if (hasGuard) {
126
- const singleArg = String(guardConstSingleArgMatch?.[1] || '').trim();
127
- const paramsText = String((guardFnMatch || guardConstParenMatch)?.[1] || '').trim();
128
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
129
- if (arity !== 1) {
109
+ if (guardExport.arity !== null && guardExport.arity !== 1) {
130
110
  throw new Error(`Zenith server script contract violation:\n` +
131
111
  ` File: ${sourceFile}\n` +
132
112
  ` Reason: guard(ctx) must accept exactly one argument\n` +
@@ -134,10 +114,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
134
114
  }
135
115
  }
136
116
  if (hasAction) {
137
- const singleArg = String(actionConstSingleArgMatch?.[1] || '').trim();
138
- const paramsText = String((actionFnMatch || actionConstParenMatch)?.[1] || '').trim();
139
- const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
140
- if (arity !== 1) {
117
+ if (actionExport.arity !== null && actionExport.arity !== 1) {
141
118
  throw new Error(`Zenith server script contract violation:\n` +
142
119
  ` File: ${sourceFile}\n` +
143
120
  ` Reason: action(ctx) must accept exactly one argument\n` +
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { mkdir } from 'node:fs/promises';
3
3
  import { basename, dirname, join, resolve } from 'node:path';
4
+ import { renderZenithEnvDts } from '../types/zenith-env-dts.js';
4
5
  /**
5
6
  * @param {string} targetPath
6
7
  * @param {string} next
@@ -60,60 +61,6 @@ function renderZenithRouteDts(manifest) {
60
61
  lines.push('');
61
62
  return `${lines.join('\n')}\n`;
62
63
  }
63
- /**
64
- * @returns {string}
65
- */
66
- function renderZenithEnvDts() {
67
- return [
68
- '// Auto-generated by Zenith CLI. Do not edit manually.',
69
- 'export {};',
70
- '',
71
- 'declare global {',
72
- ' namespace Zenith {',
73
- ' type Params = Record<string, string>;',
74
- '',
75
- ' interface ErrorState {',
76
- ' status?: number;',
77
- ' code?: string;',
78
- ' message: string;',
79
- ' }',
80
- '',
81
- ' type PageData = Record<string, unknown> & { __zenith_error?: ErrorState };',
82
- '',
83
- ' interface RouteMeta {',
84
- ' id: string;',
85
- ' file: string;',
86
- ' pattern: string;',
87
- ' }',
88
- '',
89
- ' interface LoadContext {',
90
- ' params: Params;',
91
- ' url: URL;',
92
- ' request: Request;',
93
- ' route: RouteMeta;',
94
- ' }',
95
- '',
96
- ' type Load<T extends PageData = PageData> = (ctx: LoadContext) => Promise<T> | T;',
97
- '',
98
- ' interface Fragment {',
99
- ' __zenith_fragment: true;',
100
- ' mount: (anchor: Node | null) => void;',
101
- ' unmount: () => void;',
102
- ' }',
103
- '',
104
- ' type Renderable =',
105
- ' | string',
106
- ' | number',
107
- ' | boolean',
108
- ' | null',
109
- ' | undefined',
110
- ' | Renderable[]',
111
- ' | Fragment;',
112
- ' }',
113
- '}',
114
- ''
115
- ].join('\n');
116
- }
117
64
  /**
118
65
  * @param {string} pagesDir
119
66
  * @returns {string}
@@ -143,18 +143,38 @@ function collectTemplateClassSignature(envelope) {
143
143
  return [...classes].sort();
144
144
  }
145
145
  export function buildPageOnlyFastPathSignature(envelope) {
146
+ const ir = envelope.ir || {};
146
147
  return stableJson({
147
148
  route: envelope.route,
148
149
  router: envelope.router === true,
150
+ interactivityShape: {
151
+ requiresJs: envelope.requires_js === true,
152
+ signals: Array.isArray(ir.signals) ? ir.signals.length : 0,
153
+ refs: Array.isArray(ir.ref_bindings) ? ir.ref_bindings.length : 0,
154
+ events: Array.isArray(ir.event_bindings)
155
+ ? ir.event_bindings.map((entry) => [entry.index, entry.event]).sort()
156
+ : [],
157
+ markers: Array.isArray(ir.marker_bindings)
158
+ ? ir.marker_bindings.map((entry) => [entry.index, entry.kind]).sort()
159
+ : [],
160
+ componentInstances: Array.isArray(ir.component_instances) ? ir.component_instances.length : 0,
161
+ hoistedCode: Array.isArray(ir.hoisted?.code)
162
+ ? ir.hoisted.code.filter((entry) => String(entry || '').trim().length > 0).length
163
+ : 0,
164
+ hoistedState: Array.isArray(ir.hoisted?.state) ? ir.hoisted.state.length : 0,
165
+ hoistedSignals: Array.isArray(ir.hoisted?.signals) ? ir.hoisted.signals.length : 0
166
+ },
149
167
  assetContract: collectEnvelopeAssetContract(envelope),
150
168
  templateClassSignature: collectTemplateClassSignature(envelope),
151
- styleBlocks: envelope.ir.style_blocks || [],
152
- serverScript: envelope.ir.server_script || null,
153
- prerender: envelope.ir.prerender === true,
154
- hasGuard: envelope.ir.has_guard === true,
155
- hasLoad: envelope.ir.has_load === true,
156
- guardModuleRef: envelope.ir.guard_module_ref || null,
157
- loadModuleRef: envelope.ir.load_module_ref || null
169
+ styleBlocks: ir.style_blocks || [],
170
+ serverScript: ir.server_script || null,
171
+ prerender: ir.prerender === true,
172
+ hasGuard: ir.has_guard === true,
173
+ hasLoad: ir.has_load === true,
174
+ hasAction: ir.has_action === true,
175
+ guardModuleRef: ir.guard_module_ref || null,
176
+ loadModuleRef: ir.load_module_ref || null,
177
+ actionModuleRef: ir.action_module_ref || null
158
178
  });
159
179
  }
160
180
  export function buildGlobalGraphHash(envelopes) {
@@ -5,10 +5,12 @@ import { collectAssets, runBundler } from '../build/compiler-runtime.js';
5
5
  import { buildPageEnvelopes } from '../build/page-loop.js';
6
6
  import { createPageLoopCaches } from '../build/page-loop-state.js';
7
7
  import { deriveProjectRootFromPagesDir, ensureZenithTypeDeclarations } from '../build/type-declarations.js';
8
+ import { rewriteSoftNavigationHrefBasePathInHtmlFiles } from '../base-path-html.js';
8
9
  import { injectImageMaterializationIntoRouterManifest } from '../images/router-manifest.js';
9
10
  import { buildImageArtifacts } from '../images/service.js';
10
11
  import { materializeImageMarkupInHtmlFiles } from '../images/materialize.js';
11
12
  import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from '../images/payload.js';
13
+ import { writeResourceRouteManifest } from '../resource-manifest.js';
12
14
  import { createStartupProfiler } from '../startup-profile.js';
13
15
  import { resolveBuildAdapter } from '../adapters/resolve-adapter.js';
14
16
  import { supportsTargetRouteCheck } from '../route-check-support.js';
@@ -34,6 +36,9 @@ export function createDevBuildSession(options) {
34
36
  };
35
37
  ensureToolchainCompatibility(bundlerBin);
36
38
  const state = createDevBuildState(config, basePath);
39
+ function pageManifestEntries() {
40
+ return state.manifest.filter((entry) => entry?.route_kind !== 'resource');
41
+ }
37
42
  async function syncImageState(startupProfile) {
38
43
  const { manifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
39
44
  projectRoot,
@@ -50,12 +55,13 @@ export function createDevBuildSession(options) {
50
55
  }
51
56
  async function runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, bundlerOptions = {}) {
52
57
  const orderedEnvelopes = bundlerOptions.envelopesOverride
53
- || orderEnvelopes(state.manifest, resolvedPagesDir, state.envelopeByFile);
58
+ || orderEnvelopes(pageManifestEntries(), resolvedPagesDir, state.envelopeByFile);
54
59
  if (!orderedEnvelopes || orderedEnvelopes.length === 0) {
55
60
  throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
56
61
  }
57
62
  await startupProfile.measureAsync('run_bundler', () => runBundler(orderedEnvelopes, outDir, projectRoot, activeLogger, showBundlerInfo, bundlerBin, {
58
63
  routeCheck: routeCheckEnabled,
64
+ basePath,
59
65
  devStableAssets: true,
60
66
  rebuildStrategy: bundlerOptions.rebuildStrategy || 'full',
61
67
  changedRoutes: bundlerOptions.changedRoutes || [],
@@ -63,6 +69,7 @@ export function createDevBuildSession(options) {
63
69
  globalGraphHash: bundlerOptions.globalGraphHash || ''
64
70
  }), { envelopes: orderedEnvelopes.length });
65
71
  await startupProfile.measureAsync('inject_image_materialization_manifest', () => injectImageMaterializationIntoRouterManifest(outDir, orderedEnvelopes), { envelopes: orderedEnvelopes.length });
72
+ await startupProfile.measureAsync('rewrite_soft_navigation_base_path', () => rewriteSoftNavigationHrefBasePathInHtmlFiles(outDir, basePath));
66
73
  const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
67
74
  return { assets, envelopeCount: orderedEnvelopes.length };
68
75
  }
@@ -78,14 +85,15 @@ export function createDevBuildSession(options) {
78
85
  });
79
86
  state.registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
80
87
  state.manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(resolvedPagesDir));
88
+ const pageManifest = pageManifestEntries();
81
89
  await startupProfile.measureAsync('ensure_zenith_type_declarations', () => ensureZenithTypeDeclarations({
82
- manifest: state.manifest,
90
+ manifest: pageManifest,
83
91
  pagesDir: resolvedPagesDir
84
92
  }));
85
93
  state.pageLoopCaches = createPageLoopCaches();
86
94
  const emitCompilerWarning = buildCompilerWarningEmitter(activeLogger);
87
95
  const { envelopes, expressionRewriteMetrics } = await buildPageEnvelopes({
88
- manifest: state.manifest,
96
+ manifest: pageManifest,
89
97
  pagesDir: resolvedPagesDir,
90
98
  srcDir,
91
99
  registry: state.registry,
@@ -98,20 +106,21 @@ export function createDevBuildSession(options) {
98
106
  pageLoopCaches: state.pageLoopCaches
99
107
  });
100
108
  state.envelopeByFile = new Map(envelopes.map((entry) => [entry.file, entry]));
101
- state.manifestEntryByPath = toManifestEntryMap(state.manifest, resolvedPagesDir);
109
+ state.manifestEntryByPath = toManifestEntryMap(pageManifest, resolvedPagesDir);
102
110
  state.pageOnlyFastPathSignatureByFile = new Map(envelopes.map((entry) => [entry.file, buildPageOnlyFastPathSignature(entry)]));
103
111
  state.globalGraphHash = buildGlobalGraphHash(envelopes);
104
112
  const { assets } = await runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo);
113
+ await startupProfile.measureAsync('write_resource_manifest', () => writeResourceRouteManifest(outDir, state.manifest, basePath));
105
114
  await syncImageState(startupProfile);
106
115
  startupProfile.emit('build_complete', {
107
- pages: state.manifest.length,
116
+ pages: pageManifest.length,
108
117
  assets: assets.length,
109
118
  compilerTotals,
110
119
  expressionRewriteMetrics,
111
120
  strategy: 'full'
112
121
  });
113
122
  state.hasSuccessfulBuild = true;
114
- return { pages: state.manifest.length, assets, strategy: 'full' };
123
+ return { pages: pageManifest.length, assets, strategy: 'full' };
115
124
  }
116
125
  async function runBundleOnlyBuild(activeLogger, showBundlerInfo) {
117
126
  const startupProfile = createStartupProfiler('cli-build');
@@ -120,7 +129,7 @@ export function createDevBuildSession(options) {
120
129
  const { assets } = await runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, { rebuildStrategy: 'bundle-only' });
121
130
  await syncImageState(startupProfile);
122
131
  startupProfile.emit('build_complete', {
123
- pages: state.manifest.length,
132
+ pages: pageManifestEntries().length,
124
133
  assets: assets.length,
125
134
  compilerTotals,
126
135
  expressionRewriteMetrics,
@@ -155,7 +164,7 @@ export function createDevBuildSession(options) {
155
164
  state.envelopeByFile.set(envelope.file, envelope);
156
165
  state.pageOnlyFastPathSignatureByFile.set(envelope.file, buildPageOnlyFastPathSignature(envelope));
157
166
  }
158
- const orderedEnvelopes = orderEnvelopes(state.manifest, resolvedPagesDir, state.envelopeByFile);
167
+ const orderedEnvelopes = orderEnvelopes(pageManifestEntries(), resolvedPagesDir, state.envelopeByFile);
159
168
  if (!orderedEnvelopes || orderedEnvelopes.length === 0) {
160
169
  throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
161
170
  }
@@ -169,14 +178,14 @@ export function createDevBuildSession(options) {
169
178
  });
170
179
  await syncImageState(startupProfile);
171
180
  startupProfile.emit('build_complete', {
172
- pages: state.manifest.length,
181
+ pages: pageManifestEntries().length,
173
182
  assets: assets.length,
174
183
  compilerTotals,
175
184
  expressionRewriteMetrics,
176
185
  strategy: 'page-only',
177
186
  rebuiltPages: entries.length
178
187
  });
179
- return { pages: state.manifest.length, assets, strategy: 'page-only', rebuiltPages: entries.length };
188
+ return { pages: pageManifestEntries().length, assets, strategy: 'page-only', rebuiltPages: entries.length };
180
189
  }
181
190
  return {
182
191
  async build(buildOptions = {}) {
@@ -0,0 +1,21 @@
1
+ export function buildDevErrorPayload({ pathname, state }: {
2
+ pathname: any;
3
+ state: any;
4
+ }): {
5
+ kind: string;
6
+ requestedPath: any;
7
+ buildId: any;
8
+ pendingBuildId: any;
9
+ buildStatus: any;
10
+ error: {
11
+ message: string;
12
+ };
13
+ hint: string;
14
+ };
15
+ export function respondWithDevBuildError({ req, res, pathname, state, looksLikeJsonRequest }: {
16
+ req: any;
17
+ res: any;
18
+ pathname: any;
19
+ state: any;
20
+ looksLikeJsonRequest: any;
21
+ }): void;
@@ -0,0 +1,48 @@
1
+ function truncateMessage(message, limit = 1000) {
2
+ const value = String(message || 'Dev build failed.');
3
+ return value.length > limit ? `${value.slice(0, limit - 3)}...` : value;
4
+ }
5
+ function escapeHtml(value) {
6
+ return String(value)
7
+ .replace(/&/g, '&amp;')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;');
11
+ }
12
+ export function buildDevErrorPayload({ pathname, state }) {
13
+ const message = truncateMessage(state.buildError?.message || 'Dev build failed.');
14
+ return {
15
+ kind: 'zenith_dev_build_failed',
16
+ requestedPath: pathname,
17
+ buildId: state.buildId,
18
+ pendingBuildId: state.pendingBuildId,
19
+ buildStatus: state.buildStatus,
20
+ error: { message },
21
+ hint: 'Fix the build error and save again.'
22
+ };
23
+ }
24
+ export function respondWithDevBuildError({ req, res, pathname, state, looksLikeJsonRequest }) {
25
+ const payload = buildDevErrorPayload({ pathname, state });
26
+ if (looksLikeJsonRequest(req, pathname)) {
27
+ res.writeHead(503, {
28
+ 'Content-Type': 'application/json',
29
+ 'Cache-Control': 'no-store',
30
+ 'X-Zenith-Dev-Error': 'build-failed'
31
+ });
32
+ res.end(JSON.stringify(payload));
33
+ return;
34
+ }
35
+ res.writeHead(503, {
36
+ 'Content-Type': 'text/html; charset=utf-8',
37
+ 'Cache-Control': 'no-store',
38
+ 'X-Zenith-Dev-Error': 'build-failed'
39
+ });
40
+ res.end([
41
+ '<!DOCTYPE html>',
42
+ '<html><head><meta charset="utf-8"><title>Zenith Dev Build Failed</title></head>',
43
+ '<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
44
+ '<h1 style="margin-top:0;">Zenith Dev Build Failed</h1>',
45
+ `<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${escapeHtml(pathname)}\nStatus: build failed\nError: ${escapeHtml(payload.error.message)}\nHint: ${escapeHtml(payload.hint)}</pre>`,
46
+ '</body></html>'
47
+ ].join(''));
48
+ }
@@ -0,0 +1,15 @@
1
+ export function isPortConflict(error: any): any;
2
+ export function listenWithPortFallback({ server, port, host, maxAttempts }: {
3
+ server: any;
4
+ port: any;
5
+ host: any;
6
+ maxAttempts?: number | undefined;
7
+ }): Promise<{
8
+ port: any;
9
+ requestedPort: any;
10
+ portFallback: {
11
+ requestedPort: any;
12
+ occupiedPorts: any[];
13
+ finalPort: any;
14
+ } | null;
15
+ }>;
@@ -0,0 +1,61 @@
1
+ const MAX_PORT = 65535;
2
+ const DEFAULT_MAX_ATTEMPTS = 20;
3
+ export function isPortConflict(error) {
4
+ return error && error.code === 'EADDRINUSE';
5
+ }
6
+ function normalizeRequestedPort(port) {
7
+ return Number.isInteger(port) && port >= 0 ? port : 3000;
8
+ }
9
+ function waitForListen(server, port, host) {
10
+ return new Promise((resolve, reject) => {
11
+ const cleanup = () => {
12
+ server.off('error', onError);
13
+ server.off('listening', onListening);
14
+ };
15
+ const onError = (error) => {
16
+ cleanup();
17
+ reject(error);
18
+ };
19
+ const onListening = () => {
20
+ cleanup();
21
+ resolve();
22
+ };
23
+ server.once('error', onError);
24
+ server.once('listening', onListening);
25
+ server.listen(port, host);
26
+ });
27
+ }
28
+ export async function listenWithPortFallback({ server, port, host, maxAttempts = DEFAULT_MAX_ATTEMPTS }) {
29
+ const requestedPort = normalizeRequestedPort(port);
30
+ let candidatePort = requestedPort;
31
+ const occupiedPorts = [];
32
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
33
+ try {
34
+ await waitForListen(server, candidatePort, host);
35
+ const address = server.address();
36
+ const actualPort = address && typeof address === 'object' ? address.port : candidatePort;
37
+ return {
38
+ port: actualPort,
39
+ requestedPort,
40
+ portFallback: occupiedPorts.length > 0
41
+ ? {
42
+ requestedPort,
43
+ occupiedPorts: occupiedPorts.slice(),
44
+ finalPort: actualPort
45
+ }
46
+ : null
47
+ };
48
+ }
49
+ catch (error) {
50
+ if (requestedPort === 0 ||
51
+ !isPortConflict(error) ||
52
+ candidatePort >= MAX_PORT ||
53
+ attempt >= maxAttempts) {
54
+ throw error;
55
+ }
56
+ occupiedPorts.push(candidatePort);
57
+ candidatePort += 1;
58
+ }
59
+ }
60
+ throw new Error(`Unable to bind dev server after ${maxAttempts + 1} attempts`);
61
+ }
@@ -9,6 +9,7 @@ import { materializeImageMarkup } from '../images/materialize.js';
9
9
  import { injectImageRuntimePayload } from '../images/payload.js';
10
10
  import { handleImageRequest } from '../images/service.js';
11
11
  import { resolveRequestRoute } from '../server/resolve-request-route.js';
12
+ import { respondWithDevBuildError } from './build-error-response.js';
12
13
  import { handleRouteCheckRequest } from './route-check.js';
13
14
  export function createDevRequestHandler(options) {
14
15
  const { outDir, projectRoot, imageConfig, configuredBasePath, routeCheckEnabled, isStaticExportTarget, logger, verboseLogging, buildSession, state, serverOrigin, loadRoutesForRequests, readFileForRequest, trace404, looksLikeJsonRequest, classifyNotFound, infer404Cause, buildNotFoundPayload, renderNotFoundHtml, appendSetCookieHeaders, MIME_TYPES, EVENT_STREAM_MIME, LEGACY_DEV_STREAM_PATH, IMAGE_RUNTIME_TAG_RE } = options;
@@ -230,6 +231,16 @@ export function createDevRequestHandler(options) {
230
231
  res.end(descriptor.body);
231
232
  return;
232
233
  }
234
+ if (state.buildStatus === 'error' && (!requestExt || requestExt === '.html')) {
235
+ respondWithDevBuildError({
236
+ req,
237
+ res,
238
+ pathname,
239
+ state,
240
+ looksLikeJsonRequest
241
+ });
242
+ return;
243
+ }
233
244
  const resolved = resolveRequestRoute(canonicalUrl, routes.pageRoutes || []);
234
245
  let filePath = null;
235
246
  if (isStaticExportTarget) {
@@ -245,7 +256,7 @@ export function createDevRequestHandler(options) {
245
256
  filePath = resolveWithinDist(outDir, output);
246
257
  }
247
258
  else {
248
- filePath = toStaticFilePath(outDir, canonicalPath);
259
+ filePath = null;
249
260
  }
250
261
  resolvedPathFor404 = filePath;
251
262
  staticRootFor404 = outDir;