@zenithbuild/cli 0.7.4 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/adapter-netlify.d.ts +1 -1
  3. package/dist/adapters/adapter-netlify.js +48 -14
  4. package/dist/adapters/adapter-static-export.d.ts +5 -0
  5. package/dist/adapters/adapter-static-export.js +115 -0
  6. package/dist/adapters/adapter-types.d.ts +3 -1
  7. package/dist/adapters/adapter-types.js +5 -2
  8. package/dist/adapters/adapter-vercel.d.ts +1 -1
  9. package/dist/adapters/adapter-vercel.js +67 -19
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +50 -0
  12. package/dist/adapters/resolve-adapter.js +4 -0
  13. package/dist/adapters/route-rules.d.ts +5 -0
  14. package/dist/adapters/route-rules.js +9 -0
  15. package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
  16. package/dist/adapters/validate-hosted-resource-routes.js +13 -0
  17. package/dist/auth/route-auth.d.ts +6 -0
  18. package/dist/auth/route-auth.js +236 -0
  19. package/dist/build/compiler-runtime.d.ts +1 -1
  20. package/dist/build/compiler-runtime.js +8 -2
  21. package/dist/build/hoisted-code-transforms.d.ts +4 -1
  22. package/dist/build/hoisted-code-transforms.js +5 -3
  23. package/dist/build/page-ir-normalization.d.ts +1 -1
  24. package/dist/build/page-ir-normalization.js +33 -3
  25. package/dist/build/page-loop-state.js +1 -1
  26. package/dist/build/page-loop.js +46 -2
  27. package/dist/build/server-script.d.ts +2 -1
  28. package/dist/build/server-script.js +7 -3
  29. package/dist/build-output-manifest.d.ts +3 -2
  30. package/dist/build-output-manifest.js +3 -0
  31. package/dist/build.js +29 -17
  32. package/dist/dev-build-session/helpers.d.ts +29 -0
  33. package/dist/dev-build-session/helpers.js +223 -0
  34. package/dist/dev-build-session/session.d.ts +24 -0
  35. package/dist/dev-build-session/session.js +204 -0
  36. package/dist/dev-build-session/state.d.ts +37 -0
  37. package/dist/dev-build-session/state.js +17 -0
  38. package/dist/dev-build-session.d.ts +1 -24
  39. package/dist/dev-build-session.js +1 -434
  40. package/dist/dev-server/css-state.d.ts +7 -0
  41. package/dist/dev-server/css-state.js +92 -0
  42. package/dist/dev-server/not-found.d.ts +23 -0
  43. package/dist/dev-server/not-found.js +129 -0
  44. package/dist/dev-server/request-handler.d.ts +1 -0
  45. package/dist/dev-server/request-handler.js +376 -0
  46. package/dist/dev-server/route-check.d.ts +9 -0
  47. package/dist/dev-server/route-check.js +100 -0
  48. package/dist/dev-server/watcher.d.ts +5 -0
  49. package/dist/dev-server/watcher.js +216 -0
  50. package/dist/dev-server.js +136 -883
  51. package/dist/download-result.d.ts +14 -0
  52. package/dist/download-result.js +148 -0
  53. package/dist/images/payload.js +4 -0
  54. package/dist/images/service.d.ts +13 -1
  55. package/dist/images/service.js +45 -15
  56. package/dist/manifest.d.ts +15 -1
  57. package/dist/manifest.js +70 -6
  58. package/dist/preview/create-preview-server.d.ts +18 -0
  59. package/dist/preview/create-preview-server.js +71 -0
  60. package/dist/preview/manifest.d.ts +42 -0
  61. package/dist/preview/manifest.js +57 -0
  62. package/dist/preview/paths.d.ts +3 -0
  63. package/dist/preview/paths.js +38 -0
  64. package/dist/preview/payload.d.ts +6 -0
  65. package/dist/preview/payload.js +34 -0
  66. package/dist/preview/request-handler.d.ts +1 -0
  67. package/dist/preview/request-handler.js +300 -0
  68. package/dist/preview/server-runner.d.ts +49 -0
  69. package/dist/preview/server-runner.js +220 -0
  70. package/dist/preview/server-script-runner-template.d.ts +1 -0
  71. package/dist/preview/server-script-runner-template.js +425 -0
  72. package/dist/preview.d.ts +5 -104
  73. package/dist/preview.js +7 -993
  74. package/dist/request-body.d.ts +0 -1
  75. package/dist/request-body.js +0 -6
  76. package/dist/resource-manifest.d.ts +16 -0
  77. package/dist/resource-manifest.js +53 -0
  78. package/dist/resource-response.d.ts +49 -0
  79. package/dist/resource-response.js +160 -0
  80. package/dist/resource-route-module.d.ts +15 -0
  81. package/dist/resource-route-module.js +129 -0
  82. package/dist/route-check-support.js +1 -1
  83. package/dist/server-contract/constants.d.ts +5 -0
  84. package/dist/server-contract/constants.js +5 -0
  85. package/dist/server-contract/export-validation.d.ts +5 -0
  86. package/dist/server-contract/export-validation.js +59 -0
  87. package/dist/server-contract/json-serializable.d.ts +1 -0
  88. package/dist/server-contract/json-serializable.js +52 -0
  89. package/dist/server-contract/resolve.d.ts +15 -0
  90. package/dist/server-contract/resolve.js +271 -0
  91. package/dist/server-contract/result-helpers.d.ts +51 -0
  92. package/dist/server-contract/result-helpers.js +59 -0
  93. package/dist/server-contract/route-result-validation.d.ts +2 -0
  94. package/dist/server-contract/route-result-validation.js +73 -0
  95. package/dist/server-contract/stage.d.ts +6 -0
  96. package/dist/server-contract/stage.js +22 -0
  97. package/dist/server-contract.d.ts +6 -54
  98. package/dist/server-contract.js +9 -301
  99. package/dist/server-error.d.ts +1 -1
  100. package/dist/server-error.js +2 -0
  101. package/dist/server-middleware.d.ts +10 -0
  102. package/dist/server-middleware.js +30 -0
  103. package/dist/server-output.d.ts +2 -1
  104. package/dist/server-output.js +72 -12
  105. package/dist/server-runtime/node-server.js +59 -7
  106. package/dist/server-runtime/route-render.d.ts +25 -1
  107. package/dist/server-runtime/route-render.js +81 -29
  108. package/dist/server-script-composition.d.ts +4 -2
  109. package/dist/server-script-composition.js +6 -3
  110. package/dist/static-export-paths.d.ts +3 -0
  111. package/dist/static-export-paths.js +160 -0
  112. package/package.json +3 -3
@@ -0,0 +1 @@
1
+ export function createDevRequestHandler(options: any): (req: any, res: any) => Promise<void>;
@@ -0,0 +1,376 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, join } from 'node:path';
3
+ import { appLocalRedirectLocation, imageEndpointPath, routeCheckPath, stripBasePath } from '../base-path.js';
4
+ import { readRequestBodyBuffer } from '../request-body.js';
5
+ import { buildResourceResponseDescriptor } from '../resource-response.js';
6
+ import { clientFacingRouteMessage, logServerException } from '../server-error.js';
7
+ import { executeServerRoute, injectSsrPayload, resolveWithinDist, toStaticFilePath } from '../preview.js';
8
+ import { materializeImageMarkup } from '../images/materialize.js';
9
+ import { injectImageRuntimePayload } from '../images/payload.js';
10
+ import { handleImageRequest } from '../images/service.js';
11
+ import { resolveRequestRoute } from '../server/resolve-request-route.js';
12
+ import { handleRouteCheckRequest } from './route-check.js';
13
+ export function createDevRequestHandler(options) {
14
+ const { outDir, projectRoot, imageConfig, configuredBasePath, routeCheckEnabled, isStaticExportTarget, logger, verboseLogging, buildSession, state, serverOrigin, loadRoutesForRequests, readFileForRequest, trace404, looksLikeJsonRequest, classifyNotFound, infer404Cause, buildNotFoundPayload, renderNotFoundHtml, appendSetCookieHeaders, MIME_TYPES, EVENT_STREAM_MIME, LEGACY_DEV_STREAM_PATH, IMAGE_RUNTIME_TAG_RE } = options;
15
+ return async function handleDevRequest(req, res) {
16
+ const url = new URL(req.url, serverOrigin());
17
+ const pathname = url.pathname;
18
+ // Legacy HMR endpoint (deprecated but kept alive to avoid breaking old caches instantly)
19
+ if (pathname === LEGACY_DEV_STREAM_PATH) {
20
+ res.writeHead(200, {
21
+ 'Content-Type': EVENT_STREAM_MIME,
22
+ 'Cache-Control': 'no-store',
23
+ 'Connection': 'keep-alive',
24
+ 'X-Zenith-Deprecated': 'true'
25
+ });
26
+ logger.warn('legacy HMR endpoint in use', {
27
+ hint: 'use /__zenith_dev/events',
28
+ onceKey: 'legacy-hmr-endpoint'
29
+ });
30
+ res.write(': connected\n\n');
31
+ state.hmrClients.push(res);
32
+ req.on('close', () => {
33
+ const idx = state.hmrClients.indexOf(res);
34
+ if (idx !== -1)
35
+ state.hmrClients.splice(idx, 1);
36
+ });
37
+ return;
38
+ }
39
+ // V1 Dev State Endpoint
40
+ if (pathname === '/__zenith_dev/state') {
41
+ res.writeHead(200, {
42
+ 'Content-Type': 'application/json',
43
+ 'Cache-Control': 'no-store'
44
+ });
45
+ res.end(JSON.stringify({
46
+ serverUrl: serverOrigin(),
47
+ buildId: state.buildId,
48
+ status: state.buildStatus,
49
+ lastBuildMs: state.lastBuildMs,
50
+ durationMs: state.durationMs,
51
+ cssHref: state.currentCssHref,
52
+ error: state.buildError
53
+ }));
54
+ return;
55
+ }
56
+ // V1 Dev Events Endpoint (SSE)
57
+ if (pathname === '/__zenith_dev/events') {
58
+ res.writeHead(200, {
59
+ 'Content-Type': EVENT_STREAM_MIME,
60
+ 'Cache-Control': 'no-store',
61
+ 'Connection': 'keep-alive',
62
+ 'X-Accel-Buffering': 'no'
63
+ });
64
+ res.write('retry: 1000\n');
65
+ res.write('event: connected\ndata: {}\n\n');
66
+ state.hmrClients.push(res);
67
+ req.on('close', () => {
68
+ const idx = state.hmrClients.indexOf(res);
69
+ if (idx !== -1)
70
+ state.hmrClients.splice(idx, 1);
71
+ });
72
+ return;
73
+ }
74
+ if (pathname === '/__zenith_dev/styles.css') {
75
+ if (state.buildStatus === 'error') {
76
+ const reason = typeof state.buildError?.message === 'string' && state.buildError.message.length > 0
77
+ ? state.buildError.message
78
+ : 'initial build failed';
79
+ const summary = reason.length > 280 ? `${reason.slice(0, 277)}...` : reason;
80
+ res.writeHead(503, {
81
+ 'Content-Type': 'text/css; charset=utf-8',
82
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
83
+ 'Pragma': 'no-cache',
84
+ 'Expires': '0',
85
+ 'X-Zenith-Dev-Error': 'build-failed'
86
+ });
87
+ res.end(`/* zenith-dev: css unavailable because build failed */\n/* cause: ${summary} */\n/* expected href: ${state.currentCssHref || '<none>'} */`);
88
+ return;
89
+ }
90
+ if (typeof state.currentCssContent === 'string' && state.currentCssContent.length > 0) {
91
+ res.writeHead(200, {
92
+ 'Content-Type': 'text/css; charset=utf-8',
93
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
94
+ 'Pragma': 'no-cache',
95
+ 'Expires': '0'
96
+ });
97
+ res.end(state.currentCssContent);
98
+ return;
99
+ }
100
+ if (typeof state.currentCssAssetPath === 'string' && state.currentCssAssetPath.length > 0) {
101
+ try {
102
+ const css = await readFile(join(outDir, state.currentCssAssetPath), 'utf8');
103
+ if (typeof css === 'string' && css.length > 0) {
104
+ state.currentCssContent = css;
105
+ }
106
+ }
107
+ catch {
108
+ // keep serving last known CSS body below
109
+ }
110
+ }
111
+ if (typeof state.currentCssContent !== 'string') {
112
+ state.currentCssContent = '';
113
+ }
114
+ if (state.currentCssContent.length === 0) {
115
+ state.currentCssContent = '/* zenith-dev: css pending */';
116
+ }
117
+ res.writeHead(200, {
118
+ 'Content-Type': 'text/css; charset=utf-8',
119
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
120
+ 'Pragma': 'no-cache',
121
+ 'Expires': '0'
122
+ });
123
+ res.end(state.currentCssContent);
124
+ return;
125
+ }
126
+ if (pathname === imageEndpointPath(configuredBasePath)) {
127
+ if (isStaticExportTarget) {
128
+ throw new Error('not found');
129
+ }
130
+ await handleImageRequest(req, res, {
131
+ requestUrl: url,
132
+ projectRoot,
133
+ config: imageConfig
134
+ });
135
+ return;
136
+ }
137
+ if (pathname === routeCheckPath(configuredBasePath)) {
138
+ await handleRouteCheckRequest({
139
+ req,
140
+ res,
141
+ url,
142
+ configuredBasePath,
143
+ routeCheckEnabled,
144
+ state,
145
+ loadRoutesForRequests
146
+ });
147
+ return;
148
+ }
149
+ let resolvedPathFor404 = null;
150
+ let staticRootFor404 = null;
151
+ try {
152
+ const canonicalPath = stripBasePath(pathname, configuredBasePath);
153
+ if (!state.initialBuildSettled && state.buildStatus === 'building') {
154
+ const pendingPayload = {
155
+ kind: 'zenith_dev_build_pending',
156
+ requestedPath: pathname,
157
+ buildId: state.buildId,
158
+ buildStatus: state.buildStatus,
159
+ hint: 'Initial build is still running. Retry shortly or inspect /__zenith_dev/state.'
160
+ };
161
+ if (looksLikeJsonRequest(req, pathname)) {
162
+ res.writeHead(503, {
163
+ 'Content-Type': 'application/json',
164
+ 'Cache-Control': 'no-store'
165
+ });
166
+ res.end(JSON.stringify(pendingPayload));
167
+ return;
168
+ }
169
+ res.writeHead(503, {
170
+ 'Content-Type': 'text/html; charset=utf-8',
171
+ 'Cache-Control': 'no-store'
172
+ });
173
+ res.end([
174
+ '<!DOCTYPE html>',
175
+ '<html><head><meta charset="utf-8"><title>Zenith Dev Building</title></head>',
176
+ '<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
177
+ '<h1 style="margin-top:0;">Zenith Dev Building</h1>',
178
+ `<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${pathname}\nStatus: initial build running\nHint: ${pendingPayload.hint}</pre>`,
179
+ '</body></html>'
180
+ ].join(''));
181
+ return;
182
+ }
183
+ if (canonicalPath === null) {
184
+ throw new Error('not found');
185
+ }
186
+ const requestExt = extname(canonicalPath);
187
+ if (requestExt && requestExt !== '.html') {
188
+ const assetPath = isStaticExportTarget
189
+ ? resolveWithinDist(outDir, pathname)
190
+ : join(outDir, canonicalPath);
191
+ resolvedPathFor404 = assetPath;
192
+ staticRootFor404 = outDir;
193
+ if (!assetPath) {
194
+ throw new Error('not found');
195
+ }
196
+ const asset = await readFileForRequest(assetPath);
197
+ const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
198
+ res.writeHead(200, { 'Content-Type': mime });
199
+ res.end(asset);
200
+ return;
201
+ }
202
+ const routes = await loadRoutesForRequests();
203
+ const canonicalUrl = new URL(url.toString());
204
+ canonicalUrl.pathname = canonicalPath;
205
+ const resolvedResource = resolveRequestRoute(canonicalUrl, routes.resourceRoutes || []);
206
+ if (resolvedResource.matched && resolvedResource.route) {
207
+ const requestMethod = req.method || 'GET';
208
+ const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
209
+ ? null
210
+ : await readRequestBodyBuffer(req);
211
+ const execution = await executeServerRoute({
212
+ source: resolvedResource.route.server_script || '',
213
+ sourcePath: resolvedResource.route.server_script_path || '',
214
+ params: resolvedResource.params,
215
+ requestUrl: url.toString(),
216
+ requestMethod,
217
+ requestHeaders: req.headers,
218
+ requestBodyBuffer,
219
+ routePattern: resolvedResource.route.path,
220
+ routeFile: resolvedResource.route.server_script_path || '',
221
+ routeId: resolvedResource.route.route_id || '',
222
+ routeKind: 'resource'
223
+ });
224
+ const descriptor = buildResourceResponseDescriptor(execution?.result, configuredBasePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
225
+ res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
226
+ if ((req.method || 'GET').toUpperCase() === 'HEAD') {
227
+ res.end();
228
+ return;
229
+ }
230
+ res.end(descriptor.body);
231
+ return;
232
+ }
233
+ const resolved = resolveRequestRoute(canonicalUrl, routes.pageRoutes || []);
234
+ let filePath = null;
235
+ if (isStaticExportTarget) {
236
+ filePath = toStaticFilePath(outDir, pathname);
237
+ }
238
+ else if (resolved.matched && resolved.route) {
239
+ if (verboseLogging) {
240
+ logger.router(`${req.method || 'GET'} ${pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`);
241
+ }
242
+ const output = resolved.route.output.startsWith('/')
243
+ ? resolved.route.output.slice(1)
244
+ : resolved.route.output;
245
+ filePath = resolveWithinDist(outDir, output);
246
+ }
247
+ else {
248
+ filePath = toStaticFilePath(outDir, canonicalPath);
249
+ }
250
+ resolvedPathFor404 = filePath;
251
+ staticRootFor404 = outDir;
252
+ if (!filePath) {
253
+ throw new Error('not found');
254
+ }
255
+ let ssrPayload = null;
256
+ let routeExecution = null;
257
+ if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
258
+ try {
259
+ const requestMethod = req.method || 'GET';
260
+ const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
261
+ ? null
262
+ : await readRequestBodyBuffer(req);
263
+ routeExecution = await executeServerRoute({
264
+ source: resolved.route.server_script,
265
+ sourcePath: resolved.route.server_script_path || '',
266
+ params: resolved.params,
267
+ requestUrl: url.toString(),
268
+ requestMethod,
269
+ requestHeaders: req.headers,
270
+ requestBodyBuffer,
271
+ routePattern: resolved.route.path,
272
+ routeFile: resolved.route.server_script_path || '',
273
+ routeId: resolved.route.route_id || ''
274
+ });
275
+ }
276
+ catch (error) {
277
+ logServerException('dev server route execution failed', error);
278
+ ssrPayload = {
279
+ __zenith_error: {
280
+ status: 500,
281
+ code: 'LOAD_FAILED',
282
+ message: error instanceof Error ? error.message : String(error || '')
283
+ }
284
+ };
285
+ }
286
+ const traceResult = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
287
+ const routeId = resolved.route.route_id || '';
288
+ const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
289
+ if (verboseLogging) {
290
+ logger.router(`${routeId || resolved.route.path} guard=${traceResult.guard} action=${traceResult.action} load=${traceResult.load}`);
291
+ }
292
+ const result = routeExecution?.result;
293
+ if (result && result.kind === 'redirect') {
294
+ const status = Number.isInteger(result.status) ? result.status : 302;
295
+ res.writeHead(status, appendSetCookieHeaders({
296
+ Location: appLocalRedirectLocation(result.location, configuredBasePath),
297
+ 'Cache-Control': 'no-store'
298
+ }, setCookies));
299
+ res.end('');
300
+ return;
301
+ }
302
+ if (result && result.kind === 'deny') {
303
+ const status = Number.isInteger(result.status) ? result.status : 403;
304
+ res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
305
+ res.end(clientFacingRouteMessage(status, result.message));
306
+ return;
307
+ }
308
+ if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
309
+ ssrPayload = result.data;
310
+ }
311
+ }
312
+ let content = await readFileForRequest(filePath, 'utf8');
313
+ if (resolved.matched) {
314
+ content = await materializeImageMarkup({
315
+ html: content,
316
+ payload: buildSession.getImageRuntimePayload(),
317
+ imageMaterialization: Array.isArray(resolved.route?.image_materialization)
318
+ ? resolved.route.image_materialization
319
+ : []
320
+ });
321
+ }
322
+ if (ssrPayload) {
323
+ content = injectSsrPayload(content, ssrPayload);
324
+ }
325
+ if (!IMAGE_RUNTIME_TAG_RE.test(content)) {
326
+ content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
327
+ }
328
+ res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, appendSetCookieHeaders({
329
+ 'Content-Type': 'text/html'
330
+ }, Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : []));
331
+ res.end(content);
332
+ }
333
+ catch (error) {
334
+ const category = classifyNotFound(pathname);
335
+ const cause = infer404Cause(category, state.buildStatus);
336
+ const payload = buildNotFoundPayload({
337
+ pathname,
338
+ category,
339
+ cause,
340
+ buildId: state.buildId,
341
+ buildStatus: state.buildStatus,
342
+ configuredBasePath,
343
+ currentCssHref: state.currentCssHref
344
+ });
345
+ if (state.buildStatus === 'error' && typeof state.buildError?.message === 'string') {
346
+ payload.buildError = state.buildError.message.length > 600
347
+ ? `${state.buildError.message.slice(0, 597)}...`
348
+ : state.buildError.message;
349
+ }
350
+ const displayCategory = category === 'page' ? 'page' : 'asset';
351
+ logger.warn(`404 ${displayCategory}: ${pathname} (buildId=${state.buildId}) -> cause: ${payload.cause || cause || 'not found'}`);
352
+ trace404(req, url, {
353
+ reason: 'not_found',
354
+ category,
355
+ cause: payload.cause || cause || 'not_found',
356
+ staticRoot: staticRootFor404,
357
+ resolvedPath: resolvedPathFor404,
358
+ error: error instanceof Error ? error.message : String(error || '')
359
+ });
360
+ if (looksLikeJsonRequest(req, pathname)) {
361
+ res.writeHead(404, {
362
+ 'Content-Type': 'application/json',
363
+ 'Cache-Control': 'no-store'
364
+ });
365
+ res.end(JSON.stringify(payload));
366
+ return;
367
+ }
368
+ res.writeHead(404, {
369
+ 'Content-Type': 'text/html; charset=utf-8',
370
+ 'Cache-Control': 'no-store'
371
+ });
372
+ res.end(renderNotFoundHtml(payload));
373
+ return;
374
+ }
375
+ };
376
+ }
@@ -0,0 +1,9 @@
1
+ export function handleRouteCheckRequest({ req, res, url, configuredBasePath, routeCheckEnabled, state, loadRoutesForRequests }: {
2
+ req: any;
3
+ res: any;
4
+ url: any;
5
+ configuredBasePath: any;
6
+ routeCheckEnabled: any;
7
+ state: any;
8
+ loadRoutesForRequests: any;
9
+ }): Promise<void>;
@@ -0,0 +1,100 @@
1
+ import { appLocalRedirectLocation, stripBasePath } from '../base-path.js';
2
+ import { sanitizeRouteResult } from '../server-error.js';
3
+ import { executeServerRoute } from '../preview.js';
4
+ import { resolveRequestRoute } from '../server/resolve-request-route.js';
5
+ export async function handleRouteCheckRequest({ req, res, url, configuredBasePath, routeCheckEnabled, state, loadRoutesForRequests }) {
6
+ try {
7
+ if (!routeCheckEnabled) {
8
+ res.writeHead(501, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
9
+ res.end(JSON.stringify({ error: 'route_check_unsupported' }));
10
+ return;
11
+ }
12
+ if (!state.initialBuildSettled && state.buildStatus === 'building') {
13
+ res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
14
+ res.end(JSON.stringify({
15
+ error: 'initial_build_pending',
16
+ message: 'initial build still in progress'
17
+ }));
18
+ return;
19
+ }
20
+ // Security: Require explicitly designated header to prevent public oracle probing
21
+ if (req.headers['x-zenith-route-check'] !== '1') {
22
+ res.writeHead(403, { 'Content-Type': 'application/json' });
23
+ res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
24
+ return;
25
+ }
26
+ const targetPath = String(url.searchParams.get('path') || '/');
27
+ // Security: Prevent protocol/domain injection in path
28
+ if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
29
+ res.writeHead(400, { 'Content-Type': 'application/json' });
30
+ res.end(JSON.stringify({ error: 'invalid_path_format' }));
31
+ return;
32
+ }
33
+ const targetUrl = new URL(targetPath, url.origin);
34
+ if (targetUrl.origin !== url.origin) {
35
+ res.writeHead(400, { 'Content-Type': 'application/json' });
36
+ res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
37
+ return;
38
+ }
39
+ const canonicalTargetPath = stripBasePath(targetUrl.pathname, configuredBasePath);
40
+ if (canonicalTargetPath === null) {
41
+ res.writeHead(404, { 'Content-Type': 'application/json' });
42
+ res.end(JSON.stringify({ error: 'route_not_found' }));
43
+ return;
44
+ }
45
+ const canonicalTargetUrl = new URL(targetUrl.toString());
46
+ canonicalTargetUrl.pathname = canonicalTargetPath;
47
+ const routes = await loadRoutesForRequests();
48
+ const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes.pageRoutes || []);
49
+ if (!resolvedCheck.matched || !resolvedCheck.route) {
50
+ res.writeHead(404, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify({ error: 'route_not_found' }));
52
+ return;
53
+ }
54
+ const checkResult = await executeServerRoute({
55
+ source: resolvedCheck.route.server_script || '',
56
+ sourcePath: resolvedCheck.route.server_script_path || '',
57
+ params: resolvedCheck.params,
58
+ requestUrl: targetUrl.toString(),
59
+ requestMethod: req.method || 'GET',
60
+ requestHeaders: req.headers,
61
+ routePattern: resolvedCheck.route.path,
62
+ routeFile: resolvedCheck.route.server_script_path || '',
63
+ routeId: resolvedCheck.route.route_id || '',
64
+ guardOnly: true
65
+ });
66
+ // Security: Enforce relative or same-origin redirects
67
+ if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
68
+ const loc = appLocalRedirectLocation(checkResult.result.location || '/', configuredBasePath);
69
+ checkResult.result.location = loc;
70
+ if (loc.includes('://') || loc.startsWith('//')) {
71
+ try {
72
+ const parsedLoc = new URL(loc);
73
+ if (parsedLoc.origin !== targetUrl.origin) {
74
+ checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
75
+ }
76
+ }
77
+ catch {
78
+ checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
79
+ }
80
+ }
81
+ }
82
+ res.writeHead(200, {
83
+ 'Content-Type': 'application/json',
84
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
85
+ 'Pragma': 'no-cache',
86
+ 'Expires': '0',
87
+ 'Vary': 'Cookie'
88
+ });
89
+ res.end(JSON.stringify({
90
+ result: sanitizeRouteResult(checkResult?.result || checkResult),
91
+ routeId: resolvedCheck.route.route_id || '',
92
+ to: targetUrl.toString()
93
+ }));
94
+ return;
95
+ }
96
+ catch {
97
+ res.writeHead(500, { 'Content-Type': 'application/json' });
98
+ res.end(JSON.stringify({ error: 'route_check_failed' }));
99
+ }
100
+ }
@@ -0,0 +1,5 @@
1
+ export function createDevWatcher(options: any): {
2
+ start: () => void;
3
+ close: () => void;
4
+ activeWatcherCount: () => number;
5
+ };