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