@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.
- package/README.md +4 -1
- package/dist/build/compiler-runtime.js +3 -0
- package/dist/build/page-loop-state.d.ts +1 -4
- package/dist/build/page-loop-state.js +10 -9
- package/dist/build/page-loop.js +8 -7
- package/dist/build/server-script.js +13 -36
- package/dist/build/type-declarations.js +1 -54
- package/dist/dev-build-session/helpers.js +27 -7
- package/dist/dev-build-session/session.js +19 -10
- package/dist/dev-server/build-error-response.d.ts +21 -0
- package/dist/dev-server/build-error-response.js +48 -0
- package/dist/dev-server/port-fallback.d.ts +15 -0
- package/dist/dev-server/port-fallback.js +61 -0
- package/dist/dev-server/request-handler.js +12 -1
- package/dist/dev-server/watcher.js +15 -0
- package/dist/dev-server.d.ts +5 -2
- package/dist/dev-server.js +37 -45
- package/dist/images/remote-fetch.d.ts +12 -0
- package/dist/images/remote-fetch.js +257 -0
- package/dist/images/service.d.ts +10 -0
- package/dist/images/service.js +9 -46
- package/dist/index.js +12 -2
- package/dist/manifest.js +6 -1
- package/dist/resource-response.js +25 -8
- package/dist/resource-route-module.js +5 -22
- package/dist/route-classification.d.ts +10 -0
- package/dist/route-classification.js +17 -0
- package/dist/route-handler-export-analysis.d.ts +22 -0
- package/dist/route-handler-export-analysis.js +41 -0
- package/dist/server-output.js +6 -16
- package/dist/server-route-names.d.ts +2 -0
- package/dist/server-route-names.js +38 -0
- package/dist/server-runtime/node-server.js +5 -2
- package/dist/types/generate-env-dts.js +2 -44
- package/dist/types/zenith-env-dts.d.ts +4 -0
- package/dist/types/zenith-env-dts.js +96 -0
- 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
|
|
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,
|
|
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,
|
|
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 =
|
|
58
|
+
pageIr.prerender = classification.prerender;
|
|
54
59
|
if (pageIr.ssr_data === undefined) {
|
|
55
60
|
pageIr.ssr_data = null;
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
package/dist/build/page-loop.js
CHANGED
|
@@ -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,
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
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
|
|
73
|
-
const
|
|
74
|
-
|
|
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
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
152
|
-
serverScript:
|
|
153
|
-
prerender:
|
|
154
|
-
hasGuard:
|
|
155
|
-
hasLoad:
|
|
156
|
-
|
|
157
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"');
|
|
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 =
|
|
259
|
+
filePath = null;
|
|
249
260
|
}
|
|
250
261
|
resolvedPathFor404 = filePath;
|
|
251
262
|
staticRootFor404 = outDir;
|