@zenithbuild/cli 0.7.4 → 0.7.7
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 +5 -3
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +48 -14
- package/dist/adapters/adapter-static-export.d.ts +5 -0
- package/dist/adapters/adapter-static-export.js +115 -0
- package/dist/adapters/adapter-types.d.ts +3 -1
- package/dist/adapters/adapter-types.js +5 -2
- package/dist/adapters/adapter-vercel.d.ts +1 -1
- package/dist/adapters/adapter-vercel.js +67 -19
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +50 -0
- package/dist/adapters/resolve-adapter.js +4 -0
- package/dist/adapters/route-rules.d.ts +5 -0
- package/dist/adapters/route-rules.js +9 -0
- package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
- package/dist/adapters/validate-hosted-resource-routes.js +13 -0
- package/dist/auth/route-auth.d.ts +6 -0
- package/dist/auth/route-auth.js +236 -0
- package/dist/build/compiler-runtime.d.ts +1 -1
- package/dist/build/compiler-runtime.js +8 -2
- package/dist/build/hoisted-code-transforms.d.ts +4 -1
- package/dist/build/hoisted-code-transforms.js +5 -3
- package/dist/build/page-ir-normalization.d.ts +1 -1
- package/dist/build/page-ir-normalization.js +33 -3
- package/dist/build/page-loop-state.js +1 -1
- package/dist/build/page-loop.js +46 -2
- package/dist/build/server-script.d.ts +2 -1
- package/dist/build/server-script.js +7 -3
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +29 -17
- package/dist/dev-build-session/helpers.d.ts +29 -0
- package/dist/dev-build-session/helpers.js +223 -0
- package/dist/dev-build-session/session.d.ts +24 -0
- package/dist/dev-build-session/session.js +204 -0
- package/dist/dev-build-session/state.d.ts +37 -0
- package/dist/dev-build-session/state.js +17 -0
- package/dist/dev-build-session.d.ts +1 -24
- package/dist/dev-build-session.js +1 -434
- package/dist/dev-server/css-state.d.ts +7 -0
- package/dist/dev-server/css-state.js +92 -0
- package/dist/dev-server/not-found.d.ts +23 -0
- package/dist/dev-server/not-found.js +129 -0
- package/dist/dev-server/request-handler.d.ts +1 -0
- package/dist/dev-server/request-handler.js +376 -0
- package/dist/dev-server/route-check.d.ts +9 -0
- package/dist/dev-server/route-check.js +100 -0
- package/dist/dev-server/watcher.d.ts +5 -0
- package/dist/dev-server/watcher.js +216 -0
- package/dist/dev-server.js +136 -883
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/images/payload.js +4 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +70 -6
- package/dist/preview/create-preview-server.d.ts +18 -0
- package/dist/preview/create-preview-server.js +71 -0
- package/dist/preview/manifest.d.ts +42 -0
- package/dist/preview/manifest.js +57 -0
- package/dist/preview/paths.d.ts +3 -0
- package/dist/preview/paths.js +38 -0
- package/dist/preview/payload.d.ts +6 -0
- package/dist/preview/payload.js +34 -0
- package/dist/preview/request-handler.d.ts +1 -0
- package/dist/preview/request-handler.js +300 -0
- package/dist/preview/server-runner.d.ts +49 -0
- package/dist/preview/server-runner.js +220 -0
- package/dist/preview/server-script-runner-template.d.ts +1 -0
- package/dist/preview/server-script-runner-template.js +425 -0
- package/dist/preview.d.ts +5 -104
- package/dist/preview.js +7 -993
- package/dist/request-body.d.ts +0 -1
- package/dist/request-body.js +0 -6
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +49 -0
- package/dist/resource-response.js +160 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.js +1 -1
- package/dist/server-contract/constants.d.ts +5 -0
- package/dist/server-contract/constants.js +5 -0
- package/dist/server-contract/export-validation.d.ts +5 -0
- package/dist/server-contract/export-validation.js +59 -0
- package/dist/server-contract/json-serializable.d.ts +1 -0
- package/dist/server-contract/json-serializable.js +52 -0
- package/dist/server-contract/resolve.d.ts +15 -0
- package/dist/server-contract/resolve.js +271 -0
- package/dist/server-contract/result-helpers.d.ts +51 -0
- package/dist/server-contract/result-helpers.js +59 -0
- package/dist/server-contract/route-result-validation.d.ts +2 -0
- package/dist/server-contract/route-result-validation.js +73 -0
- package/dist/server-contract/stage.d.ts +6 -0
- package/dist/server-contract/stage.js +22 -0
- package/dist/server-contract.d.ts +6 -54
- package/dist/server-contract.js +9 -301
- package/dist/server-error.d.ts +1 -1
- package/dist/server-error.js +2 -0
- package/dist/server-middleware.d.ts +10 -0
- package/dist/server-middleware.js +30 -0
- package/dist/server-output.d.ts +2 -1
- package/dist/server-output.js +72 -12
- package/dist/server-runtime/node-server.js +59 -7
- package/dist/server-runtime/route-render.d.ts +25 -1
- package/dist/server-runtime/route-render.js +81 -29
- package/dist/server-script-composition.d.ts +4 -2
- package/dist/server-script-composition.js +6 -3
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +3 -3
package/dist/dev-server.js
CHANGED
|
@@ -11,25 +11,20 @@
|
|
|
11
11
|
// V0: Uses Node.js http module + fs.watch. No external deps.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
import { createServer } from 'node:http';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
18
|
-
import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from './base-path.js';
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
16
|
+
import { normalizeBasePath } from './base-path.js';
|
|
19
17
|
import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
|
|
20
18
|
import { createDevBuildSession } from './dev-build-session.js';
|
|
21
19
|
import { createStartupProfiler } from './startup-profile.js';
|
|
22
20
|
import { createSilentLogger } from './ui/logger.js';
|
|
23
|
-
import { readChangeFingerprint } from './dev-watch.js';
|
|
24
21
|
import { createTrustedOriginResolver, publicHost } from './request-origin.js';
|
|
25
|
-
import { encodeRequestBodyBase64, readRequestBodyBuffer } from './request-body.js';
|
|
26
22
|
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { resolveRequestRoute } from './server/resolve-request-route.js';
|
|
23
|
+
import { loadRouteSurfaceState } from './preview.js';
|
|
24
|
+
import { syncCssStateFromBuild } from './dev-server/css-state.js';
|
|
25
|
+
import { buildNotFoundPayload, classifyNotFound, infer404Cause, looksLikeJsonRequest, renderNotFoundHtml, traceNotFound } from './dev-server/not-found.js';
|
|
26
|
+
import { createDevRequestHandler } from './dev-server/request-handler.js';
|
|
27
|
+
import { createDevWatcher } from './dev-server/watcher.js';
|
|
33
28
|
const MIME_TYPES = {
|
|
34
29
|
'.html': 'text/html',
|
|
35
30
|
'.js': 'application/javascript',
|
|
@@ -46,6 +41,12 @@ const MIME_TYPES = {
|
|
|
46
41
|
const IMAGE_RUNTIME_TAG_RE = new RegExp('<' + 'script\\b[^>]*\\bid=(["\'])zenith-image-runtime\\1[^>]*>[\\s\\S]*?<\\/' + 'script>', 'i');
|
|
47
42
|
const EVENT_STREAM_MIME = ['text', 'event-stream'].join('/');
|
|
48
43
|
const LEGACY_DEV_STREAM_PATH = ['/__zenith', '_hmr'].join('');
|
|
44
|
+
function appendSetCookieHeaders(headers, setCookies = []) {
|
|
45
|
+
if (Array.isArray(setCookies) && setCookies.length > 0) {
|
|
46
|
+
headers['Set-Cookie'] = setCookies.slice();
|
|
47
|
+
}
|
|
48
|
+
return headers;
|
|
49
|
+
}
|
|
49
50
|
// Note: V0 HMR script injection has been moved to the runtime client.
|
|
50
51
|
// This server purely hosts the V1 HMR contract endpoints.
|
|
51
52
|
/**
|
|
@@ -60,7 +61,9 @@ export async function createDevServer(options) {
|
|
|
60
61
|
const logger = providedLogger || createSilentLogger();
|
|
61
62
|
const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
|
|
62
63
|
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
63
|
-
const
|
|
64
|
+
const resolvedTarget = resolveBuildAdapter(config).target;
|
|
65
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
|
|
66
|
+
const isStaticExportTarget = resolvedTarget === 'static-export';
|
|
64
67
|
const resolvedPagesDir = resolve(pagesDir);
|
|
65
68
|
const resolvedOutDir = resolve(outDir);
|
|
66
69
|
const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
|
|
@@ -71,8 +74,6 @@ export async function createDevServer(options) {
|
|
|
71
74
|
const watchRoots = new Set([pagesParentDir]);
|
|
72
75
|
/** @type {import('http').ServerResponse[]} */
|
|
73
76
|
const hmrClients = [];
|
|
74
|
-
/** @type {import('fs').FSWatcher[]} */
|
|
75
|
-
let _watchers = [];
|
|
76
77
|
const sseHeartbeat = setInterval(() => {
|
|
77
78
|
for (const client of hmrClients) {
|
|
78
79
|
try {
|
|
@@ -83,26 +84,27 @@ export async function createDevServer(options) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
}, 15000);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const state = {
|
|
88
|
+
buildId: 0,
|
|
89
|
+
pendingBuildId: 0,
|
|
90
|
+
buildStatus: 'building',
|
|
91
|
+
lastBuildMs: Date.now(),
|
|
92
|
+
durationMs: 0,
|
|
93
|
+
buildError: null,
|
|
94
|
+
initialBuildSettled: false,
|
|
95
|
+
currentCssAssetPath: '',
|
|
96
|
+
currentCssHref: '',
|
|
97
|
+
currentCssContent: '',
|
|
98
|
+
currentRouteState: { pageRoutes: [], resourceRoutes: [] }
|
|
99
|
+
};
|
|
93
100
|
const traceEnabled = config.devTrace === true || process.env.ZENITH_DEV_TRACE === '1';
|
|
94
101
|
const verboseLogging = traceEnabled || logger.mode?.logLevel === 'verbose';
|
|
95
|
-
// Stable dev CSS endpoint points to this backing asset.
|
|
96
|
-
let currentCssAssetPath = '';
|
|
97
|
-
let currentCssHref = '';
|
|
98
|
-
let currentCssContent = '';
|
|
99
102
|
let actualPort = port;
|
|
100
103
|
const resolveServerOrigin = createTrustedOriginResolver({
|
|
101
104
|
host,
|
|
102
105
|
getPort: () => actualPort,
|
|
103
106
|
label: 'dev server'
|
|
104
107
|
});
|
|
105
|
-
let currentRoutes = [];
|
|
106
108
|
const rebuildDebounceMs = 5;
|
|
107
109
|
const queuedRebuildDebounceMs = 5;
|
|
108
110
|
function _publicHost() {
|
|
@@ -124,73 +126,8 @@ export async function createDevServer(options) {
|
|
|
124
126
|
// tracing must never break the dev server
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
|
-
function _classifyPath(pathname) {
|
|
128
|
-
if (pathname.startsWith('/__zenith_dev/events'))
|
|
129
|
-
return 'dev_events';
|
|
130
|
-
if (pathname.startsWith('/__zenith_dev/state'))
|
|
131
|
-
return 'dev_state';
|
|
132
|
-
if (pathname.startsWith('/__zenith_dev/styles.css'))
|
|
133
|
-
return 'dev_styles';
|
|
134
|
-
if (pathname.startsWith('/assets/'))
|
|
135
|
-
return 'asset';
|
|
136
|
-
return 'other';
|
|
137
|
-
}
|
|
138
129
|
function _trace404(req, url, details = {}) {
|
|
139
|
-
_trace
|
|
140
|
-
method: req.method || 'GET',
|
|
141
|
-
url: `${url.pathname}${url.search}`,
|
|
142
|
-
classify: _classifyPath(url.pathname),
|
|
143
|
-
...details
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
function _classifyNotFound(pathname) {
|
|
147
|
-
const lower = String(pathname || '').toLowerCase();
|
|
148
|
-
if (lower.startsWith('/__zenith_dev/'))
|
|
149
|
-
return 'dev_internal';
|
|
150
|
-
if (lower.startsWith('/__zenith/'))
|
|
151
|
-
return 'zenith_internal';
|
|
152
|
-
if (lower.startsWith('/_assets/')
|
|
153
|
-
|| lower.startsWith('/assets/')
|
|
154
|
-
|| lower.endsWith('.css')
|
|
155
|
-
|| lower.endsWith('.js')
|
|
156
|
-
|| lower.endsWith('.map')
|
|
157
|
-
|| lower.endsWith('.json')) {
|
|
158
|
-
return 'asset';
|
|
159
|
-
}
|
|
160
|
-
return 'page';
|
|
161
|
-
}
|
|
162
|
-
function _routeFileHint(pathname) {
|
|
163
|
-
const normalized = String(pathname || '/').replace(/\/+$/, '');
|
|
164
|
-
if (normalized === '' || normalized === '/') {
|
|
165
|
-
return 'src/pages/index.zen';
|
|
166
|
-
}
|
|
167
|
-
return `src/pages${normalized}.zen`;
|
|
168
|
-
}
|
|
169
|
-
function _infer404Cause(category) {
|
|
170
|
-
if (category === 'dev_internal' || category === 'zenith_internal') {
|
|
171
|
-
if (buildStatus === 'error') {
|
|
172
|
-
return 'initial build failed';
|
|
173
|
-
}
|
|
174
|
-
return 'unknown Zenith dev endpoint';
|
|
175
|
-
}
|
|
176
|
-
if (category === 'asset') {
|
|
177
|
-
if (buildStatus === 'error') {
|
|
178
|
-
return 'initial build failed';
|
|
179
|
-
}
|
|
180
|
-
return 'asset not emitted by latest build';
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
function _looksLikeJsonRequest(req, pathname) {
|
|
185
|
-
const accept = String(req.headers.accept || '').toLowerCase();
|
|
186
|
-
const secFetchDest = String(req.headers['sec-fetch-dest'] || '').toLowerCase();
|
|
187
|
-
if (accept.includes('application/json') || accept.includes('application/problem+json')) {
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
if (pathname.endsWith('.json')) {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
return secFetchDest === 'empty';
|
|
130
|
+
traceNotFound(_trace, req, url, details);
|
|
194
131
|
}
|
|
195
132
|
function _isBuildSwapReadError(error) {
|
|
196
133
|
const code = typeof error?.code === 'string' ? error.code : '';
|
|
@@ -202,7 +139,7 @@ export async function createDevServer(options) {
|
|
|
202
139
|
});
|
|
203
140
|
}
|
|
204
141
|
async function _readFileForRequest(filePath, encoding = undefined) {
|
|
205
|
-
const attempts = buildStatus === 'building' ? 200 : 1;
|
|
142
|
+
const attempts = state.buildStatus === 'building' ? 200 : 1;
|
|
206
143
|
let lastError = null;
|
|
207
144
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
208
145
|
try {
|
|
@@ -220,176 +157,39 @@ export async function createDevServer(options) {
|
|
|
220
157
|
}
|
|
221
158
|
throw lastError;
|
|
222
159
|
}
|
|
223
|
-
function _buildNotFoundPayload(pathname, category, cause) {
|
|
224
|
-
const hintedPath = category === 'page'
|
|
225
|
-
? (stripBasePath(pathname, configuredBasePath) || pathname)
|
|
226
|
-
: pathname;
|
|
227
|
-
const payload = {
|
|
228
|
-
kind: 'zenith_dev_not_found',
|
|
229
|
-
category,
|
|
230
|
-
requestedPath: pathname,
|
|
231
|
-
buildId,
|
|
232
|
-
buildStatus,
|
|
233
|
-
cause: cause || ''
|
|
234
|
-
};
|
|
235
|
-
if (category === 'asset') {
|
|
236
|
-
payload.hint = buildStatus === 'error'
|
|
237
|
-
? 'Dev server is running but initial build failed; fix compile errors and refresh.'
|
|
238
|
-
: 'Check emitted assets in dist and verify the requested path.';
|
|
239
|
-
if (pathname.endsWith('.css')) {
|
|
240
|
-
payload.expectedCssHref = currentCssHref || null;
|
|
241
|
-
payload.hint = buildStatus === 'error'
|
|
242
|
-
? `Dev server is running but initial build failed; expected CSS at ${currentCssHref || '<none>'}.`
|
|
243
|
-
: `Requested CSS is missing; expected current href ${currentCssHref || '<none>'}.`;
|
|
244
|
-
}
|
|
245
|
-
return payload;
|
|
246
|
-
}
|
|
247
|
-
if (category === 'dev_internal' || category === 'zenith_internal') {
|
|
248
|
-
payload.hint = buildStatus === 'error'
|
|
249
|
-
? 'Dev server is running but initial build failed; restart after fixing compile errors.'
|
|
250
|
-
: 'Check Zenith dev endpoint path and dev client version.';
|
|
251
|
-
payload.docsLink = '/docs/documentation/contracts/hmr-v1-contract.md';
|
|
252
|
-
return payload;
|
|
253
|
-
}
|
|
254
|
-
const routeFile = _routeFileHint(hintedPath);
|
|
255
|
-
payload.routeFile = routeFile;
|
|
256
|
-
payload.cause = `no route file found at ${routeFile}`;
|
|
257
|
-
payload.hint = `Create ${routeFile} or verify router manifest output.`;
|
|
258
|
-
return payload;
|
|
259
|
-
}
|
|
260
|
-
function _renderNotFoundHtml(payload) {
|
|
261
|
-
const escaped = (value) => String(value || '')
|
|
262
|
-
.replaceAll('&', '&')
|
|
263
|
-
.replaceAll('<', '<')
|
|
264
|
-
.replaceAll('>', '>');
|
|
265
|
-
const details = [
|
|
266
|
-
`Requested: ${payload.requestedPath}`,
|
|
267
|
-
`Category: ${payload.category}`,
|
|
268
|
-
`Build: ${payload.buildStatus} (id=${payload.buildId})`,
|
|
269
|
-
`Cause: ${payload.cause}`,
|
|
270
|
-
payload.expectedCssHref ? `Expected CSS href: ${payload.expectedCssHref}` : '',
|
|
271
|
-
`Hint: ${payload.hint || 'Inspect dev server output.'}`,
|
|
272
|
-
payload.docsLink ? `Docs: ${payload.docsLink}` : ''
|
|
273
|
-
].filter(Boolean).join('\n');
|
|
274
|
-
return [
|
|
275
|
-
'<!DOCTYPE html>',
|
|
276
|
-
'<html><head><meta charset="utf-8"><title>Zenith Dev 404</title></head>',
|
|
277
|
-
'<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
|
|
278
|
-
'<h1 style="margin-top:0;">Zenith Dev 404</h1>',
|
|
279
|
-
`<pre style="white-space: pre-wrap; line-height: 1.5;">${escaped(details)}</pre>`,
|
|
280
|
-
'</body></html>'
|
|
281
|
-
].join('');
|
|
282
|
-
}
|
|
283
|
-
function _pickCssAsset(assets) {
|
|
284
|
-
if (!Array.isArray(assets) || assets.length === 0) {
|
|
285
|
-
return '';
|
|
286
|
-
}
|
|
287
|
-
const cssAssets = assets
|
|
288
|
-
.filter((entry) => typeof entry === 'string' && entry.endsWith('.css'))
|
|
289
|
-
.map((entry) => entry.startsWith('/') ? entry : `/${entry}`);
|
|
290
|
-
if (cssAssets.length === 0) {
|
|
291
|
-
return '';
|
|
292
|
-
}
|
|
293
|
-
const devStable = cssAssets.find((entry) => entry.endsWith('/styles.dev.css'));
|
|
294
|
-
if (devStable) {
|
|
295
|
-
return devStable;
|
|
296
|
-
}
|
|
297
|
-
const preferred = cssAssets.find((entry) => /\/styles(\.|\/|$)/.test(entry));
|
|
298
|
-
return preferred || cssAssets[0];
|
|
299
|
-
}
|
|
300
|
-
function _delay(ms) {
|
|
301
|
-
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
302
|
-
}
|
|
303
|
-
async function _waitForCssFile(absolutePath, retries = 16, delayMs = 40) {
|
|
304
|
-
for (let i = 0; i <= retries; i++) {
|
|
305
|
-
try {
|
|
306
|
-
const info = await stat(absolutePath);
|
|
307
|
-
if (info.isFile()) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
// keep retrying
|
|
313
|
-
}
|
|
314
|
-
if (i < retries) {
|
|
315
|
-
await _delay(delayMs);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
160
|
async function _syncCssStateFromBuild(buildResult, nextBuildId) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const ready = await _waitForCssFile(absoluteCssPath);
|
|
329
|
-
if (!ready) {
|
|
330
|
-
_trace('css_sync_skipped', {
|
|
331
|
-
reason: 'css_not_ready',
|
|
332
|
-
buildId: nextBuildId,
|
|
333
|
-
cssAsset: candidate,
|
|
334
|
-
resolvedPath: absoluteCssPath
|
|
335
|
-
});
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
let cssContent = '';
|
|
339
|
-
try {
|
|
340
|
-
cssContent = await readFile(absoluteCssPath, 'utf8');
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
_trace('css_sync_skipped', {
|
|
344
|
-
reason: 'css_read_failed',
|
|
345
|
-
buildId: nextBuildId,
|
|
346
|
-
cssAsset: candidate,
|
|
347
|
-
resolvedPath: absoluteCssPath
|
|
348
|
-
});
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
if (typeof cssContent !== 'string') {
|
|
352
|
-
_trace('css_sync_skipped', {
|
|
353
|
-
reason: 'css_invalid_type',
|
|
354
|
-
buildId: nextBuildId,
|
|
355
|
-
cssAsset: candidate,
|
|
356
|
-
resolvedPath: absoluteCssPath
|
|
357
|
-
});
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
if (cssContent.length === 0) {
|
|
361
|
-
_trace('css_sync_skipped', {
|
|
362
|
-
reason: 'css_empty',
|
|
363
|
-
buildId: nextBuildId,
|
|
364
|
-
cssAsset: candidate,
|
|
365
|
-
resolvedPath: absoluteCssPath
|
|
366
|
-
});
|
|
367
|
-
cssContent = '/* zenith-dev: empty css */';
|
|
368
|
-
}
|
|
369
|
-
currentCssAssetPath = candidate;
|
|
370
|
-
currentCssContent = cssContent;
|
|
371
|
-
return true;
|
|
161
|
+
return syncCssStateFromBuild({
|
|
162
|
+
buildResult,
|
|
163
|
+
nextBuildId,
|
|
164
|
+
outDir,
|
|
165
|
+
state,
|
|
166
|
+
trace: _trace
|
|
167
|
+
});
|
|
372
168
|
}
|
|
373
169
|
async function _loadRoutesForRequests() {
|
|
374
|
-
if (buildStatus === 'building' &&
|
|
375
|
-
|
|
170
|
+
if (state.buildStatus === 'building' &&
|
|
171
|
+
((Array.isArray(state.currentRouteState.pageRoutes) && state.currentRouteState.pageRoutes.length > 0) ||
|
|
172
|
+
(Array.isArray(state.currentRouteState.resourceRoutes) && state.currentRouteState.resourceRoutes.length > 0))) {
|
|
173
|
+
return state.currentRouteState;
|
|
376
174
|
}
|
|
377
175
|
try {
|
|
378
|
-
const
|
|
379
|
-
if (Array.isArray(
|
|
380
|
-
|
|
381
|
-
|
|
176
|
+
const routeState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
177
|
+
if ((Array.isArray(routeState.pageRoutes) && routeState.pageRoutes.length > 0) ||
|
|
178
|
+
(Array.isArray(routeState.resourceRoutes) && routeState.resourceRoutes.length > 0)) {
|
|
179
|
+
state.currentRouteState = routeState;
|
|
180
|
+
return routeState;
|
|
382
181
|
}
|
|
383
182
|
}
|
|
384
183
|
catch (error) {
|
|
385
|
-
if (!(Array.isArray(
|
|
184
|
+
if (!(Array.isArray(state.currentRouteState.pageRoutes) && state.currentRouteState.pageRoutes.length > 0) &&
|
|
185
|
+
!(Array.isArray(state.currentRouteState.resourceRoutes) && state.currentRouteState.resourceRoutes.length > 0)) {
|
|
386
186
|
throw error;
|
|
387
187
|
}
|
|
388
188
|
}
|
|
389
|
-
return
|
|
189
|
+
return state.currentRouteState;
|
|
390
190
|
}
|
|
391
191
|
function _broadcastEvent(type, payload = {}) {
|
|
392
|
-
const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : buildId;
|
|
192
|
+
const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : state.buildId;
|
|
393
193
|
const data = JSON.stringify({
|
|
394
194
|
buildId: eventBuildId,
|
|
395
195
|
...payload
|
|
@@ -397,8 +197,8 @@ export async function createDevServer(options) {
|
|
|
397
197
|
_trace('sse_emit', {
|
|
398
198
|
type,
|
|
399
199
|
buildId: eventBuildId,
|
|
400
|
-
status: buildStatus,
|
|
401
|
-
cssHref: currentCssHref,
|
|
200
|
+
status: state.buildStatus,
|
|
201
|
+
cssHref: state.currentCssHref,
|
|
402
202
|
changedFiles: Array.isArray(payload.changedFiles) ? payload.changedFiles : undefined
|
|
403
203
|
});
|
|
404
204
|
for (const client of hmrClients) {
|
|
@@ -411,659 +211,112 @@ export async function createDevServer(options) {
|
|
|
411
211
|
}
|
|
412
212
|
}
|
|
413
213
|
async function _runInitialBuild() {
|
|
414
|
-
buildStatus = 'building';
|
|
415
|
-
buildError = null;
|
|
214
|
+
state.buildStatus = 'building';
|
|
215
|
+
state.buildError = null;
|
|
416
216
|
const startTime = Date.now();
|
|
417
|
-
startupProfile.emit('initial_build_start', { buildId });
|
|
217
|
+
startupProfile.emit('initial_build_start', { buildId: state.buildId });
|
|
418
218
|
try {
|
|
419
219
|
logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
|
|
420
220
|
const initialBuild = await buildSession.build();
|
|
421
|
-
const cssReady = await _syncCssStateFromBuild(initialBuild, buildId);
|
|
422
|
-
|
|
423
|
-
buildStatus = 'ok';
|
|
424
|
-
buildError = null;
|
|
425
|
-
lastBuildMs = Date.now();
|
|
426
|
-
durationMs = lastBuildMs - startTime;
|
|
427
|
-
if (cssReady && currentCssHref.length > 0) {
|
|
428
|
-
logger.css(`ready (${currentCssHref})`, {
|
|
221
|
+
const cssReady = await _syncCssStateFromBuild(initialBuild, state.buildId);
|
|
222
|
+
state.currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
223
|
+
state.buildStatus = 'ok';
|
|
224
|
+
state.buildError = null;
|
|
225
|
+
state.lastBuildMs = Date.now();
|
|
226
|
+
state.durationMs = state.lastBuildMs - startTime;
|
|
227
|
+
if (cssReady && state.currentCssHref.length > 0) {
|
|
228
|
+
logger.css(`ready (${state.currentCssHref})`, {
|
|
229
|
+
onceKey: `css-ready:${state.buildId}:${state.currentCssHref}`
|
|
230
|
+
});
|
|
429
231
|
}
|
|
430
232
|
_trace('state_snapshot', {
|
|
431
|
-
status: buildStatus,
|
|
432
|
-
buildId,
|
|
433
|
-
cssHref: currentCssHref,
|
|
434
|
-
durationMs
|
|
233
|
+
status: state.buildStatus,
|
|
234
|
+
buildId: state.buildId,
|
|
235
|
+
cssHref: state.currentCssHref,
|
|
236
|
+
durationMs: state.durationMs
|
|
435
237
|
});
|
|
436
238
|
startupProfile.emit('initial_build_complete', {
|
|
437
|
-
buildId,
|
|
438
|
-
status: buildStatus,
|
|
439
|
-
durationMs,
|
|
239
|
+
buildId: state.buildId,
|
|
240
|
+
status: state.buildStatus,
|
|
241
|
+
durationMs: state.durationMs,
|
|
440
242
|
cssReady,
|
|
441
|
-
routes: Array.isArray(
|
|
243
|
+
routes: (Array.isArray(state.currentRouteState.pageRoutes) ? state.currentRouteState.pageRoutes.length : 0) +
|
|
244
|
+
(Array.isArray(state.currentRouteState.resourceRoutes) ? state.currentRouteState.resourceRoutes.length : 0)
|
|
442
245
|
});
|
|
443
246
|
}
|
|
444
247
|
catch (err) {
|
|
445
|
-
buildStatus = 'error';
|
|
446
|
-
buildError = { message: err instanceof Error ? err.message : String(err) };
|
|
447
|
-
lastBuildMs = Date.now();
|
|
448
|
-
durationMs = lastBuildMs - startTime;
|
|
248
|
+
state.buildStatus = 'error';
|
|
249
|
+
state.buildError = { message: err instanceof Error ? err.message : String(err) };
|
|
250
|
+
state.lastBuildMs = Date.now();
|
|
251
|
+
state.durationMs = state.lastBuildMs - startTime;
|
|
449
252
|
logger.error('initial build failed', {
|
|
450
253
|
hint: 'fix the error and restart dev',
|
|
451
254
|
error: err
|
|
452
255
|
});
|
|
453
256
|
_trace('state_snapshot', {
|
|
454
|
-
status: buildStatus,
|
|
455
|
-
buildId,
|
|
456
|
-
durationMs,
|
|
457
|
-
error: buildError
|
|
257
|
+
status: state.buildStatus,
|
|
258
|
+
buildId: state.buildId,
|
|
259
|
+
durationMs: state.durationMs,
|
|
260
|
+
error: state.buildError
|
|
458
261
|
});
|
|
459
262
|
startupProfile.emit('initial_build_complete', {
|
|
460
|
-
buildId,
|
|
461
|
-
status: buildStatus,
|
|
462
|
-
durationMs,
|
|
463
|
-
error: buildError?.message || ''
|
|
263
|
+
buildId: state.buildId,
|
|
264
|
+
status: state.buildStatus,
|
|
265
|
+
durationMs: state.durationMs,
|
|
266
|
+
error: state.buildError?.message || ''
|
|
464
267
|
});
|
|
465
268
|
}
|
|
466
269
|
finally {
|
|
467
|
-
initialBuildSettled = true;
|
|
270
|
+
state.initialBuildSettled = true;
|
|
468
271
|
}
|
|
469
272
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
res.writeHead(200, {
|
|
514
|
-
'Content-Type': EVENT_STREAM_MIME,
|
|
515
|
-
'Cache-Control': 'no-store',
|
|
516
|
-
'Connection': 'keep-alive',
|
|
517
|
-
'X-Accel-Buffering': 'no'
|
|
518
|
-
});
|
|
519
|
-
res.write('retry: 1000\n');
|
|
520
|
-
res.write('event: connected\ndata: {}\n\n');
|
|
521
|
-
hmrClients.push(res);
|
|
522
|
-
req.on('close', () => {
|
|
523
|
-
const idx = hmrClients.indexOf(res);
|
|
524
|
-
if (idx !== -1)
|
|
525
|
-
hmrClients.splice(idx, 1);
|
|
526
|
-
});
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (pathname === '/__zenith_dev/styles.css') {
|
|
530
|
-
if (buildStatus === 'error') {
|
|
531
|
-
const reason = typeof buildError?.message === 'string' && buildError.message.length > 0
|
|
532
|
-
? buildError.message
|
|
533
|
-
: 'initial build failed';
|
|
534
|
-
const summary = reason.length > 280 ? `${reason.slice(0, 277)}...` : reason;
|
|
535
|
-
res.writeHead(503, {
|
|
536
|
-
'Content-Type': 'text/css; charset=utf-8',
|
|
537
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
538
|
-
'Pragma': 'no-cache',
|
|
539
|
-
'Expires': '0',
|
|
540
|
-
'X-Zenith-Dev-Error': 'build-failed'
|
|
541
|
-
});
|
|
542
|
-
res.end(`/* zenith-dev: css unavailable because build failed */\n/* cause: ${summary} */\n/* expected href: ${currentCssHref || '<none>'} */`);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
if (typeof currentCssContent === 'string' && currentCssContent.length > 0) {
|
|
546
|
-
res.writeHead(200, {
|
|
547
|
-
'Content-Type': 'text/css; charset=utf-8',
|
|
548
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
549
|
-
'Pragma': 'no-cache',
|
|
550
|
-
'Expires': '0'
|
|
551
|
-
});
|
|
552
|
-
res.end(currentCssContent);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (typeof currentCssAssetPath === 'string' && currentCssAssetPath.length > 0) {
|
|
556
|
-
try {
|
|
557
|
-
const css = await readFile(join(outDir, currentCssAssetPath), 'utf8');
|
|
558
|
-
if (typeof css === 'string' && css.length > 0) {
|
|
559
|
-
currentCssContent = css;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
// keep serving last known CSS body below
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (typeof currentCssContent !== 'string') {
|
|
567
|
-
currentCssContent = '';
|
|
568
|
-
}
|
|
569
|
-
if (currentCssContent.length === 0) {
|
|
570
|
-
currentCssContent = '/* zenith-dev: css pending */';
|
|
571
|
-
}
|
|
572
|
-
res.writeHead(200, {
|
|
573
|
-
'Content-Type': 'text/css; charset=utf-8',
|
|
574
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
575
|
-
'Pragma': 'no-cache',
|
|
576
|
-
'Expires': '0'
|
|
577
|
-
});
|
|
578
|
-
res.end(currentCssContent);
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
if (pathname === imageEndpointPath(configuredBasePath)) {
|
|
582
|
-
await handleImageRequest(req, res, {
|
|
583
|
-
requestUrl: url,
|
|
584
|
-
projectRoot,
|
|
585
|
-
config: config.images
|
|
586
|
-
});
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
if (pathname === routeCheckPath(configuredBasePath)) {
|
|
590
|
-
try {
|
|
591
|
-
if (!routeCheckEnabled) {
|
|
592
|
-
res.writeHead(501, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
593
|
-
res.end(JSON.stringify({ error: 'route_check_unsupported' }));
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (!initialBuildSettled && buildStatus === 'building') {
|
|
597
|
-
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
598
|
-
res.end(JSON.stringify({
|
|
599
|
-
error: 'initial_build_pending',
|
|
600
|
-
message: 'initial build still in progress'
|
|
601
|
-
}));
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
// Security: Require explicitly designated header to prevent public oracle probing
|
|
605
|
-
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
606
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
607
|
-
res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
const targetPath = String(url.searchParams.get('path') || '/');
|
|
611
|
-
// Security: Prevent protocol/domain injection in path
|
|
612
|
-
if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
|
|
613
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
614
|
-
res.end(JSON.stringify({ error: 'invalid_path_format' }));
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
const targetUrl = new URL(targetPath, url.origin);
|
|
618
|
-
if (targetUrl.origin !== url.origin) {
|
|
619
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
620
|
-
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
const canonicalTargetPath = stripBasePath(targetUrl.pathname, configuredBasePath);
|
|
624
|
-
if (canonicalTargetPath === null) {
|
|
625
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
626
|
-
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
630
|
-
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
631
|
-
const routes = await _loadRoutesForRequests();
|
|
632
|
-
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes);
|
|
633
|
-
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
634
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
635
|
-
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const checkResult = await executeServerRoute({
|
|
639
|
-
source: resolvedCheck.route.server_script || '',
|
|
640
|
-
sourcePath: resolvedCheck.route.server_script_path || '',
|
|
641
|
-
params: resolvedCheck.params,
|
|
642
|
-
requestUrl: targetUrl.toString(),
|
|
643
|
-
requestMethod: req.method || 'GET',
|
|
644
|
-
requestHeaders: req.headers,
|
|
645
|
-
routePattern: resolvedCheck.route.path,
|
|
646
|
-
routeFile: resolvedCheck.route.server_script_path || '',
|
|
647
|
-
routeId: resolvedCheck.route.route_id || '',
|
|
648
|
-
guardOnly: true
|
|
649
|
-
});
|
|
650
|
-
// Security: Enforce relative or same-origin redirects
|
|
651
|
-
if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
|
|
652
|
-
const loc = appLocalRedirectLocation(checkResult.result.location || '/', configuredBasePath);
|
|
653
|
-
checkResult.result.location = loc;
|
|
654
|
-
if (loc.includes('://') || loc.startsWith('//')) {
|
|
655
|
-
try {
|
|
656
|
-
const parsedLoc = new URL(loc);
|
|
657
|
-
if (parsedLoc.origin !== targetUrl.origin) {
|
|
658
|
-
checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
catch {
|
|
662
|
-
checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
res.writeHead(200, {
|
|
667
|
-
'Content-Type': 'application/json',
|
|
668
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
669
|
-
'Pragma': 'no-cache',
|
|
670
|
-
'Expires': '0',
|
|
671
|
-
'Vary': 'Cookie'
|
|
672
|
-
});
|
|
673
|
-
res.end(JSON.stringify({
|
|
674
|
-
result: sanitizeRouteResult(checkResult?.result || checkResult),
|
|
675
|
-
routeId: resolvedCheck.route.route_id || '',
|
|
676
|
-
to: targetUrl.toString()
|
|
677
|
-
}));
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
catch {
|
|
681
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
682
|
-
res.end(JSON.stringify({ error: 'route_check_failed' }));
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
let resolvedPathFor404 = null;
|
|
687
|
-
let staticRootFor404 = null;
|
|
688
|
-
try {
|
|
689
|
-
const canonicalPath = stripBasePath(pathname, configuredBasePath);
|
|
690
|
-
if (!initialBuildSettled && buildStatus === 'building') {
|
|
691
|
-
const pendingPayload = {
|
|
692
|
-
kind: 'zenith_dev_build_pending',
|
|
693
|
-
requestedPath: pathname,
|
|
694
|
-
buildId,
|
|
695
|
-
buildStatus,
|
|
696
|
-
hint: 'Initial build is still running. Retry shortly or inspect /__zenith_dev/state.'
|
|
697
|
-
};
|
|
698
|
-
if (_looksLikeJsonRequest(req, pathname)) {
|
|
699
|
-
res.writeHead(503, {
|
|
700
|
-
'Content-Type': 'application/json',
|
|
701
|
-
'Cache-Control': 'no-store'
|
|
702
|
-
});
|
|
703
|
-
res.end(JSON.stringify(pendingPayload));
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
res.writeHead(503, {
|
|
707
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
708
|
-
'Cache-Control': 'no-store'
|
|
709
|
-
});
|
|
710
|
-
res.end([
|
|
711
|
-
'<!DOCTYPE html>',
|
|
712
|
-
'<html><head><meta charset="utf-8"><title>Zenith Dev Building</title></head>',
|
|
713
|
-
'<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
|
|
714
|
-
'<h1 style="margin-top:0;">Zenith Dev Building</h1>',
|
|
715
|
-
`<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${pathname}\nStatus: initial build running\nHint: ${pendingPayload.hint}</pre>`,
|
|
716
|
-
'</body></html>'
|
|
717
|
-
].join(''));
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
if (canonicalPath === null) {
|
|
721
|
-
throw new Error('not found');
|
|
722
|
-
}
|
|
723
|
-
const requestExt = extname(canonicalPath);
|
|
724
|
-
if (requestExt && requestExt !== '.html') {
|
|
725
|
-
const assetPath = join(outDir, canonicalPath);
|
|
726
|
-
resolvedPathFor404 = assetPath;
|
|
727
|
-
staticRootFor404 = outDir;
|
|
728
|
-
const asset = await _readFileForRequest(assetPath);
|
|
729
|
-
const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
|
|
730
|
-
res.writeHead(200, { 'Content-Type': mime });
|
|
731
|
-
res.end(asset);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
const routes = await _loadRoutesForRequests();
|
|
735
|
-
const canonicalUrl = new URL(url.toString());
|
|
736
|
-
canonicalUrl.pathname = canonicalPath;
|
|
737
|
-
const resolved = resolveRequestRoute(canonicalUrl, routes);
|
|
738
|
-
let filePath = null;
|
|
739
|
-
if (resolved.matched && resolved.route) {
|
|
740
|
-
if (verboseLogging) {
|
|
741
|
-
logger.router(`${req.method || 'GET'} ${pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`);
|
|
742
|
-
}
|
|
743
|
-
const output = resolved.route.output.startsWith('/')
|
|
744
|
-
? resolved.route.output.slice(1)
|
|
745
|
-
: resolved.route.output;
|
|
746
|
-
filePath = resolveWithinDist(outDir, output);
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
filePath = toStaticFilePath(outDir, canonicalPath);
|
|
750
|
-
}
|
|
751
|
-
resolvedPathFor404 = filePath;
|
|
752
|
-
staticRootFor404 = outDir;
|
|
753
|
-
if (!filePath) {
|
|
754
|
-
throw new Error('not found');
|
|
755
|
-
}
|
|
756
|
-
let ssrPayload = null;
|
|
757
|
-
let routeExecution = null;
|
|
758
|
-
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
759
|
-
try {
|
|
760
|
-
const requestMethod = req.method || 'GET';
|
|
761
|
-
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
762
|
-
? null
|
|
763
|
-
: await readRequestBodyBuffer(req);
|
|
764
|
-
routeExecution = await executeServerRoute({
|
|
765
|
-
source: resolved.route.server_script,
|
|
766
|
-
sourcePath: resolved.route.server_script_path || '',
|
|
767
|
-
params: resolved.params,
|
|
768
|
-
requestUrl: url.toString(),
|
|
769
|
-
requestMethod,
|
|
770
|
-
requestHeaders: req.headers,
|
|
771
|
-
requestBodyBase64: encodeRequestBodyBase64(requestBodyBuffer),
|
|
772
|
-
routePattern: resolved.route.path,
|
|
773
|
-
routeFile: resolved.route.server_script_path || '',
|
|
774
|
-
routeId: resolved.route.route_id || ''
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
logServerException('dev server route execution failed', error);
|
|
779
|
-
ssrPayload = {
|
|
780
|
-
__zenith_error: {
|
|
781
|
-
status: 500,
|
|
782
|
-
code: 'LOAD_FAILED',
|
|
783
|
-
message: error instanceof Error ? error.message : String(error || '')
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
788
|
-
const routeId = resolved.route.route_id || '';
|
|
789
|
-
if (verboseLogging) {
|
|
790
|
-
logger.router(`${routeId || resolved.route.path} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
791
|
-
}
|
|
792
|
-
const result = routeExecution?.result;
|
|
793
|
-
if (result && result.kind === 'redirect') {
|
|
794
|
-
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
795
|
-
res.writeHead(status, {
|
|
796
|
-
Location: appLocalRedirectLocation(result.location, configuredBasePath),
|
|
797
|
-
'Cache-Control': 'no-store'
|
|
798
|
-
});
|
|
799
|
-
res.end('');
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
if (result && result.kind === 'deny') {
|
|
803
|
-
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
804
|
-
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
805
|
-
res.end(clientFacingRouteMessage(status, result.message));
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
809
|
-
ssrPayload = result.data;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
let content = await _readFileForRequest(filePath, 'utf8');
|
|
813
|
-
if (resolved.matched) {
|
|
814
|
-
content = await materializeImageMarkup({
|
|
815
|
-
html: content,
|
|
816
|
-
payload: buildSession.getImageRuntimePayload(),
|
|
817
|
-
imageMaterialization: Array.isArray(resolved.route?.image_materialization)
|
|
818
|
-
? resolved.route.image_materialization
|
|
819
|
-
: []
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
if (ssrPayload) {
|
|
823
|
-
content = injectSsrPayload(content, ssrPayload);
|
|
824
|
-
}
|
|
825
|
-
if (!IMAGE_RUNTIME_TAG_RE.test(content)) {
|
|
826
|
-
content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
|
|
827
|
-
}
|
|
828
|
-
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, {
|
|
829
|
-
'Content-Type': 'text/html'
|
|
830
|
-
});
|
|
831
|
-
res.end(content);
|
|
832
|
-
}
|
|
833
|
-
catch (error) {
|
|
834
|
-
const category = _classifyNotFound(pathname);
|
|
835
|
-
const cause = _infer404Cause(category);
|
|
836
|
-
const payload = _buildNotFoundPayload(pathname, category, cause);
|
|
837
|
-
if (buildStatus === 'error' && typeof buildError?.message === 'string') {
|
|
838
|
-
payload.buildError = buildError.message.length > 600
|
|
839
|
-
? `${buildError.message.slice(0, 597)}...`
|
|
840
|
-
: buildError.message;
|
|
841
|
-
}
|
|
842
|
-
const displayCategory = category === 'page' ? 'page' : 'asset';
|
|
843
|
-
logger.warn(`404 ${displayCategory}: ${pathname} (buildId=${buildId}) -> cause: ${payload.cause || cause || 'not found'}`);
|
|
844
|
-
_trace404(req, url, {
|
|
845
|
-
reason: 'not_found',
|
|
846
|
-
category,
|
|
847
|
-
cause: payload.cause || cause || 'not_found',
|
|
848
|
-
staticRoot: staticRootFor404,
|
|
849
|
-
resolvedPath: resolvedPathFor404,
|
|
850
|
-
error: error instanceof Error ? error.message : String(error || '')
|
|
851
|
-
});
|
|
852
|
-
if (_looksLikeJsonRequest(req, pathname)) {
|
|
853
|
-
res.writeHead(404, {
|
|
854
|
-
'Content-Type': 'application/json',
|
|
855
|
-
'Cache-Control': 'no-store'
|
|
856
|
-
});
|
|
857
|
-
res.end(JSON.stringify(payload));
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
res.writeHead(404, {
|
|
861
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
862
|
-
'Cache-Control': 'no-store'
|
|
863
|
-
});
|
|
864
|
-
res.end(_renderNotFoundHtml(payload));
|
|
865
|
-
}
|
|
273
|
+
state.hmrClients = hmrClients;
|
|
274
|
+
const server = createServer(createDevRequestHandler({
|
|
275
|
+
outDir,
|
|
276
|
+
projectRoot,
|
|
277
|
+
imageConfig: config.images,
|
|
278
|
+
configuredBasePath,
|
|
279
|
+
routeCheckEnabled,
|
|
280
|
+
isStaticExportTarget,
|
|
281
|
+
logger,
|
|
282
|
+
verboseLogging,
|
|
283
|
+
buildSession,
|
|
284
|
+
state,
|
|
285
|
+
serverOrigin: _serverOrigin,
|
|
286
|
+
loadRoutesForRequests: _loadRoutesForRequests,
|
|
287
|
+
readFileForRequest: _readFileForRequest,
|
|
288
|
+
trace404: _trace404,
|
|
289
|
+
looksLikeJsonRequest,
|
|
290
|
+
classifyNotFound,
|
|
291
|
+
infer404Cause,
|
|
292
|
+
buildNotFoundPayload,
|
|
293
|
+
renderNotFoundHtml,
|
|
294
|
+
appendSetCookieHeaders,
|
|
295
|
+
MIME_TYPES,
|
|
296
|
+
EVENT_STREAM_MIME,
|
|
297
|
+
LEGACY_DEV_STREAM_PATH,
|
|
298
|
+
IMAGE_RUNTIME_TAG_RE
|
|
299
|
+
}));
|
|
300
|
+
const watcherController = createDevWatcher({
|
|
301
|
+
watchRoots,
|
|
302
|
+
resolvedOutDir,
|
|
303
|
+
resolvedOutDirTmp,
|
|
304
|
+
projectRoot,
|
|
305
|
+
rebuildDebounceMs,
|
|
306
|
+
queuedRebuildDebounceMs,
|
|
307
|
+
buildSession,
|
|
308
|
+
outDir,
|
|
309
|
+
configuredBasePath,
|
|
310
|
+
logger,
|
|
311
|
+
startupProfile,
|
|
312
|
+
state,
|
|
313
|
+
syncCssStateFromBuild: _syncCssStateFromBuild,
|
|
314
|
+
broadcastEvent: _broadcastEvent,
|
|
315
|
+
trace: _trace
|
|
866
316
|
});
|
|
867
|
-
/**
|
|
868
|
-
* Broadcast HMR reload to all connected clients.
|
|
869
|
-
*/
|
|
870
|
-
function _broadcastReload() {
|
|
871
|
-
for (const client of hmrClients) {
|
|
872
|
-
try {
|
|
873
|
-
client.write('data: reload\n\n');
|
|
874
|
-
}
|
|
875
|
-
catch {
|
|
876
|
-
// client disconnected
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
let _buildDebounce = null;
|
|
881
|
-
let _queuedFiles = new Set();
|
|
882
|
-
const _lastQueuedFingerprints = new Map();
|
|
883
|
-
let _buildInFlight = false;
|
|
884
|
-
function _isWithin(parent, child) {
|
|
885
|
-
const rel = relative(parent, child);
|
|
886
|
-
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
887
|
-
}
|
|
888
|
-
function _toDisplayPath(absPath) {
|
|
889
|
-
const rel = relative(projectRoot, absPath);
|
|
890
|
-
if (rel === '')
|
|
891
|
-
return '.';
|
|
892
|
-
if (!rel.startsWith('..') && !isAbsolute(rel)) {
|
|
893
|
-
return rel;
|
|
894
|
-
}
|
|
895
|
-
return absPath;
|
|
896
|
-
}
|
|
897
|
-
function _shouldIgnoreChange(absPath) {
|
|
898
|
-
if (_isWithin(resolvedOutDir, absPath)) {
|
|
899
|
-
return true;
|
|
900
|
-
}
|
|
901
|
-
if (_isWithin(resolvedOutDirTmp, absPath)) {
|
|
902
|
-
return true;
|
|
903
|
-
}
|
|
904
|
-
const rel = relative(projectRoot, absPath);
|
|
905
|
-
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
906
|
-
return false;
|
|
907
|
-
}
|
|
908
|
-
const segments = rel.split(/[\\/]+/g);
|
|
909
|
-
return segments.includes('node_modules')
|
|
910
|
-
|| segments.includes('.git')
|
|
911
|
-
|| segments.includes('.zenith')
|
|
912
|
-
|| segments.includes('target')
|
|
913
|
-
|| segments.includes('.turbo');
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* Start watching source roots for changes.
|
|
917
|
-
*/
|
|
918
|
-
function _startWatcher() {
|
|
919
|
-
const watcherStartedAt = performance.now();
|
|
920
|
-
const triggerBuildDrain = (delayMs = rebuildDebounceMs) => {
|
|
921
|
-
if (_buildDebounce !== null) {
|
|
922
|
-
clearTimeout(_buildDebounce);
|
|
923
|
-
}
|
|
924
|
-
_buildDebounce = setTimeout(() => {
|
|
925
|
-
_buildDebounce = null;
|
|
926
|
-
void drainBuildQueue();
|
|
927
|
-
}, delayMs);
|
|
928
|
-
};
|
|
929
|
-
const drainBuildQueue = async () => {
|
|
930
|
-
if (_buildInFlight) {
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
const changedFiles = Array.from(_queuedFiles);
|
|
934
|
-
const changed = changedFiles.map(_toDisplayPath).sort();
|
|
935
|
-
if (changed.length === 0) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
_queuedFiles.clear();
|
|
939
|
-
_buildInFlight = true;
|
|
940
|
-
const cycleBuildId = pendingBuildId + 1;
|
|
941
|
-
pendingBuildId = cycleBuildId;
|
|
942
|
-
buildStatus = 'building';
|
|
943
|
-
logger.build(`Rebuild (id=${cycleBuildId})`);
|
|
944
|
-
_broadcastEvent('build_start', { buildId: cycleBuildId, changedFiles: changed });
|
|
945
|
-
const startTime = Date.now();
|
|
946
|
-
const previousCssAssetPath = currentCssAssetPath;
|
|
947
|
-
const previousCssContent = currentCssContent;
|
|
948
|
-
const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
|
|
949
|
-
try {
|
|
950
|
-
const buildResult = await buildSession.build({ changedFiles, logger });
|
|
951
|
-
const cssReady = await _syncCssStateFromBuild(buildResult, cycleBuildId);
|
|
952
|
-
if (!onlyCss) {
|
|
953
|
-
currentRoutes = await loadRouteManifest(outDir);
|
|
954
|
-
}
|
|
955
|
-
const cssChanged = cssReady && (currentCssAssetPath !== previousCssAssetPath ||
|
|
956
|
-
currentCssContent !== previousCssContent);
|
|
957
|
-
buildId = cycleBuildId;
|
|
958
|
-
buildStatus = 'ok';
|
|
959
|
-
buildError = null;
|
|
960
|
-
lastBuildMs = Date.now();
|
|
961
|
-
durationMs = lastBuildMs - startTime;
|
|
962
|
-
logger.build(`Complete (id=${cycleBuildId}, ${durationMs}ms)`);
|
|
963
|
-
_broadcastEvent('build_complete', {
|
|
964
|
-
buildId: cycleBuildId,
|
|
965
|
-
durationMs,
|
|
966
|
-
status: buildStatus,
|
|
967
|
-
cssHref: currentCssHref,
|
|
968
|
-
changedFiles: changed
|
|
969
|
-
});
|
|
970
|
-
_trace('state_snapshot', {
|
|
971
|
-
status: buildStatus,
|
|
972
|
-
buildId: cycleBuildId,
|
|
973
|
-
cssHref: currentCssHref,
|
|
974
|
-
durationMs,
|
|
975
|
-
changedFiles: changed
|
|
976
|
-
});
|
|
977
|
-
if (cssChanged && currentCssHref.length > 0) {
|
|
978
|
-
logger.css(`ready (${currentCssHref})`);
|
|
979
|
-
logger.hmr(`css_update (buildId=${cycleBuildId})`);
|
|
980
|
-
_broadcastEvent('css_update', { href: currentCssHref, changedFiles: changed });
|
|
981
|
-
}
|
|
982
|
-
if (!onlyCss) {
|
|
983
|
-
logger.hmr(`reload (buildId=${cycleBuildId})`);
|
|
984
|
-
_broadcastEvent('reload', { changedFiles: changed });
|
|
985
|
-
}
|
|
986
|
-
else {
|
|
987
|
-
_trace('css_only_update', {
|
|
988
|
-
buildId: cycleBuildId,
|
|
989
|
-
cssHref: currentCssHref,
|
|
990
|
-
cssChanged,
|
|
991
|
-
changedFiles: changed
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
catch (err) {
|
|
996
|
-
const fullError = err instanceof Error ? err.message : String(err);
|
|
997
|
-
buildStatus = 'error';
|
|
998
|
-
buildError = { message: fullError.length > 10000 ? fullError.slice(0, 10000) + '... (truncated)' : fullError };
|
|
999
|
-
lastBuildMs = Date.now();
|
|
1000
|
-
durationMs = lastBuildMs - startTime;
|
|
1001
|
-
logger.error('rebuild failed', {
|
|
1002
|
-
hint: 'fix the error and save again',
|
|
1003
|
-
error: err
|
|
1004
|
-
});
|
|
1005
|
-
_broadcastEvent('build_error', { buildId: cycleBuildId, ...buildError, changedFiles: changed });
|
|
1006
|
-
_trace('state_snapshot', {
|
|
1007
|
-
status: buildStatus,
|
|
1008
|
-
buildId,
|
|
1009
|
-
cssHref: currentCssHref,
|
|
1010
|
-
durationMs,
|
|
1011
|
-
error: buildError
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
finally {
|
|
1015
|
-
_buildInFlight = false;
|
|
1016
|
-
if (_queuedFiles.size > 0) {
|
|
1017
|
-
triggerBuildDrain(queuedRebuildDebounceMs);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
const roots = Array.from(watchRoots);
|
|
1022
|
-
for (const root of roots) {
|
|
1023
|
-
if (!existsSync(root))
|
|
1024
|
-
continue;
|
|
1025
|
-
try {
|
|
1026
|
-
const watcher = watch(root, { recursive: true }, (_eventType, filename) => {
|
|
1027
|
-
if (!filename) {
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
const changedPath = resolve(root, String(filename));
|
|
1031
|
-
if (_shouldIgnoreChange(changedPath)) {
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
void (async () => {
|
|
1035
|
-
const fingerprint = await readChangeFingerprint(changedPath);
|
|
1036
|
-
if (_lastQueuedFingerprints.get(changedPath) === fingerprint) {
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
_lastQueuedFingerprints.set(changedPath, fingerprint);
|
|
1040
|
-
_queuedFiles.add(changedPath);
|
|
1041
|
-
triggerBuildDrain();
|
|
1042
|
-
})();
|
|
1043
|
-
});
|
|
1044
|
-
_watchers.push(watcher);
|
|
1045
|
-
}
|
|
1046
|
-
catch {
|
|
1047
|
-
// fs.watch recursive may not be supported on this platform/root
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
startupProfile.emit('watcher_ready', {
|
|
1051
|
-
roots: roots.length,
|
|
1052
|
-
activeWatchers: _watchers.length,
|
|
1053
|
-
durationMs: startupProfile.roundMs(performance.now() - watcherStartedAt)
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
317
|
const closeServer = () => {
|
|
1057
318
|
clearInterval(sseHeartbeat);
|
|
1058
|
-
|
|
1059
|
-
try {
|
|
1060
|
-
watcher.close();
|
|
1061
|
-
}
|
|
1062
|
-
catch {
|
|
1063
|
-
// ignore close errors
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
_watchers = [];
|
|
319
|
+
watcherController.close();
|
|
1067
320
|
for (const client of hmrClients) {
|
|
1068
321
|
try {
|
|
1069
322
|
client.end();
|
|
@@ -1086,16 +339,16 @@ export async function createDevServer(options) {
|
|
|
1086
339
|
startupProfile.emit('server_bound', {
|
|
1087
340
|
host: _publicHost(),
|
|
1088
341
|
port: actualPort,
|
|
1089
|
-
buildStatus
|
|
342
|
+
buildStatus: state.buildStatus
|
|
1090
343
|
});
|
|
1091
344
|
_trace('server_bound', {
|
|
1092
345
|
host: _publicHost(),
|
|
1093
346
|
port: actualPort,
|
|
1094
|
-
buildStatus
|
|
347
|
+
buildStatus: state.buildStatus
|
|
1095
348
|
});
|
|
1096
349
|
try {
|
|
1097
350
|
await _runInitialBuild();
|
|
1098
|
-
|
|
351
|
+
watcherController.start();
|
|
1099
352
|
if (!settled) {
|
|
1100
353
|
settled = true;
|
|
1101
354
|
resolve({
|