@zenithbuild/cli 0.6.13 → 0.7.0
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/build/compiler-runtime.d.ts +59 -0
- package/dist/build/compiler-runtime.js +277 -0
- package/dist/build/expression-rewrites.d.ts +88 -0
- package/dist/build/expression-rewrites.js +372 -0
- package/dist/build/hoisted-code-transforms.d.ts +44 -0
- package/dist/build/hoisted-code-transforms.js +316 -0
- package/dist/build/merge-component-ir.d.ts +16 -0
- package/dist/build/merge-component-ir.js +257 -0
- package/dist/build/page-component-loop.d.ts +92 -0
- package/dist/build/page-component-loop.js +257 -0
- package/dist/build/page-ir-normalization.d.ts +23 -0
- package/dist/build/page-ir-normalization.js +370 -0
- package/dist/build/page-loop-metrics.d.ts +100 -0
- package/dist/build/page-loop-metrics.js +131 -0
- package/dist/build/page-loop-state.d.ts +261 -0
- package/dist/build/page-loop-state.js +92 -0
- package/dist/build/page-loop.d.ts +33 -0
- package/dist/build/page-loop.js +217 -0
- package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
- package/dist/build/scoped-identifier-rewrite.js +245 -0
- package/dist/build/server-script.d.ts +41 -0
- package/dist/build/server-script.js +210 -0
- package/dist/build/type-declarations.d.ts +16 -0
- package/dist/build/type-declarations.js +158 -0
- package/dist/build/typescript-expression-utils.d.ts +23 -0
- package/dist/build/typescript-expression-utils.js +272 -0
- package/dist/build.d.ts +10 -18
- package/dist/build.js +74 -2261
- package/dist/component-instance-ir.d.ts +2 -2
- package/dist/component-instance-ir.js +146 -39
- package/dist/component-occurrences.js +63 -15
- package/dist/config.d.ts +66 -0
- package/dist/config.js +86 -0
- package/dist/debug-script.d.ts +1 -0
- package/dist/debug-script.js +8 -0
- package/dist/dev-build-session.d.ts +23 -0
- package/dist/dev-build-session.js +421 -0
- package/dist/dev-server.js +405 -58
- package/dist/framework-components/Image.zen +316 -0
- package/dist/images/materialize.d.ts +17 -0
- package/dist/images/materialize.js +200 -0
- package/dist/images/payload.d.ts +18 -0
- package/dist/images/payload.js +65 -0
- package/dist/images/runtime.d.ts +4 -0
- package/dist/images/runtime.js +254 -0
- package/dist/images/service.d.ts +4 -0
- package/dist/images/service.js +302 -0
- package/dist/images/shared.d.ts +58 -0
- package/dist/images/shared.js +306 -0
- package/dist/index.js +2 -17
- package/dist/manifest.js +45 -0
- package/dist/preview.d.ts +4 -1
- package/dist/preview.js +59 -6
- package/dist/resolve-components.js +20 -3
- package/dist/server-contract.js +3 -2
- package/dist/server-script-composition.d.ts +39 -0
- package/dist/server-script-composition.js +133 -0
- package/dist/startup-profile.d.ts +10 -0
- package/dist/startup-profile.js +62 -0
- package/dist/toolchain-paths.d.ts +1 -0
- package/dist/toolchain-paths.js +31 -0
- package/dist/version-check.d.ts +2 -1
- package/dist/version-check.js +12 -5
- package/package.json +5 -4
package/dist/dev-server.js
CHANGED
|
@@ -13,11 +13,16 @@
|
|
|
13
13
|
import { createServer } from 'node:http';
|
|
14
14
|
import { existsSync, watch } from 'node:fs';
|
|
15
15
|
import { readFile, stat } from 'node:fs/promises';
|
|
16
|
+
import { performance } from 'node:perf_hooks';
|
|
16
17
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
17
|
-
import {
|
|
18
|
+
import { createDevBuildSession } from './dev-build-session.js';
|
|
19
|
+
import { createStartupProfiler } from './startup-profile.js';
|
|
18
20
|
import { createSilentLogger } from './ui/logger.js';
|
|
19
21
|
import { readChangeFingerprint } from './dev-watch.js';
|
|
20
|
-
import { executeServerRoute, injectSsrPayload, loadRouteManifest, resolveWithinDist, toStaticFilePath } from './preview.js';
|
|
22
|
+
import { defaultRouteDenyMessage, executeServerRoute, injectSsrPayload, loadRouteManifest, resolveWithinDist, toStaticFilePath } from './preview.js';
|
|
23
|
+
import { materializeImageMarkup } from './images/materialize.js';
|
|
24
|
+
import { injectImageRuntimePayload } from './images/payload.js';
|
|
25
|
+
import { handleImageRequest } from './images/service.js';
|
|
21
26
|
import { resolveRequestRoute } from './server/resolve-request-route.js';
|
|
22
27
|
const MIME_TYPES = {
|
|
23
28
|
'.html': 'text/html',
|
|
@@ -25,8 +30,12 @@ const MIME_TYPES = {
|
|
|
25
30
|
'.css': 'text/css',
|
|
26
31
|
'.json': 'application/json',
|
|
27
32
|
'.png': 'image/png',
|
|
33
|
+
'.jpeg': 'image/jpeg',
|
|
28
34
|
'.jpg': 'image/jpeg',
|
|
29
|
-
'.svg': 'image/svg+xml'
|
|
35
|
+
'.svg': 'image/svg+xml',
|
|
36
|
+
'.webp': 'image/webp',
|
|
37
|
+
'.avif': 'image/avif',
|
|
38
|
+
'.gif': 'image/gif'
|
|
30
39
|
};
|
|
31
40
|
// Note: V0 HMR script injection has been moved to the runtime client.
|
|
32
41
|
// This server purely hosts the V1 HMR contract endpoints.
|
|
@@ -37,8 +46,10 @@ const MIME_TYPES = {
|
|
|
37
46
|
* @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
|
|
38
47
|
*/
|
|
39
48
|
export async function createDevServer(options) {
|
|
49
|
+
const startupProfile = createStartupProfiler('cli-dev-server');
|
|
40
50
|
const { pagesDir, outDir, port = 3000, host = '127.0.0.1', config = {}, logger: providedLogger = null } = options;
|
|
41
51
|
const logger = providedLogger || createSilentLogger();
|
|
52
|
+
const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
|
|
42
53
|
const resolvedPagesDir = resolve(pagesDir);
|
|
43
54
|
const resolvedOutDir = resolve(outDir);
|
|
44
55
|
const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
|
|
@@ -63,10 +74,11 @@ export async function createDevServer(options) {
|
|
|
63
74
|
}, 15000);
|
|
64
75
|
let buildId = 0;
|
|
65
76
|
let pendingBuildId = 0;
|
|
66
|
-
let buildStatus = '
|
|
77
|
+
let buildStatus = 'building'; // 'ok' | 'error' | 'building'
|
|
67
78
|
let lastBuildMs = Date.now();
|
|
68
79
|
let durationMs = 0;
|
|
69
80
|
let buildError = null;
|
|
81
|
+
let initialBuildSettled = false;
|
|
70
82
|
const traceEnabled = config.devTrace === true || process.env.ZENITH_DEV_TRACE === '1';
|
|
71
83
|
const verboseLogging = traceEnabled || logger.mode?.logLevel === 'verbose';
|
|
72
84
|
// Stable dev CSS endpoint points to this backing asset.
|
|
@@ -74,6 +86,9 @@ export async function createDevServer(options) {
|
|
|
74
86
|
let currentCssHref = '';
|
|
75
87
|
let currentCssContent = '';
|
|
76
88
|
let actualPort = port;
|
|
89
|
+
let currentRoutes = [];
|
|
90
|
+
const rebuildDebounceMs = 5;
|
|
91
|
+
const queuedRebuildDebounceMs = 5;
|
|
77
92
|
function _publicHost() {
|
|
78
93
|
if (host === '0.0.0.0' || host === '::') {
|
|
79
94
|
return '127.0.0.1';
|
|
@@ -115,6 +130,140 @@ export async function createDevServer(options) {
|
|
|
115
130
|
...details
|
|
116
131
|
});
|
|
117
132
|
}
|
|
133
|
+
function _classifyNotFound(pathname) {
|
|
134
|
+
const lower = String(pathname || '').toLowerCase();
|
|
135
|
+
if (lower.startsWith('/__zenith_dev/'))
|
|
136
|
+
return 'dev_internal';
|
|
137
|
+
if (lower.startsWith('/__zenith/'))
|
|
138
|
+
return 'zenith_internal';
|
|
139
|
+
if (lower.startsWith('/_assets/')
|
|
140
|
+
|| lower.startsWith('/assets/')
|
|
141
|
+
|| lower.endsWith('.css')
|
|
142
|
+
|| lower.endsWith('.js')
|
|
143
|
+
|| lower.endsWith('.map')
|
|
144
|
+
|| lower.endsWith('.json')) {
|
|
145
|
+
return 'asset';
|
|
146
|
+
}
|
|
147
|
+
return 'page';
|
|
148
|
+
}
|
|
149
|
+
function _routeFileHint(pathname) {
|
|
150
|
+
const normalized = String(pathname || '/').replace(/\/+$/, '');
|
|
151
|
+
if (normalized === '' || normalized === '/') {
|
|
152
|
+
return 'src/pages/index.zen';
|
|
153
|
+
}
|
|
154
|
+
return `src/pages${normalized}.zen`;
|
|
155
|
+
}
|
|
156
|
+
function _infer404Cause(category) {
|
|
157
|
+
if (category === 'dev_internal' || category === 'zenith_internal') {
|
|
158
|
+
if (buildStatus === 'error') {
|
|
159
|
+
return 'initial build failed';
|
|
160
|
+
}
|
|
161
|
+
return 'unknown Zenith dev endpoint';
|
|
162
|
+
}
|
|
163
|
+
if (category === 'asset') {
|
|
164
|
+
if (buildStatus === 'error') {
|
|
165
|
+
return 'initial build failed';
|
|
166
|
+
}
|
|
167
|
+
return 'asset not emitted by latest build';
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function _looksLikeJsonRequest(req, pathname) {
|
|
172
|
+
const accept = String(req.headers.accept || '').toLowerCase();
|
|
173
|
+
const secFetchDest = String(req.headers['sec-fetch-dest'] || '').toLowerCase();
|
|
174
|
+
if (accept.includes('application/json') || accept.includes('application/problem+json')) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (pathname.endsWith('.json')) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return secFetchDest === 'empty';
|
|
181
|
+
}
|
|
182
|
+
function _isBuildSwapReadError(error) {
|
|
183
|
+
const code = typeof error?.code === 'string' ? error.code : '';
|
|
184
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
185
|
+
}
|
|
186
|
+
function _delay(ms) {
|
|
187
|
+
return new Promise((resolveDelay) => {
|
|
188
|
+
setTimeout(resolveDelay, ms);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function _readFileForRequest(filePath, encoding = undefined) {
|
|
192
|
+
const attempts = buildStatus === 'building' ? 200 : 1;
|
|
193
|
+
let lastError = null;
|
|
194
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
195
|
+
try {
|
|
196
|
+
return encoding === undefined
|
|
197
|
+
? await readFile(filePath)
|
|
198
|
+
: await readFile(filePath, encoding);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
lastError = error;
|
|
202
|
+
if (!_isBuildSwapReadError(error) || attempt === attempts - 1) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
await _delay(50);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw lastError;
|
|
209
|
+
}
|
|
210
|
+
function _buildNotFoundPayload(pathname, category, cause) {
|
|
211
|
+
const payload = {
|
|
212
|
+
kind: 'zenith_dev_not_found',
|
|
213
|
+
category,
|
|
214
|
+
requestedPath: pathname,
|
|
215
|
+
buildId,
|
|
216
|
+
buildStatus,
|
|
217
|
+
cause: cause || ''
|
|
218
|
+
};
|
|
219
|
+
if (category === 'asset') {
|
|
220
|
+
payload.hint = buildStatus === 'error'
|
|
221
|
+
? 'Dev server is running but initial build failed; fix compile errors and refresh.'
|
|
222
|
+
: 'Check emitted assets in dist and verify the requested path.';
|
|
223
|
+
if (pathname.endsWith('.css')) {
|
|
224
|
+
payload.expectedCssHref = currentCssHref || null;
|
|
225
|
+
payload.hint = buildStatus === 'error'
|
|
226
|
+
? `Dev server is running but initial build failed; expected CSS at ${currentCssHref || '<none>'}.`
|
|
227
|
+
: `Requested CSS is missing; expected current href ${currentCssHref || '<none>'}.`;
|
|
228
|
+
}
|
|
229
|
+
return payload;
|
|
230
|
+
}
|
|
231
|
+
if (category === 'dev_internal' || category === 'zenith_internal') {
|
|
232
|
+
payload.hint = buildStatus === 'error'
|
|
233
|
+
? 'Dev server is running but initial build failed; restart after fixing compile errors.'
|
|
234
|
+
: 'Check Zenith dev endpoint path and dev client version.';
|
|
235
|
+
payload.docsLink = '/docs/documentation/contracts/hmr-v1-contract.md';
|
|
236
|
+
return payload;
|
|
237
|
+
}
|
|
238
|
+
const routeFile = _routeFileHint(pathname);
|
|
239
|
+
payload.routeFile = routeFile;
|
|
240
|
+
payload.cause = `no route file found at ${routeFile}`;
|
|
241
|
+
payload.hint = `Create ${routeFile} or verify router manifest output.`;
|
|
242
|
+
return payload;
|
|
243
|
+
}
|
|
244
|
+
function _renderNotFoundHtml(payload) {
|
|
245
|
+
const escaped = (value) => String(value || '')
|
|
246
|
+
.replaceAll('&', '&')
|
|
247
|
+
.replaceAll('<', '<')
|
|
248
|
+
.replaceAll('>', '>');
|
|
249
|
+
const details = [
|
|
250
|
+
`Requested: ${payload.requestedPath}`,
|
|
251
|
+
`Category: ${payload.category}`,
|
|
252
|
+
`Build: ${payload.buildStatus} (id=${payload.buildId})`,
|
|
253
|
+
`Cause: ${payload.cause}`,
|
|
254
|
+
payload.expectedCssHref ? `Expected CSS href: ${payload.expectedCssHref}` : '',
|
|
255
|
+
`Hint: ${payload.hint || 'Inspect dev server output.'}`,
|
|
256
|
+
payload.docsLink ? `Docs: ${payload.docsLink}` : ''
|
|
257
|
+
].filter(Boolean).join('\n');
|
|
258
|
+
return [
|
|
259
|
+
'<!DOCTYPE html>',
|
|
260
|
+
'<html><head><meta charset="utf-8"><title>Zenith Dev 404</title></head>',
|
|
261
|
+
'<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
|
|
262
|
+
'<h1 style="margin-top:0;">Zenith Dev 404</h1>',
|
|
263
|
+
`<pre style="white-space: pre-wrap; line-height: 1.5;">${escaped(details)}</pre>`,
|
|
264
|
+
'</body></html>'
|
|
265
|
+
].join('');
|
|
266
|
+
}
|
|
118
267
|
function _pickCssAsset(assets) {
|
|
119
268
|
if (!Array.isArray(assets) || assets.length === 0) {
|
|
120
269
|
return '';
|
|
@@ -125,6 +274,10 @@ export async function createDevServer(options) {
|
|
|
125
274
|
if (cssAssets.length === 0) {
|
|
126
275
|
return '';
|
|
127
276
|
}
|
|
277
|
+
const devStable = cssAssets.find((entry) => entry.endsWith('/styles.dev.css'));
|
|
278
|
+
if (devStable) {
|
|
279
|
+
return devStable;
|
|
280
|
+
}
|
|
128
281
|
const preferred = cssAssets.find((entry) => /\/styles(\.|\/|$)/.test(entry));
|
|
129
282
|
return preferred || cssAssets[0];
|
|
130
283
|
}
|
|
@@ -201,6 +354,24 @@ export async function createDevServer(options) {
|
|
|
201
354
|
currentCssContent = cssContent;
|
|
202
355
|
return true;
|
|
203
356
|
}
|
|
357
|
+
async function _loadRoutesForRequests() {
|
|
358
|
+
if (buildStatus === 'building' && Array.isArray(currentRoutes) && currentRoutes.length > 0) {
|
|
359
|
+
return currentRoutes;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const routes = await loadRouteManifest(outDir);
|
|
363
|
+
if (Array.isArray(routes) && routes.length > 0) {
|
|
364
|
+
currentRoutes = routes;
|
|
365
|
+
return routes;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
if (!(Array.isArray(currentRoutes) && currentRoutes.length > 0)) {
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return currentRoutes;
|
|
374
|
+
}
|
|
204
375
|
function _broadcastEvent(type, payload = {}) {
|
|
205
376
|
const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : buildId;
|
|
206
377
|
const data = JSON.stringify({
|
|
@@ -223,22 +394,62 @@ export async function createDevServer(options) {
|
|
|
223
394
|
}
|
|
224
395
|
}
|
|
225
396
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
logger.
|
|
397
|
+
async function _runInitialBuild() {
|
|
398
|
+
buildStatus = 'building';
|
|
399
|
+
buildError = null;
|
|
400
|
+
const startTime = Date.now();
|
|
401
|
+
startupProfile.emit('initial_build_start', { buildId });
|
|
402
|
+
try {
|
|
403
|
+
logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
|
|
404
|
+
const initialBuild = await buildSession.build();
|
|
405
|
+
const cssReady = await _syncCssStateFromBuild(initialBuild, buildId);
|
|
406
|
+
currentRoutes = await loadRouteManifest(outDir);
|
|
407
|
+
buildStatus = 'ok';
|
|
408
|
+
buildError = null;
|
|
409
|
+
lastBuildMs = Date.now();
|
|
410
|
+
durationMs = lastBuildMs - startTime;
|
|
411
|
+
if (cssReady && currentCssHref.length > 0) {
|
|
412
|
+
logger.css(`ready (${currentCssHref})`, { onceKey: `css-ready:${buildId}:${currentCssHref}` });
|
|
413
|
+
}
|
|
414
|
+
_trace('state_snapshot', {
|
|
415
|
+
status: buildStatus,
|
|
416
|
+
buildId,
|
|
417
|
+
cssHref: currentCssHref,
|
|
418
|
+
durationMs
|
|
419
|
+
});
|
|
420
|
+
startupProfile.emit('initial_build_complete', {
|
|
421
|
+
buildId,
|
|
422
|
+
status: buildStatus,
|
|
423
|
+
durationMs,
|
|
424
|
+
cssReady,
|
|
425
|
+
routes: Array.isArray(currentRoutes) ? currentRoutes.length : 0
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
buildStatus = 'error';
|
|
430
|
+
buildError = { message: err instanceof Error ? err.message : String(err) };
|
|
431
|
+
lastBuildMs = Date.now();
|
|
432
|
+
durationMs = lastBuildMs - startTime;
|
|
433
|
+
logger.error('initial build failed', {
|
|
434
|
+
hint: 'fix the error and restart dev',
|
|
435
|
+
error: err
|
|
436
|
+
});
|
|
437
|
+
_trace('state_snapshot', {
|
|
438
|
+
status: buildStatus,
|
|
439
|
+
buildId,
|
|
440
|
+
durationMs,
|
|
441
|
+
error: buildError
|
|
442
|
+
});
|
|
443
|
+
startupProfile.emit('initial_build_complete', {
|
|
444
|
+
buildId,
|
|
445
|
+
status: buildStatus,
|
|
446
|
+
durationMs,
|
|
447
|
+
error: buildError?.message || ''
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
initialBuildSettled = true;
|
|
233
452
|
}
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
buildStatus = 'error';
|
|
237
|
-
buildError = { message: err instanceof Error ? err.message : String(err) };
|
|
238
|
-
logger.error('initial build failed', {
|
|
239
|
-
hint: 'fix the error and restart dev',
|
|
240
|
-
error: err
|
|
241
|
-
});
|
|
242
453
|
}
|
|
243
454
|
const server = createServer(async (req, res) => {
|
|
244
455
|
const requestBase = typeof req.headers.host === 'string' && req.headers.host.length > 0
|
|
@@ -303,6 +514,21 @@ export async function createDevServer(options) {
|
|
|
303
514
|
return;
|
|
304
515
|
}
|
|
305
516
|
if (pathname === '/__zenith_dev/styles.css') {
|
|
517
|
+
if (buildStatus === 'error') {
|
|
518
|
+
const reason = typeof buildError?.message === 'string' && buildError.message.length > 0
|
|
519
|
+
? buildError.message
|
|
520
|
+
: 'initial build failed';
|
|
521
|
+
const summary = reason.length > 280 ? `${reason.slice(0, 277)}...` : reason;
|
|
522
|
+
res.writeHead(503, {
|
|
523
|
+
'Content-Type': 'text/css; charset=utf-8',
|
|
524
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
525
|
+
'Pragma': 'no-cache',
|
|
526
|
+
'Expires': '0',
|
|
527
|
+
'X-Zenith-Dev-Error': 'build-failed'
|
|
528
|
+
});
|
|
529
|
+
res.end(`/* zenith-dev: css unavailable because build failed */\n/* cause: ${summary} */\n/* expected href: ${currentCssHref || '<none>'} */`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
306
532
|
if (typeof currentCssContent === 'string' && currentCssContent.length > 0) {
|
|
307
533
|
res.writeHead(200, {
|
|
308
534
|
'Content-Type': 'text/css; charset=utf-8',
|
|
@@ -339,8 +565,24 @@ export async function createDevServer(options) {
|
|
|
339
565
|
res.end(currentCssContent);
|
|
340
566
|
return;
|
|
341
567
|
}
|
|
568
|
+
if (pathname === '/_zenith/image') {
|
|
569
|
+
await handleImageRequest(req, res, {
|
|
570
|
+
requestUrl: url,
|
|
571
|
+
projectRoot,
|
|
572
|
+
config: config.images
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
342
576
|
if (pathname === '/__zenith/route-check') {
|
|
343
577
|
try {
|
|
578
|
+
if (!initialBuildSettled && buildStatus === 'building') {
|
|
579
|
+
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
580
|
+
res.end(JSON.stringify({
|
|
581
|
+
error: 'initial_build_pending',
|
|
582
|
+
message: 'initial build still in progress'
|
|
583
|
+
}));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
344
586
|
// Security: Require explicitly designated header to prevent public oracle probing
|
|
345
587
|
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
346
588
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
@@ -360,7 +602,7 @@ export async function createDevServer(options) {
|
|
|
360
602
|
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
361
603
|
return;
|
|
362
604
|
}
|
|
363
|
-
const routes = await
|
|
605
|
+
const routes = await _loadRoutesForRequests();
|
|
364
606
|
const resolvedCheck = resolveRequestRoute(targetUrl, routes);
|
|
365
607
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
366
608
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -417,18 +659,48 @@ export async function createDevServer(options) {
|
|
|
417
659
|
let resolvedPathFor404 = null;
|
|
418
660
|
let staticRootFor404 = null;
|
|
419
661
|
try {
|
|
662
|
+
if (!initialBuildSettled && buildStatus === 'building') {
|
|
663
|
+
const pendingPayload = {
|
|
664
|
+
kind: 'zenith_dev_build_pending',
|
|
665
|
+
requestedPath: pathname,
|
|
666
|
+
buildId,
|
|
667
|
+
buildStatus,
|
|
668
|
+
hint: 'Initial build is still running. Retry shortly or inspect /__zenith_dev/state.'
|
|
669
|
+
};
|
|
670
|
+
if (_looksLikeJsonRequest(req, pathname)) {
|
|
671
|
+
res.writeHead(503, {
|
|
672
|
+
'Content-Type': 'application/json',
|
|
673
|
+
'Cache-Control': 'no-store'
|
|
674
|
+
});
|
|
675
|
+
res.end(JSON.stringify(pendingPayload));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
res.writeHead(503, {
|
|
679
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
680
|
+
'Cache-Control': 'no-store'
|
|
681
|
+
});
|
|
682
|
+
res.end([
|
|
683
|
+
'<!DOCTYPE html>',
|
|
684
|
+
'<html><head><meta charset="utf-8"><title>Zenith Dev Building</title></head>',
|
|
685
|
+
'<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
|
|
686
|
+
'<h1 style="margin-top:0;">Zenith Dev Building</h1>',
|
|
687
|
+
`<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${pathname}\nStatus: initial build running\nHint: ${pendingPayload.hint}</pre>`,
|
|
688
|
+
'</body></html>'
|
|
689
|
+
].join(''));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
420
692
|
const requestExt = extname(pathname);
|
|
421
693
|
if (requestExt && requestExt !== '.html') {
|
|
422
694
|
const assetPath = join(outDir, pathname);
|
|
423
695
|
resolvedPathFor404 = assetPath;
|
|
424
696
|
staticRootFor404 = outDir;
|
|
425
|
-
const asset = await
|
|
697
|
+
const asset = await _readFileForRequest(assetPath);
|
|
426
698
|
const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
|
|
427
699
|
res.writeHead(200, { 'Content-Type': mime });
|
|
428
700
|
res.end(asset);
|
|
429
701
|
return;
|
|
430
702
|
}
|
|
431
|
-
const routes = await
|
|
703
|
+
const routes = await _loadRoutesForRequests();
|
|
432
704
|
const resolved = resolveRequestRoute(url, routes);
|
|
433
705
|
let filePath = null;
|
|
434
706
|
if (resolved.matched && resolved.route) {
|
|
@@ -490,28 +762,63 @@ export async function createDevServer(options) {
|
|
|
490
762
|
if (result && result.kind === 'deny') {
|
|
491
763
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
492
764
|
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
493
|
-
res.end(result.message || (status
|
|
765
|
+
res.end(result.message || defaultRouteDenyMessage(status));
|
|
494
766
|
return;
|
|
495
767
|
}
|
|
496
768
|
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
497
769
|
ssrPayload = result.data;
|
|
498
770
|
}
|
|
499
771
|
}
|
|
500
|
-
let content = await
|
|
772
|
+
let content = await _readFileForRequest(filePath, 'utf8');
|
|
773
|
+
if (resolved.matched && resolved.route?.page_asset) {
|
|
774
|
+
const pageAssetPath = resolveWithinDist(outDir, resolved.route.page_asset);
|
|
775
|
+
content = await materializeImageMarkup({
|
|
776
|
+
html: content,
|
|
777
|
+
pageAssetPath,
|
|
778
|
+
payload: buildSession.getImageRuntimePayload(),
|
|
779
|
+
ssrData: ssrPayload,
|
|
780
|
+
routePathname: resolved.route.path || pathname
|
|
781
|
+
});
|
|
782
|
+
}
|
|
501
783
|
if (ssrPayload) {
|
|
502
784
|
content = injectSsrPayload(content, ssrPayload);
|
|
503
785
|
}
|
|
786
|
+
content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
|
|
504
787
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
505
788
|
res.end(content);
|
|
506
789
|
}
|
|
507
|
-
catch {
|
|
790
|
+
catch (error) {
|
|
791
|
+
const category = _classifyNotFound(pathname);
|
|
792
|
+
const cause = _infer404Cause(category);
|
|
793
|
+
const payload = _buildNotFoundPayload(pathname, category, cause);
|
|
794
|
+
if (buildStatus === 'error' && typeof buildError?.message === 'string') {
|
|
795
|
+
payload.buildError = buildError.message.length > 600
|
|
796
|
+
? `${buildError.message.slice(0, 597)}...`
|
|
797
|
+
: buildError.message;
|
|
798
|
+
}
|
|
799
|
+
const displayCategory = category === 'page' ? 'page' : 'asset';
|
|
800
|
+
logger.warn(`404 ${displayCategory}: ${pathname} (buildId=${buildId}) -> cause: ${payload.cause || cause || 'not found'}`);
|
|
508
801
|
_trace404(req, url, {
|
|
509
802
|
reason: 'not_found',
|
|
803
|
+
category,
|
|
804
|
+
cause: payload.cause || cause || 'not_found',
|
|
510
805
|
staticRoot: staticRootFor404,
|
|
511
|
-
resolvedPath: resolvedPathFor404
|
|
806
|
+
resolvedPath: resolvedPathFor404,
|
|
807
|
+
error: error instanceof Error ? error.message : String(error || '')
|
|
808
|
+
});
|
|
809
|
+
if (_looksLikeJsonRequest(req, pathname)) {
|
|
810
|
+
res.writeHead(404, {
|
|
811
|
+
'Content-Type': 'application/json',
|
|
812
|
+
'Cache-Control': 'no-store'
|
|
813
|
+
});
|
|
814
|
+
res.end(JSON.stringify(payload));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
res.writeHead(404, {
|
|
818
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
819
|
+
'Cache-Control': 'no-store'
|
|
512
820
|
});
|
|
513
|
-
res.
|
|
514
|
-
res.end('404 Not Found');
|
|
821
|
+
res.end(_renderNotFoundHtml(payload));
|
|
515
822
|
}
|
|
516
823
|
});
|
|
517
824
|
/**
|
|
@@ -566,7 +873,8 @@ export async function createDevServer(options) {
|
|
|
566
873
|
* Start watching source roots for changes.
|
|
567
874
|
*/
|
|
568
875
|
function _startWatcher() {
|
|
569
|
-
const
|
|
876
|
+
const watcherStartedAt = performance.now();
|
|
877
|
+
const triggerBuildDrain = (delayMs = rebuildDebounceMs) => {
|
|
570
878
|
if (_buildDebounce !== null) {
|
|
571
879
|
clearTimeout(_buildDebounce);
|
|
572
880
|
}
|
|
@@ -579,7 +887,8 @@ export async function createDevServer(options) {
|
|
|
579
887
|
if (_buildInFlight) {
|
|
580
888
|
return;
|
|
581
889
|
}
|
|
582
|
-
const
|
|
890
|
+
const changedFiles = Array.from(_queuedFiles);
|
|
891
|
+
const changed = changedFiles.map(_toDisplayPath).sort();
|
|
583
892
|
if (changed.length === 0) {
|
|
584
893
|
return;
|
|
585
894
|
}
|
|
@@ -593,9 +902,13 @@ export async function createDevServer(options) {
|
|
|
593
902
|
const startTime = Date.now();
|
|
594
903
|
const previousCssAssetPath = currentCssAssetPath;
|
|
595
904
|
const previousCssContent = currentCssContent;
|
|
905
|
+
const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
|
|
596
906
|
try {
|
|
597
|
-
const buildResult = await build({
|
|
907
|
+
const buildResult = await buildSession.build({ changedFiles, logger });
|
|
598
908
|
const cssReady = await _syncCssStateFromBuild(buildResult, cycleBuildId);
|
|
909
|
+
if (!onlyCss) {
|
|
910
|
+
currentRoutes = await loadRouteManifest(outDir);
|
|
911
|
+
}
|
|
599
912
|
const cssChanged = cssReady && (currentCssAssetPath !== previousCssAssetPath ||
|
|
600
913
|
currentCssContent !== previousCssContent);
|
|
601
914
|
buildId = cycleBuildId;
|
|
@@ -623,7 +936,6 @@ export async function createDevServer(options) {
|
|
|
623
936
|
logger.hmr(`css_update (buildId=${cycleBuildId})`);
|
|
624
937
|
_broadcastEvent('css_update', { href: currentCssHref, changedFiles: changed });
|
|
625
938
|
}
|
|
626
|
-
const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
|
|
627
939
|
if (!onlyCss) {
|
|
628
940
|
logger.hmr(`reload (buildId=${cycleBuildId})`);
|
|
629
941
|
_broadcastEvent('reload', { changedFiles: changed });
|
|
@@ -659,7 +971,7 @@ export async function createDevServer(options) {
|
|
|
659
971
|
finally {
|
|
660
972
|
_buildInFlight = false;
|
|
661
973
|
if (_queuedFiles.size > 0) {
|
|
662
|
-
triggerBuildDrain(
|
|
974
|
+
triggerBuildDrain(queuedRebuildDebounceMs);
|
|
663
975
|
}
|
|
664
976
|
}
|
|
665
977
|
};
|
|
@@ -692,35 +1004,70 @@ export async function createDevServer(options) {
|
|
|
692
1004
|
// fs.watch recursive may not be supported on this platform/root
|
|
693
1005
|
}
|
|
694
1006
|
}
|
|
1007
|
+
startupProfile.emit('watcher_ready', {
|
|
1008
|
+
roots: roots.length,
|
|
1009
|
+
activeWatchers: _watchers.length,
|
|
1010
|
+
durationMs: startupProfile.roundMs(performance.now() - watcherStartedAt)
|
|
1011
|
+
});
|
|
695
1012
|
}
|
|
696
|
-
|
|
697
|
-
|
|
1013
|
+
const closeServer = () => {
|
|
1014
|
+
clearInterval(sseHeartbeat);
|
|
1015
|
+
for (const watcher of _watchers) {
|
|
1016
|
+
try {
|
|
1017
|
+
watcher.close();
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
// ignore close errors
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
_watchers = [];
|
|
1024
|
+
for (const client of hmrClients) {
|
|
1025
|
+
try {
|
|
1026
|
+
client.end();
|
|
1027
|
+
}
|
|
1028
|
+
catch { }
|
|
1029
|
+
}
|
|
1030
|
+
hmrClients.length = 0;
|
|
1031
|
+
server.close();
|
|
1032
|
+
};
|
|
1033
|
+
return new Promise((resolve, reject) => {
|
|
1034
|
+
let settled = false;
|
|
1035
|
+
server.once('error', (error) => {
|
|
1036
|
+
if (!settled) {
|
|
1037
|
+
settled = true;
|
|
1038
|
+
reject(error);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
server.listen(port, host, async () => {
|
|
698
1042
|
actualPort = server.address().port;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
server,
|
|
1043
|
+
startupProfile.emit('server_bound', {
|
|
1044
|
+
host: _publicHost(),
|
|
702
1045
|
port: actualPort,
|
|
703
|
-
|
|
704
|
-
clearInterval(sseHeartbeat);
|
|
705
|
-
for (const watcher of _watchers) {
|
|
706
|
-
try {
|
|
707
|
-
watcher.close();
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
// ignore close errors
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
_watchers = [];
|
|
714
|
-
for (const client of hmrClients) {
|
|
715
|
-
try {
|
|
716
|
-
client.end();
|
|
717
|
-
}
|
|
718
|
-
catch { }
|
|
719
|
-
}
|
|
720
|
-
hmrClients.length = 0;
|
|
721
|
-
server.close();
|
|
722
|
-
}
|
|
1046
|
+
buildStatus
|
|
723
1047
|
});
|
|
1048
|
+
_trace('server_bound', {
|
|
1049
|
+
host: _publicHost(),
|
|
1050
|
+
port: actualPort,
|
|
1051
|
+
buildStatus
|
|
1052
|
+
});
|
|
1053
|
+
try {
|
|
1054
|
+
await _runInitialBuild();
|
|
1055
|
+
_startWatcher();
|
|
1056
|
+
if (!settled) {
|
|
1057
|
+
settled = true;
|
|
1058
|
+
resolve({
|
|
1059
|
+
server,
|
|
1060
|
+
port: actualPort,
|
|
1061
|
+
close: closeServer
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
if (!settled) {
|
|
1067
|
+
settled = true;
|
|
1068
|
+
reject(error);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
724
1071
|
});
|
|
725
1072
|
});
|
|
726
1073
|
}
|