@zenithbuild/cli 0.7.4 → 0.7.5

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 (58) 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 +56 -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 +70 -14
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +49 -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/page-loop-state.js +1 -1
  22. package/dist/build/server-script.d.ts +2 -1
  23. package/dist/build/server-script.js +7 -3
  24. package/dist/build-output-manifest.d.ts +3 -2
  25. package/dist/build-output-manifest.js +3 -0
  26. package/dist/build.js +29 -17
  27. package/dist/dev-server.js +79 -25
  28. package/dist/download-result.d.ts +14 -0
  29. package/dist/download-result.js +148 -0
  30. package/dist/images/service.d.ts +13 -1
  31. package/dist/images/service.js +45 -15
  32. package/dist/manifest.d.ts +15 -1
  33. package/dist/manifest.js +24 -5
  34. package/dist/preview.d.ts +11 -3
  35. package/dist/preview.js +188 -62
  36. package/dist/request-body.d.ts +0 -1
  37. package/dist/request-body.js +0 -6
  38. package/dist/resource-manifest.d.ts +16 -0
  39. package/dist/resource-manifest.js +53 -0
  40. package/dist/resource-response.d.ts +34 -0
  41. package/dist/resource-response.js +71 -0
  42. package/dist/resource-route-module.d.ts +15 -0
  43. package/dist/resource-route-module.js +129 -0
  44. package/dist/route-check-support.js +1 -1
  45. package/dist/server-contract.d.ts +24 -16
  46. package/dist/server-contract.js +217 -25
  47. package/dist/server-error.d.ts +1 -1
  48. package/dist/server-error.js +2 -0
  49. package/dist/server-output.d.ts +2 -1
  50. package/dist/server-output.js +59 -11
  51. package/dist/server-runtime/node-server.js +34 -4
  52. package/dist/server-runtime/route-render.d.ts +25 -1
  53. package/dist/server-runtime/route-render.js +81 -29
  54. package/dist/server-script-composition.d.ts +4 -2
  55. package/dist/server-script-composition.js +6 -3
  56. package/dist/static-export-paths.d.ts +3 -0
  57. package/dist/static-export-paths.js +160 -0
  58. package/package.json +3 -3
package/dist/preview.js CHANGED
@@ -19,8 +19,10 @@ import { isConfigKeyExplicit, isLoadedConfig, loadConfig, validateConfig } from
19
19
  import { materializeImageMarkup } from './images/materialize.js';
20
20
  import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
21
21
  import { handleImageRequest } from './images/service.js';
22
- import { encodeRequestBodyBase64, readRequestBodyBuffer } from './request-body.js';
22
+ import { readRequestBodyBuffer } from './request-body.js';
23
23
  import { createTrustedOriginResolver } from './request-origin.js';
24
+ import { buildResourceResponseDescriptor } from './resource-response.js';
25
+ import { loadResourceRouteManifest } from './resource-manifest.js';
24
26
  import { supportsTargetRouteCheck } from './route-check-support.js';
25
27
  import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
26
28
  import { createSilentLogger } from './ui/logger.js';
@@ -40,6 +42,12 @@ const MIME_TYPES = {
40
42
  '.gif': 'image/gif'
41
43
  };
42
44
  const IMAGE_RUNTIME_TAG_RE = /<script\b[^>]*\bid=(["'])zenith-image-runtime\1[^>]*>[\s\S]*?<\/script>/i;
45
+ function appendSetCookieHeaders(headers, setCookies = []) {
46
+ if (Array.isArray(setCookies) && setCookies.length > 0) {
47
+ headers['Set-Cookie'] = setCookies.slice();
48
+ }
49
+ return headers;
50
+ }
43
51
  const SERVER_SCRIPT_RUNNER = String.raw `
44
52
  import vm from 'node:vm';
45
53
  import fs from 'node:fs/promises';
@@ -55,6 +63,7 @@ const requestHeaders = JSON.parse(process.env.ZENITH_SERVER_REQUEST_HEADERS || '
55
63
  const routePattern = process.env.ZENITH_SERVER_ROUTE_PATTERN || '';
56
64
  const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
57
65
  const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
66
+ const routeKind = process.env.ZENITH_SERVER_ROUTE_KIND || 'page';
58
67
  const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
59
68
 
60
69
  if (!source.trim()) {
@@ -222,14 +231,39 @@ function ctxData(payload) {
222
231
  data: payload
223
232
  };
224
233
  }
234
+ function ctxJson(payload, status = 200) {
235
+ return {
236
+ kind: 'json',
237
+ data: payload,
238
+ status: Number.isInteger(status) ? status : 200
239
+ };
240
+ }
241
+ function ctxText(body, status = 200) {
242
+ return {
243
+ kind: 'text',
244
+ body: typeof body === 'string' ? body : String(body ?? ''),
245
+ status: Number.isInteger(status) ? status : 200
246
+ };
247
+ }
248
+
249
+ async function readStdinBuffer() {
250
+ const chunks = [];
251
+ for await (const chunk of process.stdin) {
252
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
253
+ }
254
+ return Buffer.concat(chunks);
255
+ }
225
256
 
226
257
  const requestInit = {
227
258
  method: requestMethod,
228
259
  headers: new Headers(safeRequestHeaders)
229
260
  };
230
- const requestBodyBase64 = String(process.env.ZENITH_SERVER_REQUEST_BODY_BASE64 || '');
231
- if (requestMethod !== 'GET' && requestMethod !== 'HEAD' && requestBodyBase64.length > 0) {
232
- requestInit.body = Buffer.from(requestBodyBase64, 'base64');
261
+ const requestBodyBuffer =
262
+ requestMethod !== 'GET' && requestMethod !== 'HEAD'
263
+ ? await readStdinBuffer()
264
+ : Buffer.alloc(0);
265
+ if (requestMethod !== 'GET' && requestMethod !== 'HEAD' && requestBodyBuffer.length > 0) {
266
+ requestInit.body = requestBodyBuffer;
233
267
  requestInit.duplex = 'half';
234
268
  }
235
269
  const requestSnapshot = new Request(requestUrl, requestInit);
@@ -249,28 +283,27 @@ const routeContext = {
249
283
  route: routeMeta,
250
284
  env: {},
251
285
  action: null,
252
- auth: {
253
- async getSession(_ctx) {
254
- return null;
255
- },
256
- async requireSession(_ctx) {
257
- throw ctxRedirect('/login', 302);
258
- }
259
- },
260
286
  allow: ctxAllow,
261
287
  redirect: ctxRedirect,
262
288
  deny: ctxDeny,
263
289
  invalid: ctxInvalid,
264
- data: ctxData
290
+ data: ctxData,
291
+ json: ctxJson,
292
+ text: ctxText
265
293
  };
266
294
 
267
295
  const context = vm.createContext({
268
296
  params: routeParams,
269
297
  ctx: routeContext,
270
298
  fetch: globalThis.fetch,
299
+ Blob: globalThis.Blob,
300
+ File: globalThis.File,
301
+ FormData: globalThis.FormData,
271
302
  Headers: globalThis.Headers,
272
303
  Request: globalThis.Request,
273
304
  Response: globalThis.Response,
305
+ TextEncoder: globalThis.TextEncoder,
306
+ TextDecoder: globalThis.TextDecoder,
274
307
  URL,
275
308
  URLSearchParams,
276
309
  Buffer,
@@ -314,23 +347,31 @@ async function loadFileModule(moduleUrl) {
314
347
  if (moduleCache.has(moduleUrl)) {
315
348
  return moduleCache.get(moduleUrl);
316
349
  }
350
+ const modulePromise = (async () => {
351
+ const filename = fileURLToPath(moduleUrl);
352
+ let code = await fs.readFile(filename, 'utf8');
353
+ code = await transpileIfNeeded(filename, code);
354
+ const module = new vm.SourceTextModule(code, {
355
+ context,
356
+ identifier: moduleUrl,
357
+ initializeImportMeta(meta) {
358
+ meta.url = moduleUrl;
359
+ }
360
+ });
317
361
 
318
- const filename = fileURLToPath(moduleUrl);
319
- let code = await fs.readFile(filename, 'utf8');
320
- code = await transpileIfNeeded(filename, code);
321
- const module = new vm.SourceTextModule(code, {
322
- context,
323
- identifier: moduleUrl,
324
- initializeImportMeta(meta) {
325
- meta.url = moduleUrl;
326
- }
327
- });
362
+ await module.link((specifier, referencingModule) => {
363
+ return linkModule(specifier, referencingModule.identifier);
364
+ });
365
+ return module;
366
+ })();
328
367
 
329
- moduleCache.set(moduleUrl, module);
330
- await module.link((specifier, referencingModule) => {
331
- return linkModule(specifier, referencingModule.identifier);
332
- });
333
- return module;
368
+ moduleCache.set(moduleUrl, modulePromise);
369
+ try {
370
+ return await modulePromise;
371
+ } catch (error) {
372
+ moduleCache.delete(moduleUrl);
373
+ throw error;
374
+ }
334
375
  }
335
376
 
336
377
  async function linkModule(specifier, parentIdentifier) {
@@ -341,11 +382,14 @@ async function linkModule(specifier, parentIdentifier) {
341
382
  return loadFileModule(resolvedUrl);
342
383
  }
343
384
 
344
- const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender']);
385
+ const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender', 'exportPaths']);
345
386
  const prelude = "const params = globalThis.params;\n" +
346
387
  "const ctx = globalThis.ctx;\n" +
347
- "import { resolveRouteResult } from 'zenith:server-contract';\n" +
348
- "globalThis.resolveRouteResult = resolveRouteResult;\n";
388
+ "import { download, resolveRouteResult } from 'zenith:server-contract';\n" +
389
+ "import { attachRouteAuth } from 'zenith:route-auth';\n" +
390
+ "ctx.download = download;\n" +
391
+ "globalThis.resolveRouteResult = resolveRouteResult;\n" +
392
+ "globalThis.attachRouteAuth = attachRouteAuth;\n";
349
393
  const entryIdentifier = sourcePath
350
394
  ? pathToFileURL(sourcePath).href
351
395
  : 'zenith:server-script';
@@ -366,14 +410,35 @@ moduleCache.set(entryIdentifier, entryModule);
366
410
  await entryModule.link((specifier, referencingModule) => {
367
411
  if (specifier === 'zenith:server-contract') {
368
412
  const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
369
- const contractUrl = pathToFileURL(process.env.ZENITH_SERVER_CONTRACT_PATH || defaultPath).href;
370
- return loadFileModule(contractUrl).catch(() =>
413
+ const configuredPath = process.env.ZENITH_SERVER_CONTRACT_PATH || '';
414
+ const contractUrl = pathToFileURL(configuredPath || defaultPath).href;
415
+ if (configuredPath) {
416
+ return loadFileModule(contractUrl);
417
+ }
418
+ return loadFileModule(contractUrl).catch(() =>
419
+ loadFileModule(pathToFileURL(defaultPath).href)
420
+ );
421
+ }
422
+ if (specifier === 'zenith:route-auth') {
423
+ const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'auth', 'route-auth.js');
424
+ const configuredPath = process.env.ZENITH_SERVER_ROUTE_AUTH_PATH || '';
425
+ const authUrl = pathToFileURL(configuredPath || defaultPath).href;
426
+ if (configuredPath) {
427
+ return loadFileModule(authUrl);
428
+ }
429
+ return loadFileModule(authUrl).catch(() =>
371
430
  loadFileModule(pathToFileURL(defaultPath).href)
372
431
  );
373
432
  }
374
433
  return linkModule(specifier, referencingModule.identifier);
375
434
  });
376
435
  await entryModule.evaluate();
436
+ context.attachRouteAuth(routeContext, {
437
+ requestUrl: routeContext.url,
438
+ guardOnly,
439
+ redirect: ctxRedirect,
440
+ deny: ctxDeny
441
+ });
377
442
 
378
443
  const namespaceKeys = Object.keys(entryModule.namespace);
379
444
  for (const key of namespaceKeys) {
@@ -388,7 +453,8 @@ try {
388
453
  exports: exported,
389
454
  ctx: context.ctx,
390
455
  filePath: sourcePath || 'server_script',
391
- guardOnly: guardOnly
456
+ guardOnly: guardOnly,
457
+ routeKind: routeKind
392
458
  });
393
459
 
394
460
  process.stdout.write(JSON.stringify(resolved || null));
@@ -436,7 +502,9 @@ export async function createPreviewServer(options) {
436
502
  const logger = providedLogger || createSilentLogger();
437
503
  const verboseLogging = logger.mode?.logLevel === 'verbose';
438
504
  const configuredBasePath = normalizeBasePath(config.basePath || '/');
439
- const routeCheckEnabled = supportsTargetRouteCheck(resolveBuildAdapter(config).target);
505
+ const resolvedTarget = resolveBuildAdapter(config).target;
506
+ const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
507
+ const isStaticExportTarget = resolvedTarget === 'static-export';
440
508
  let actualPort = port;
441
509
  const resolveServerOrigin = createTrustedOriginResolver({
442
510
  host,
@@ -455,7 +523,7 @@ export async function createPreviewServer(options) {
455
523
  }
456
524
  const server = createServer(async (req, res) => {
457
525
  const url = new URL(req.url, resolveServerOrigin());
458
- const { basePath, routes } = await loadRouteManifestState(distDir, configuredBasePath);
526
+ const { basePath, pageRoutes, resourceRoutes } = await loadRouteSurfaceState(distDir, configuredBasePath);
459
527
  const canonicalPath = stripBasePath(url.pathname, basePath);
460
528
  try {
461
529
  if (url.pathname === routeCheckPath(basePath)) {
@@ -491,7 +559,7 @@ export async function createPreviewServer(options) {
491
559
  }
492
560
  const canonicalTargetUrl = new URL(targetUrl.toString());
493
561
  canonicalTargetUrl.pathname = canonicalTargetPath;
494
- const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes);
562
+ const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, pageRoutes);
495
563
  if (!resolvedCheck.matched || !resolvedCheck.route) {
496
564
  res.writeHead(404, { 'Content-Type': 'application/json' });
497
565
  res.end(JSON.stringify({ error: 'route_not_found' }));
@@ -540,6 +608,9 @@ export async function createPreviewServer(options) {
540
608
  return;
541
609
  }
542
610
  if (url.pathname === imageEndpointPath(basePath)) {
611
+ if (isStaticExportTarget) {
612
+ throw new Error('not found');
613
+ }
543
614
  await handleImageRequest(req, res, {
544
615
  requestUrl: url,
545
616
  projectRoot,
@@ -551,7 +622,9 @@ export async function createPreviewServer(options) {
551
622
  throw new Error('not found');
552
623
  }
553
624
  if (extname(canonicalPath) && extname(canonicalPath) !== '.html') {
554
- const staticPath = resolveWithinDist(distDir, canonicalPath);
625
+ const staticPath = isStaticExportTarget
626
+ ? resolveWithinDist(distDir, url.pathname)
627
+ : resolveWithinDist(distDir, canonicalPath);
555
628
  if (!staticPath || !(await fileExists(staticPath))) {
556
629
  throw new Error('not found');
557
630
  }
@@ -561,9 +634,47 @@ export async function createPreviewServer(options) {
561
634
  res.end(content);
562
635
  return;
563
636
  }
637
+ if (isStaticExportTarget) {
638
+ const directHtmlPath = toStaticFilePath(distDir, url.pathname);
639
+ if (!directHtmlPath || !(await fileExists(directHtmlPath))) {
640
+ throw new Error('not found');
641
+ }
642
+ const html = await readFile(directHtmlPath, 'utf8');
643
+ res.writeHead(200, { 'Content-Type': 'text/html' });
644
+ res.end(html);
645
+ return;
646
+ }
564
647
  const canonicalUrl = new URL(url.toString());
565
648
  canonicalUrl.pathname = canonicalPath;
566
- const resolved = resolveRequestRoute(canonicalUrl, routes);
649
+ const resolvedResource = resolveRequestRoute(canonicalUrl, resourceRoutes);
650
+ if (resolvedResource.matched && resolvedResource.route) {
651
+ const requestMethod = req.method || 'GET';
652
+ const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
653
+ ? null
654
+ : await readRequestBodyBuffer(req);
655
+ const execution = await executeServerRoute({
656
+ source: resolvedResource.route.server_script || '',
657
+ sourcePath: resolvedResource.route.server_script_path || '',
658
+ params: resolvedResource.params,
659
+ requestUrl: url.toString(),
660
+ requestMethod,
661
+ requestHeaders: req.headers,
662
+ requestBodyBuffer,
663
+ routePattern: resolvedResource.route.path,
664
+ routeFile: resolvedResource.route.server_script_path || '',
665
+ routeId: resolvedResource.route.route_id || routeIdFromSourcePath(resolvedResource.route.server_script_path || ''),
666
+ routeKind: 'resource'
667
+ });
668
+ const descriptor = buildResourceResponseDescriptor(execution?.result, basePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
669
+ res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
670
+ if ((req.method || 'GET').toUpperCase() === 'HEAD') {
671
+ res.end();
672
+ return;
673
+ }
674
+ res.end(descriptor.body);
675
+ return;
676
+ }
677
+ const resolved = resolveRequestRoute(canonicalUrl, pageRoutes);
567
678
  let htmlPath = null;
568
679
  if (resolved.matched && resolved.route) {
569
680
  if (verboseLogging) {
@@ -595,7 +706,7 @@ export async function createPreviewServer(options) {
595
706
  requestUrl: url.toString(),
596
707
  requestMethod,
597
708
  requestHeaders: req.headers,
598
- requestBodyBase64: encodeRequestBodyBase64(requestBodyBuffer),
709
+ requestBodyBuffer,
599
710
  routePattern: resolved.route.path,
600
711
  routeFile: resolved.route.server_script_path || '',
601
712
  routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
@@ -613,22 +724,23 @@ export async function createPreviewServer(options) {
613
724
  }
614
725
  const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
615
726
  const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
727
+ const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
616
728
  if (verboseLogging) {
617
729
  logger.router(`${routeId} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
618
730
  }
619
731
  const result = routeExecution?.result;
620
732
  if (result && result.kind === 'redirect') {
621
733
  const status = Number.isInteger(result.status) ? result.status : 302;
622
- res.writeHead(status, {
734
+ res.writeHead(status, appendSetCookieHeaders({
623
735
  Location: appLocalRedirectLocation(result.location, basePath),
624
736
  'Cache-Control': 'no-store'
625
- });
737
+ }, setCookies));
626
738
  res.end('');
627
739
  return;
628
740
  }
629
741
  if (result && result.kind === 'deny') {
630
742
  const status = Number.isInteger(result.status) ? result.status : 403;
631
- res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
743
+ res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
632
744
  res.end(clientFacingRouteMessage(status, result.message));
633
745
  return;
634
746
  }
@@ -652,9 +764,9 @@ export async function createPreviewServer(options) {
652
764
  if (!IMAGE_RUNTIME_TAG_RE.test(html)) {
653
765
  html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath));
654
766
  }
655
- res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, {
767
+ res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, appendSetCookieHeaders({
656
768
  'Content-Type': 'text/html'
657
- });
769
+ }, Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : []));
658
770
  res.end(html);
659
771
  }
660
772
  catch {
@@ -696,38 +808,42 @@ export async function createPreviewServer(options) {
696
808
  * @returns {Promise<PreviewRoute[]>}
697
809
  */
698
810
  export async function loadRouteManifest(distDir) {
699
- const state = await loadRouteManifestState(distDir, '/');
700
- return state.routes;
811
+ const state = await loadRouteSurfaceState(distDir, '/');
812
+ return state.pageRoutes;
701
813
  }
702
- async function loadRouteManifestState(distDir, fallbackBasePath = '/') {
814
+ export async function loadRouteSurfaceState(distDir, fallbackBasePath = '/') {
703
815
  const manifestPath = join(distDir, 'assets', 'router-manifest.json');
816
+ const resourceState = await loadResourceRouteManifest(distDir, normalizeBasePath(fallbackBasePath || '/'));
704
817
  try {
705
818
  const source = await readFile(manifestPath, 'utf8');
706
819
  const parsed = JSON.parse(source);
707
820
  const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
821
+ const basePath = normalizeBasePath(parsed?.base_path || resourceState.basePath || fallbackBasePath || '/');
708
822
  return {
709
- basePath: normalizeBasePath(parsed?.base_path || fallbackBasePath || '/'),
710
- routes: routes
823
+ basePath,
824
+ pageRoutes: routes
711
825
  .filter((entry) => entry &&
712
826
  typeof entry === 'object' &&
713
827
  typeof entry.path === 'string' &&
714
828
  typeof entry.output === 'string')
715
- .sort((a, b) => compareRouteSpecificity(a.path, b.path))
829
+ .sort((a, b) => compareRouteSpecificity(a.path, b.path)),
830
+ resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
716
831
  };
717
832
  }
718
833
  catch {
719
834
  return {
720
- basePath: normalizeBasePath(fallbackBasePath || '/'),
721
- routes: []
835
+ basePath: normalizeBasePath(resourceState.basePath || fallbackBasePath || '/'),
836
+ pageRoutes: [],
837
+ resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
722
838
  };
723
839
  }
724
840
  }
725
841
  export const matchRoute = matchManifestRoute;
726
842
  /**
727
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
728
- * @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
843
+ * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource' }} input
844
+ * @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
729
845
  */
730
- export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBase64, routePattern, routeFile, routeId, guardOnly = false }) {
846
+ export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false }) {
731
847
  if (!source || !String(source).trim()) {
732
848
  return {
733
849
  result: { kind: 'data', data: {} },
@@ -741,10 +857,11 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
741
857
  requestUrl: requestUrl || 'http://localhost/',
742
858
  requestMethod: requestMethod || 'GET',
743
859
  requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
744
- requestBodyBase64: requestBodyBase64 || '',
860
+ requestBodyBuffer: Buffer.isBuffer(requestBodyBuffer) ? requestBodyBuffer : null,
745
861
  routePattern: routePattern || '',
746
862
  routeFile: routeFile || sourcePath || '',
747
863
  routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
864
+ routeKind,
748
865
  guardOnly
749
866
  });
750
867
  if (payload === null || payload === undefined) {
@@ -779,7 +896,10 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
779
896
  load: String(trace.load || 'none')
780
897
  }
781
898
  : { guard: 'none', action: 'none', load: 'none' },
782
- status: Number.isInteger(payload.status) ? payload.status : undefined
899
+ status: Number.isInteger(payload.status) ? payload.status : undefined,
900
+ setCookies: Array.isArray(payload.setCookies)
901
+ ? payload.setCookies.filter((value) => typeof value === 'string' && value.length > 0)
902
+ : []
783
903
  };
784
904
  }
785
905
  return {
@@ -825,7 +945,7 @@ export async function executeServerScript(input) {
825
945
  return {};
826
946
  }
827
947
  /**
828
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, routePattern: string, routeFile: string, routeId: string }} input
948
+ * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, requestBodyBuffer?: Buffer | null, routePattern: string, routeFile: string, routeId: string, routeKind?: 'page' | 'resource' }} input
829
949
  * @returns {Promise<unknown>}
830
950
  */
831
951
  function spawnNodeServerRunner(input) {
@@ -839,15 +959,21 @@ function spawnNodeServerRunner(input) {
839
959
  ZENITH_SERVER_REQUEST_URL: input.requestUrl || 'http://localhost/',
840
960
  ZENITH_SERVER_REQUEST_METHOD: input.requestMethod || 'GET',
841
961
  ZENITH_SERVER_REQUEST_HEADERS: JSON.stringify(input.requestHeaders || {}),
842
- ZENITH_SERVER_REQUEST_BODY_BASE64: input.requestBodyBase64 || '',
843
962
  ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
844
963
  ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
845
964
  ZENITH_SERVER_ROUTE_ID: input.routeId || '',
965
+ ZENITH_SERVER_ROUTE_KIND: input.routeKind || 'page',
846
966
  ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
847
- ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js')
967
+ ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js'),
968
+ ZENITH_SERVER_ROUTE_AUTH_PATH: join(__dirname, 'auth', 'route-auth.js')
848
969
  },
849
- stdio: ['ignore', 'pipe', 'pipe']
970
+ stdio: ['pipe', 'pipe', 'pipe']
971
+ });
972
+ const runnerRequestBody = Buffer.isBuffer(input.requestBodyBuffer) ? input.requestBodyBuffer : null;
973
+ child.stdin.on('error', () => {
974
+ // ignore broken pipes when the runner exits before consuming stdin
850
975
  });
976
+ child.stdin.end(runnerRequestBody && runnerRequestBody.length > 0 ? runnerRequestBody : undefined);
851
977
  let stdout = '';
852
978
  let stderr = '';
853
979
  child.stdout.on('data', (chunk) => {
@@ -1,2 +1 @@
1
1
  export function readRequestBodyBuffer(req: any): Promise<Buffer<ArrayBuffer>>;
2
- export function encodeRequestBodyBase64(bodyBuffer: any): string;
@@ -5,9 +5,3 @@ export async function readRequestBodyBuffer(req) {
5
5
  }
6
6
  return Buffer.concat(chunks);
7
7
  }
8
- export function encodeRequestBodyBase64(bodyBuffer) {
9
- if (!Buffer.isBuffer(bodyBuffer) || bodyBuffer.length === 0) {
10
- return '';
11
- }
12
- return bodyBuffer.toString('base64');
13
- }
@@ -0,0 +1,16 @@
1
+ export function writeResourceRouteManifest(staticDir: any, routeManifest: any, basePath?: string): Promise<({
2
+ path: any;
3
+ file: any;
4
+ route_kind: string;
5
+ server_script: any;
6
+ server_script_path: any;
7
+ has_guard: boolean;
8
+ has_load: boolean;
9
+ has_action: boolean;
10
+ params: any;
11
+ route_id: any;
12
+ } | null)[]>;
13
+ export function loadResourceRouteManifest(distDir: any, fallbackBasePath?: string): Promise<{
14
+ basePath: any;
15
+ routes: any;
16
+ }>;
@@ -0,0 +1,53 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { compareRouteSpecificity } from './server/resolve-request-route.js';
4
+ function sanitizeResourceRoute(entry) {
5
+ if (!entry || typeof entry !== 'object') {
6
+ return null;
7
+ }
8
+ if (typeof entry.path !== 'string' || typeof entry.server_script !== 'string') {
9
+ return null;
10
+ }
11
+ return {
12
+ path: entry.path,
13
+ file: typeof entry.file === 'string' ? entry.file : '',
14
+ route_kind: 'resource',
15
+ server_script: entry.server_script,
16
+ server_script_path: typeof entry.server_script_path === 'string' ? entry.server_script_path : '',
17
+ has_guard: entry.has_guard === true,
18
+ has_load: entry.has_load === true,
19
+ has_action: entry.has_action === true,
20
+ params: Array.isArray(entry.params) ? entry.params.filter((value) => typeof value === 'string') : [],
21
+ route_id: typeof entry.route_id === 'string' ? entry.route_id : null
22
+ };
23
+ }
24
+ export async function writeResourceRouteManifest(staticDir, routeManifest, basePath = '/') {
25
+ const routes = (Array.isArray(routeManifest) ? routeManifest : [])
26
+ .filter((entry) => entry?.route_kind === 'resource')
27
+ .map((entry) => sanitizeResourceRoute(entry))
28
+ .filter(Boolean)
29
+ .sort((left, right) => compareRouteSpecificity(left.path, right.path));
30
+ const manifestPath = join(staticDir, 'assets', 'resource-manifest.json');
31
+ await mkdir(join(staticDir, 'assets'), { recursive: true });
32
+ await writeFile(manifestPath, `${JSON.stringify({ base_path: basePath, routes }, null, 2)}\n`, 'utf8');
33
+ return routes;
34
+ }
35
+ export async function loadResourceRouteManifest(distDir, fallbackBasePath = '/') {
36
+ const manifestPath = join(distDir, 'assets', 'resource-manifest.json');
37
+ try {
38
+ const parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
39
+ return {
40
+ basePath: typeof parsed?.base_path === 'string' ? parsed.base_path : fallbackBasePath,
41
+ routes: (Array.isArray(parsed?.routes) ? parsed.routes : [])
42
+ .map((entry) => sanitizeResourceRoute(entry))
43
+ .filter(Boolean)
44
+ .sort((left, right) => compareRouteSpecificity(left.path, right.path))
45
+ };
46
+ }
47
+ catch {
48
+ return {
49
+ basePath: fallbackBasePath,
50
+ routes: []
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,34 @@
1
+ export function buildResourceResponseDescriptor(result: any, basePath?: string, setCookies?: any[]): {
2
+ status: any;
3
+ headers: {
4
+ Location: string;
5
+ 'Cache-Control': string;
6
+ 'Content-Type'?: undefined;
7
+ 'Content-Disposition'?: undefined;
8
+ 'Content-Length'?: undefined;
9
+ };
10
+ body: string;
11
+ setCookies: any[];
12
+ } | {
13
+ status: any;
14
+ headers: {
15
+ 'Content-Type': string;
16
+ Location?: undefined;
17
+ 'Cache-Control'?: undefined;
18
+ 'Content-Disposition'?: undefined;
19
+ 'Content-Length'?: undefined;
20
+ };
21
+ body: any;
22
+ setCookies: any[];
23
+ } | {
24
+ status: number;
25
+ headers: {
26
+ 'Content-Type': any;
27
+ 'Content-Disposition': string;
28
+ 'Content-Length': string;
29
+ Location?: undefined;
30
+ 'Cache-Control'?: undefined;
31
+ };
32
+ body: Buffer<ArrayBuffer>;
33
+ setCookies: any[];
34
+ };