@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.
Files changed (64) hide show
  1. package/dist/build/compiler-runtime.d.ts +59 -0
  2. package/dist/build/compiler-runtime.js +277 -0
  3. package/dist/build/expression-rewrites.d.ts +88 -0
  4. package/dist/build/expression-rewrites.js +372 -0
  5. package/dist/build/hoisted-code-transforms.d.ts +44 -0
  6. package/dist/build/hoisted-code-transforms.js +316 -0
  7. package/dist/build/merge-component-ir.d.ts +16 -0
  8. package/dist/build/merge-component-ir.js +257 -0
  9. package/dist/build/page-component-loop.d.ts +92 -0
  10. package/dist/build/page-component-loop.js +257 -0
  11. package/dist/build/page-ir-normalization.d.ts +23 -0
  12. package/dist/build/page-ir-normalization.js +370 -0
  13. package/dist/build/page-loop-metrics.d.ts +100 -0
  14. package/dist/build/page-loop-metrics.js +131 -0
  15. package/dist/build/page-loop-state.d.ts +261 -0
  16. package/dist/build/page-loop-state.js +92 -0
  17. package/dist/build/page-loop.d.ts +33 -0
  18. package/dist/build/page-loop.js +217 -0
  19. package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
  20. package/dist/build/scoped-identifier-rewrite.js +245 -0
  21. package/dist/build/server-script.d.ts +41 -0
  22. package/dist/build/server-script.js +210 -0
  23. package/dist/build/type-declarations.d.ts +16 -0
  24. package/dist/build/type-declarations.js +158 -0
  25. package/dist/build/typescript-expression-utils.d.ts +23 -0
  26. package/dist/build/typescript-expression-utils.js +272 -0
  27. package/dist/build.d.ts +10 -18
  28. package/dist/build.js +74 -2261
  29. package/dist/component-instance-ir.d.ts +2 -2
  30. package/dist/component-instance-ir.js +146 -39
  31. package/dist/component-occurrences.js +63 -15
  32. package/dist/config.d.ts +66 -0
  33. package/dist/config.js +86 -0
  34. package/dist/debug-script.d.ts +1 -0
  35. package/dist/debug-script.js +8 -0
  36. package/dist/dev-build-session.d.ts +23 -0
  37. package/dist/dev-build-session.js +421 -0
  38. package/dist/dev-server.js +405 -58
  39. package/dist/framework-components/Image.zen +316 -0
  40. package/dist/images/materialize.d.ts +17 -0
  41. package/dist/images/materialize.js +200 -0
  42. package/dist/images/payload.d.ts +18 -0
  43. package/dist/images/payload.js +65 -0
  44. package/dist/images/runtime.d.ts +4 -0
  45. package/dist/images/runtime.js +254 -0
  46. package/dist/images/service.d.ts +4 -0
  47. package/dist/images/service.js +302 -0
  48. package/dist/images/shared.d.ts +58 -0
  49. package/dist/images/shared.js +306 -0
  50. package/dist/index.js +2 -17
  51. package/dist/manifest.js +45 -0
  52. package/dist/preview.d.ts +4 -1
  53. package/dist/preview.js +59 -6
  54. package/dist/resolve-components.js +20 -3
  55. package/dist/server-contract.js +3 -2
  56. package/dist/server-script-composition.d.ts +39 -0
  57. package/dist/server-script-composition.js +133 -0
  58. package/dist/startup-profile.d.ts +10 -0
  59. package/dist/startup-profile.js +62 -0
  60. package/dist/toolchain-paths.d.ts +1 -0
  61. package/dist/toolchain-paths.js +31 -0
  62. package/dist/version-check.d.ts +2 -1
  63. package/dist/version-check.js +12 -5
  64. package/package.json +5 -4
@@ -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 { build } from './build.js';
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 = 'ok'; // 'ok' | 'error' | 'building'
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('&', '&amp;')
247
+ .replaceAll('<', '&lt;')
248
+ .replaceAll('>', '&gt;');
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
- // Initial build
227
- try {
228
- logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
229
- const initialBuild = await build({ pagesDir, outDir, config, logger });
230
- await _syncCssStateFromBuild(initialBuild, buildId);
231
- if (currentCssHref.length > 0) {
232
- logger.css(`ready (${currentCssHref})`, { onceKey: `css-ready:${buildId}:${currentCssHref}` });
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 loadRouteManifest(outDir);
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 readFile(assetPath);
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 loadRouteManifest(outDir);
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 === 401 ? 'Unauthorized' : 'Forbidden'));
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 readFile(filePath, 'utf8');
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.writeHead(404, { 'Content-Type': 'text/plain' });
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 triggerBuildDrain = (delayMs = 50) => {
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 changed = Array.from(_queuedFiles).map(_toDisplayPath).sort();
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({ pagesDir, outDir, config, logger });
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(20);
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
- return new Promise((resolve) => {
697
- server.listen(port, host, () => {
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
- _startWatcher();
700
- resolve({
701
- server,
1043
+ startupProfile.emit('server_bound', {
1044
+ host: _publicHost(),
702
1045
  port: actualPort,
703
- close: () => {
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
  }