@zenithbuild/cli 0.7.2 → 0.7.4

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 (54) hide show
  1. package/README.md +14 -11
  2. package/dist/adapters/adapter-netlify.js +1 -0
  3. package/dist/adapters/adapter-node.js +8 -0
  4. package/dist/adapters/adapter-vercel.js +1 -0
  5. package/dist/build/compiler-runtime.d.ts +10 -9
  6. package/dist/build/compiler-runtime.js +51 -1
  7. package/dist/build/compiler-signal-expression.d.ts +1 -0
  8. package/dist/build/compiler-signal-expression.js +155 -0
  9. package/dist/build/expression-rewrites.d.ts +1 -6
  10. package/dist/build/expression-rewrites.js +61 -65
  11. package/dist/build/page-component-loop.d.ts +3 -13
  12. package/dist/build/page-component-loop.js +21 -46
  13. package/dist/build/page-ir-normalization.d.ts +0 -8
  14. package/dist/build/page-ir-normalization.js +13 -234
  15. package/dist/build/page-loop-state.d.ts +6 -9
  16. package/dist/build/page-loop-state.js +9 -8
  17. package/dist/build/page-loop.js +27 -22
  18. package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
  19. package/dist/build/scoped-identifier-rewrite.js +28 -128
  20. package/dist/build/server-script.d.ts +2 -1
  21. package/dist/build/server-script.js +29 -3
  22. package/dist/build.js +5 -3
  23. package/dist/component-instance-ir.js +158 -52
  24. package/dist/dev-build-session.js +20 -6
  25. package/dist/dev-server.js +82 -39
  26. package/dist/framework-components/Image.zen +1 -1
  27. package/dist/images/materialization-plan.d.ts +1 -0
  28. package/dist/images/materialization-plan.js +6 -0
  29. package/dist/images/materialize.d.ts +5 -3
  30. package/dist/images/materialize.js +24 -109
  31. package/dist/images/router-manifest.d.ts +1 -0
  32. package/dist/images/router-manifest.js +49 -0
  33. package/dist/index.js +8 -2
  34. package/dist/manifest.js +3 -2
  35. package/dist/preview.d.ts +4 -3
  36. package/dist/preview.js +87 -53
  37. package/dist/request-body.d.ts +2 -0
  38. package/dist/request-body.js +13 -0
  39. package/dist/request-origin.d.ts +2 -0
  40. package/dist/request-origin.js +45 -0
  41. package/dist/route-check-support.d.ts +1 -0
  42. package/dist/route-check-support.js +4 -0
  43. package/dist/server-contract.d.ts +15 -0
  44. package/dist/server-contract.js +102 -32
  45. package/dist/server-error.d.ts +4 -0
  46. package/dist/server-error.js +34 -0
  47. package/dist/server-output.d.ts +2 -0
  48. package/dist/server-output.js +13 -0
  49. package/dist/server-runtime/node-server.js +33 -27
  50. package/dist/server-runtime/route-render.d.ts +3 -3
  51. package/dist/server-runtime/route-render.js +20 -31
  52. package/dist/server-script-composition.d.ts +11 -5
  53. package/dist/server-script-composition.js +25 -10
  54. package/package.json +6 -2
@@ -1,10 +1,10 @@
1
1
  // server-contract.js — Zenith CLI V0
2
2
  // ---------------------------------------------------------------------------
3
3
  // Shared validation and payload resolution logic for <script server> blocks.
4
- const NEW_KEYS = new Set(['data', 'load', 'guard', 'prerender']);
4
+ const NEW_KEYS = new Set(['data', 'load', 'guard', 'action', 'prerender']);
5
5
  const LEGACY_KEYS = new Set(['ssr_data', 'props', 'ssr', 'prerender']);
6
- const ALLOWED_KEYS = new Set(['data', 'load', 'guard', 'prerender', 'ssr_data', 'props', 'ssr']);
7
- const ROUTE_RESULT_KINDS = new Set(['allow', 'redirect', 'deny', 'data']);
6
+ const ALLOWED_KEYS = new Set(['data', 'load', 'guard', 'action', 'prerender', 'ssr_data', 'props', 'ssr']);
7
+ const ROUTE_RESULT_KINDS = new Set(['allow', 'redirect', 'deny', 'data', 'invalid']);
8
8
  export function allow() {
9
9
  return { kind: 'allow' };
10
10
  }
@@ -25,6 +25,13 @@ export function deny(status = 403, message = undefined) {
25
25
  export function data(payload) {
26
26
  return { kind: 'data', data: payload };
27
27
  }
28
+ export function invalid(payload, status = 400) {
29
+ return {
30
+ kind: 'invalid',
31
+ data: payload,
32
+ status: Number.isInteger(status) ? status : 400
33
+ };
34
+ }
28
35
  function isRouteResultLike(value) {
29
36
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
30
37
  return false;
@@ -57,6 +64,44 @@ function assertValidRouteResultShape(value, where, allowedKinds) {
57
64
  throw new Error(`[Zenith] ${where}: deny message must be a string when provided.`);
58
65
  }
59
66
  }
67
+ if (kind === 'invalid') {
68
+ if (!Number.isInteger(value.status) || (value.status !== 400 && value.status !== 422)) {
69
+ throw new Error(`[Zenith] ${where}: invalid status must be 400 or 422.`);
70
+ }
71
+ }
72
+ }
73
+ function assertOneArgRouteFunction({ filePath, exportName, value }) {
74
+ if (typeof value !== 'function') {
75
+ throw new Error(`[Zenith] ${filePath}: "${exportName}" must be a function.`);
76
+ }
77
+ if (value.length !== 1) {
78
+ throw new Error(`[Zenith] ${filePath}: "${exportName}(ctx)" must take exactly 1 argument.`);
79
+ }
80
+ const fnStr = value.toString();
81
+ const paramsMatch = fnStr.match(/^[^{=]+\(([^)]*)\)/);
82
+ if (paramsMatch && paramsMatch[1].includes('...')) {
83
+ throw new Error(`[Zenith] ${filePath}: "${exportName}(ctx)" must not contain rest parameters.`);
84
+ }
85
+ }
86
+ function buildActionState(result) {
87
+ if (!result || typeof result !== 'object') {
88
+ return null;
89
+ }
90
+ if (result.kind === 'data') {
91
+ return {
92
+ ok: true,
93
+ status: 200,
94
+ data: result.data
95
+ };
96
+ }
97
+ if (result.kind === 'invalid') {
98
+ return {
99
+ ok: false,
100
+ status: Number.isInteger(result.status) ? result.status : 400,
101
+ data: result.data
102
+ };
103
+ }
104
+ return null;
60
105
  }
61
106
  export function validateServerExports({ exports, filePath }) {
62
107
  const exportKeys = Object.keys(exports);
@@ -67,7 +112,8 @@ export function validateServerExports({ exports, filePath }) {
67
112
  const hasData = 'data' in exports;
68
113
  const hasLoad = 'load' in exports;
69
114
  const hasGuard = 'guard' in exports;
70
- const hasNew = hasData || hasLoad;
115
+ const hasAction = 'action' in exports;
116
+ const hasNew = hasData || hasLoad || hasAction;
71
117
  const hasLegacy = ('ssr_data' in exports) || ('props' in exports) || ('ssr' in exports);
72
118
  if (hasData && hasLoad) {
73
119
  throw new Error(`[Zenith] ${filePath}: cannot export both "data" and "load". Choose one.`);
@@ -78,31 +124,14 @@ export function validateServerExports({ exports, filePath }) {
78
124
  if ('prerender' in exports && typeof exports.prerender !== 'boolean') {
79
125
  throw new Error(`[Zenith] ${filePath}: "prerender" must be a boolean.`);
80
126
  }
81
- if (hasLoad && typeof exports.load !== 'function') {
82
- throw new Error(`[Zenith] ${filePath}: "load" must be a function.`);
83
- }
84
127
  if (hasLoad) {
85
- if (exports.load.length !== 1) {
86
- throw new Error(`[Zenith] ${filePath}: "load(ctx)" must take exactly 1 argument.`);
87
- }
88
- const fnStr = exports.load.toString();
89
- const paramsMatch = fnStr.match(/^[^{=]+\(([^)]*)\)/);
90
- if (paramsMatch && paramsMatch[1].includes('...')) {
91
- throw new Error(`[Zenith] ${filePath}: "load(ctx)" must not contain rest parameters.`);
92
- }
93
- }
94
- if (hasGuard && typeof exports.guard !== 'function') {
95
- throw new Error(`[Zenith] ${filePath}: "guard" must be a function.`);
128
+ assertOneArgRouteFunction({ filePath, exportName: 'load', value: exports.load });
96
129
  }
97
130
  if (hasGuard) {
98
- if (exports.guard.length !== 1) {
99
- throw new Error(`[Zenith] ${filePath}: "guard(ctx)" must take exactly 1 argument.`);
100
- }
101
- const fnStr = exports.guard.toString();
102
- const paramsMatch = fnStr.match(/^[^{=]+\(([^)]*)\)/);
103
- if (paramsMatch && paramsMatch[1].includes('...')) {
104
- throw new Error(`[Zenith] ${filePath}: "guard(ctx)" must not contain rest parameters.`);
105
- }
131
+ assertOneArgRouteFunction({ filePath, exportName: 'guard', value: exports.guard });
132
+ }
133
+ if (hasAction) {
134
+ assertOneArgRouteFunction({ filePath, exportName: 'action', value: exports.action });
106
135
  }
107
136
  }
108
137
  export function assertJsonSerializable(value, where = 'payload') {
@@ -160,8 +189,15 @@ export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = f
160
189
  validateServerExports({ exports, filePath });
161
190
  const trace = {
162
191
  guard: 'none',
192
+ action: 'none',
163
193
  load: 'none'
164
194
  };
195
+ let responseStatus = 200;
196
+ const requestMethod = String(ctx?.method || ctx?.request?.method || 'GET').toUpperCase();
197
+ const isActionRequest = !guardOnly && requestMethod === 'POST';
198
+ if (ctx && typeof ctx === 'object') {
199
+ ctx.action = null;
200
+ }
165
201
  if ('guard' in exports) {
166
202
  const guardRaw = await exports.guard(ctx);
167
203
  const guardResult = guardRaw == null ? allow() : guardRaw;
@@ -177,6 +213,32 @@ export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = f
177
213
  if (guardOnly) {
178
214
  return { result: allow(), trace };
179
215
  }
216
+ if (isActionRequest && 'action' in exports) {
217
+ const actionRaw = await exports.action(ctx);
218
+ let actionResult = null;
219
+ if (isRouteResultLike(actionRaw)) {
220
+ actionResult = actionRaw;
221
+ assertValidRouteResultShape(actionResult, `${filePath}: action(ctx) return`, new Set(['data', 'invalid', 'redirect', 'deny']));
222
+ if (actionResult.kind === 'data' || actionResult.kind === 'invalid') {
223
+ assertJsonSerializable(actionResult.data, `${filePath}: action(ctx) return`);
224
+ }
225
+ }
226
+ else {
227
+ assertJsonSerializable(actionRaw, `${filePath}: action(ctx) return`);
228
+ actionResult = data(actionRaw);
229
+ }
230
+ trace.action = actionResult.kind;
231
+ if (actionResult.kind === 'redirect' || actionResult.kind === 'deny') {
232
+ return { result: actionResult, trace };
233
+ }
234
+ const actionState = buildActionState(actionResult);
235
+ if (ctx && typeof ctx === 'object') {
236
+ ctx.action = actionState;
237
+ }
238
+ if (actionState && actionState.ok === false) {
239
+ responseStatus = actionState.status;
240
+ }
241
+ }
180
242
  let payload;
181
243
  if ('load' in exports) {
182
244
  const loadRaw = await exports.load(ctx);
@@ -190,34 +252,42 @@ export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = f
190
252
  loadResult = data(loadRaw);
191
253
  }
192
254
  trace.load = loadResult.kind;
193
- return { result: loadResult, trace };
255
+ return { result: loadResult, trace, status: loadResult.kind === 'data' ? responseStatus : undefined };
194
256
  }
195
257
  if ('data' in exports) {
196
258
  payload = exports.data;
197
259
  assertJsonSerializable(payload, `${filePath}: data export`);
198
260
  trace.load = 'data';
199
- return { result: data(payload), trace };
261
+ return { result: data(payload), trace, status: responseStatus };
200
262
  }
201
263
  // legacy fallback
202
264
  if ('ssr_data' in exports) {
203
265
  payload = exports.ssr_data;
204
266
  assertJsonSerializable(payload, `${filePath}: ssr_data export`);
205
267
  trace.load = 'data';
206
- return { result: data(payload), trace };
268
+ return { result: data(payload), trace, status: responseStatus };
207
269
  }
208
270
  if ('props' in exports) {
209
271
  payload = exports.props;
210
272
  assertJsonSerializable(payload, `${filePath}: props export`);
211
273
  trace.load = 'data';
212
- return { result: data(payload), trace };
274
+ return { result: data(payload), trace, status: responseStatus };
213
275
  }
214
276
  if ('ssr' in exports) {
215
277
  payload = exports.ssr;
216
278
  assertJsonSerializable(payload, `${filePath}: ssr export`);
217
279
  trace.load = 'data';
218
- return { result: data(payload), trace };
280
+ return { result: data(payload), trace, status: responseStatus };
281
+ }
282
+ if (isActionRequest && ctx?.action) {
283
+ trace.load = 'data';
284
+ return {
285
+ result: data({ action: ctx.action }),
286
+ trace,
287
+ status: responseStatus
288
+ };
219
289
  }
220
- return { result: data({}), trace };
290
+ return { result: data({}), trace, status: responseStatus };
221
291
  }
222
292
  export async function resolveServerPayload({ exports, ctx, filePath }) {
223
293
  const resolved = await resolveRouteResult({ exports, ctx, filePath });
@@ -0,0 +1,4 @@
1
+ export function defaultRouteDenyMessage(status: any): "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
2
+ export function clientFacingRouteMessage(status: any, message: any): string;
3
+ export function sanitizeRouteResult(result: any): any;
4
+ export function logServerException(scope: any, error: any): void;
@@ -0,0 +1,34 @@
1
+ export function defaultRouteDenyMessage(status) {
2
+ if (status === 401)
3
+ return 'Unauthorized';
4
+ if (status === 403)
5
+ return 'Forbidden';
6
+ if (status === 404)
7
+ return 'Not Found';
8
+ return 'Internal Server Error';
9
+ }
10
+ export function clientFacingRouteMessage(status, message) {
11
+ const resolvedStatus = Number.isInteger(status) ? status : 500;
12
+ if (resolvedStatus >= 500) {
13
+ return defaultRouteDenyMessage(resolvedStatus);
14
+ }
15
+ const resolvedMessage = typeof message === 'string' ? message : '';
16
+ return resolvedMessage || defaultRouteDenyMessage(resolvedStatus);
17
+ }
18
+ export function sanitizeRouteResult(result) {
19
+ if (!result || typeof result !== 'object' || Array.isArray(result) || result.kind !== 'deny') {
20
+ return result;
21
+ }
22
+ const status = Number.isInteger(result.status) ? result.status : 403;
23
+ return {
24
+ ...result,
25
+ status,
26
+ message: clientFacingRouteMessage(status, result.message)
27
+ };
28
+ }
29
+ export function logServerException(scope, error) {
30
+ const details = error instanceof Error
31
+ ? (typeof error.stack === 'string' && error.stack.length > 0 ? error.stack : error.message)
32
+ : String(error);
33
+ console.error(`[Zenith:Server] ${scope}\n${details}`);
34
+ }
@@ -17,8 +17,10 @@ export function writeServerOutput({ coreOutputDir, staticDir, projectRoot, confi
17
17
  server_script_path: any;
18
18
  guard_module_ref: any;
19
19
  load_module_ref: any;
20
+ action_module_ref: any;
20
21
  has_guard: boolean;
21
22
  has_load: boolean;
23
+ has_action: boolean;
22
24
  params: string[];
23
25
  image_manifest_file: string | null;
24
26
  image_config: any;
@@ -29,6 +29,14 @@ const SERVER_RUNTIME_FILES = [
29
29
  {
30
30
  from: new URL('./images/shared.js', import.meta.url),
31
31
  to: 'images/shared.js'
32
+ },
33
+ {
34
+ from: new URL('./images/runtime.js', import.meta.url),
35
+ to: 'images/runtime.js'
36
+ },
37
+ {
38
+ from: new URL('./server-error.js', import.meta.url),
39
+ to: 'server-error.js'
32
40
  }
33
41
  ];
34
42
  function normalizeRouteName(routePath) {
@@ -270,12 +278,17 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
270
278
  server_script_path: route.server_script_path || null,
271
279
  guard_module_ref: route.guard_module_ref || null,
272
280
  load_module_ref: route.load_module_ref || null,
281
+ action_module_ref: route.action_module_ref || null,
273
282
  has_guard: route.has_guard === true,
274
283
  has_load: route.has_load === true,
284
+ has_action: route.has_action === true,
275
285
  params: extractRouteParams(route.path),
276
286
  image_manifest_file: imageManifestFile,
277
287
  image_config: config?.images || {}
278
288
  };
289
+ if (Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
290
+ meta.image_materialization = route.image_materialization;
291
+ }
279
292
  await writeFile(join(routeDir, 'route.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
280
293
  emittedRoutes.push(meta);
281
294
  }
@@ -5,6 +5,8 @@ import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from '../base-path.js';
7
7
  import { handleImageRequest } from '../images/service.js';
8
+ import { createTrustedOriginResolver } from '../request-origin.js';
9
+ import { defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from '../server-error.js';
8
10
  import { executeRouteRequest, renderRouteRequest } from './route-render.js';
9
11
  import { resolveRequestRoute } from './resolve-request-route.js';
10
12
  const __filename = fileURLToPath(import.meta.url);
@@ -68,18 +70,6 @@ function toStaticFilePath(staticDir, pathname) {
68
70
  }
69
71
  return resolveWithinRoot(staticDir, resolvedPath);
70
72
  }
71
- function publicHost(host) {
72
- if (host === '0.0.0.0' || host === '::') {
73
- return '127.0.0.1';
74
- }
75
- return host;
76
- }
77
- function createRequestBase(req, fallbackOrigin) {
78
- if (typeof req.headers.host === 'string' && req.headers.host.length > 0) {
79
- return `http://${req.headers.host}`;
80
- }
81
- return fallbackOrigin;
82
- }
83
73
  async function createWebRequest(req, url) {
84
74
  const init = {
85
75
  method: req.method || 'GET',
@@ -232,10 +222,11 @@ async function handleRouteCheck(req, res, url, context) {
232
222
  result = normalizeRouteCheckResult(execution.result, targetUrl, context.basePath);
233
223
  }
234
224
  catch (error) {
225
+ logServerException('node route-check failed', error);
235
226
  result = {
236
227
  kind: 'deny',
237
228
  status: 500,
238
- message: String(error)
229
+ message: defaultRouteDenyMessage(500)
239
230
  };
240
231
  }
241
232
  }
@@ -247,13 +238,13 @@ async function handleRouteCheck(req, res, url, context) {
247
238
  Vary: 'Cookie'
248
239
  });
249
240
  res.end(JSON.stringify({
250
- result,
241
+ result: sanitizeRouteResult(result),
251
242
  routeId,
252
243
  to: targetUrl.toString()
253
244
  }));
254
245
  }
255
246
  async function handleNodeRequest(req, res, context, serverOrigin) {
256
- const url = new URL(req.url || '/', createRequestBase(req, serverOrigin));
247
+ const url = new URL(req.url || '/', serverOrigin);
257
248
  const canonicalPath = stripBasePath(url.pathname, context.basePath);
258
249
  if (url.pathname === routeCheckPath(context.basePath)) {
259
250
  await handleRouteCheck(req, res, url, context);
@@ -294,9 +285,6 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
294
285
  params: serverResolved.params,
295
286
  routeModulePath: join(routeDir, 'route', 'entry.js'),
296
287
  shellHtmlPath: join(routeDir, 'route', 'page.html'),
297
- pageAssetPath: serverResolved.route.page_asset_file
298
- ? join(routeDir, 'route', serverResolved.route.page_asset_file)
299
- : null,
300
288
  imageManifestPath: serverResolved.route.image_manifest_file
301
289
  ? join(routeDir, 'route', serverResolved.route.image_manifest_file)
302
290
  : null,
@@ -321,32 +309,50 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
321
309
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
322
310
  res.end('404 Not Found');
323
311
  }
324
- export async function createRequestHandler(options = {}) {
325
- const context = await loadRuntimeContext(options);
326
- const host = publicHost(options.host || '127.0.0.1');
327
- const port = Number.isInteger(options.port) ? options.port : 3000;
328
- const serverOrigin = `http://${host}:${port}`;
312
+ function createNodeRequestHandler(context, resolveServerOrigin) {
329
313
  return async (req, res) => {
330
314
  try {
331
- await handleNodeRequest(req, res, context, serverOrigin);
315
+ await handleNodeRequest(req, res, context, resolveServerOrigin());
332
316
  }
333
317
  catch (error) {
318
+ logServerException('node request handler failed', error);
334
319
  res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
335
- res.end(String(error));
320
+ res.end(defaultRouteDenyMessage(500));
336
321
  }
337
322
  };
338
323
  }
324
+ export async function createRequestHandler(options = {}) {
325
+ const context = await loadRuntimeContext(options);
326
+ const resolveServerOrigin = createTrustedOriginResolver({
327
+ publicOrigin: options.publicOrigin,
328
+ host: options.host || '127.0.0.1',
329
+ port: Number.isInteger(options.port) ? options.port : undefined,
330
+ label: 'createRequestHandler()'
331
+ });
332
+ resolveServerOrigin();
333
+ return createNodeRequestHandler(context, resolveServerOrigin);
334
+ }
339
335
  export async function createNodeServer(options = {}) {
340
336
  const { port = 3000, host = '127.0.0.1' } = options;
341
- const handler = await createRequestHandler({ ...options, port, host });
337
+ const context = await loadRuntimeContext(options);
338
+ let actualPort = Number.isInteger(port) && port > 0 ? port : 0;
339
+ const resolveServerOrigin = createTrustedOriginResolver({
340
+ publicOrigin: options.publicOrigin,
341
+ host,
342
+ getPort: () => actualPort,
343
+ label: 'createNodeServer()'
344
+ });
345
+ const handler = createNodeRequestHandler(context, resolveServerOrigin);
342
346
  const server = createServer((req, res) => {
343
347
  void handler(req, res);
344
348
  });
345
349
  return new Promise((resolveServer) => {
346
350
  server.listen(port, host, () => {
351
+ const address = server.address();
352
+ actualPort = address && typeof address === 'object' ? address.port : port;
347
353
  resolveServer({
348
354
  server,
349
- port: server.address().port,
355
+ port: actualPort,
350
356
  close: () => server.close()
351
357
  });
352
358
  });
@@ -8,7 +8,7 @@ export function extractInternalParams(requestUrl: any, route: any): {};
8
8
  * routeModulePath: string,
9
9
  * guardOnly?: boolean
10
10
  * }} options
11
- * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
11
+ * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
12
12
  */
13
13
  export function executeRouteRequest(options: {
14
14
  request: Request;
@@ -30,8 +30,10 @@ export function executeRouteRequest(options: {
30
30
  };
31
31
  trace: {
32
32
  guard: string;
33
+ action: string;
33
34
  load: string;
34
35
  };
36
+ status?: number;
35
37
  }>;
36
38
  /**
37
39
  * @param {{
@@ -40,7 +42,6 @@ export function executeRouteRequest(options: {
40
42
  * params: Record<string, string>,
41
43
  * routeModulePath: string,
42
44
  * shellHtmlPath: string,
43
- * pageAssetPath?: string | null,
44
45
  * imageManifestPath?: string | null,
45
46
  * imageConfig?: Record<string, unknown>
46
47
  * }} options
@@ -58,7 +59,6 @@ export function renderRouteRequest(options: {
58
59
  params: Record<string, string>;
59
60
  routeModulePath: string;
60
61
  shellHtmlPath: string;
61
- pageAssetPath?: string | null;
62
62
  imageManifestPath?: string | null;
63
63
  imageConfig?: Record<string, unknown>;
64
64
  }): Promise<Response>;
@@ -3,7 +3,8 @@ import { pathToFileURL } from 'node:url';
3
3
  import { appLocalRedirectLocation, normalizeBasePath, prependBasePath } from '../base-path.js';
4
4
  import { createImageRuntimePayload, injectImageRuntimePayload } from '../images/payload.js';
5
5
  import { materializeImageMarkup } from '../images/materialize.js';
6
- import { allow, data, deny, redirect, resolveRouteResult } from '../server-contract.js';
6
+ import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException } from '../server-error.js';
7
+ import { allow, data, deny, invalid, redirect, resolveRouteResult } from '../server-contract.js';
7
8
  const MODULE_CACHE = new Map();
8
9
  const INTERNAL_QUERY_PREFIX = '__zenith_param_';
9
10
  function parseCookies(rawCookieHeader) {
@@ -40,18 +41,6 @@ function escapeInlineJson(payload) {
40
41
  .replace(/\u2028/g, '\\u2028')
41
42
  .replace(/\u2029/g, '\\u2029');
42
43
  }
43
- function defaultRouteDenyMessage(status) {
44
- if (status === 401) {
45
- return 'Unauthorized';
46
- }
47
- if (status === 403) {
48
- return 'Forbidden';
49
- }
50
- if (status === 404) {
51
- return 'Not Found';
52
- }
53
- return 'Internal Server Error';
54
- }
55
44
  function createTextResponse(status, message) {
56
45
  return new Response(message || defaultRouteDenyMessage(status), {
57
46
  status,
@@ -166,6 +155,7 @@ function createRouteContext({ request, route, params, publicUrl }) {
166
155
  file: route.file || route.server_script_path || route.route_id || route.path
167
156
  },
168
157
  env: {},
158
+ action: null,
169
159
  auth: {
170
160
  async getSession() {
171
161
  return null;
@@ -177,6 +167,7 @@ function createRouteContext({ request, route, params, publicUrl }) {
177
167
  allow,
178
168
  redirect,
179
169
  deny,
170
+ invalid,
180
171
  data
181
172
  };
182
173
  }
@@ -188,7 +179,7 @@ function createRouteContext({ request, route, params, publicUrl }) {
188
179
  * routeModulePath: string,
189
180
  * guardOnly?: boolean
190
181
  * }} options
191
- * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
182
+ * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
192
183
  */
193
184
  export async function executeRouteRequest(options) {
194
185
  const { request, route, params, routeModulePath, guardOnly = false } = options;
@@ -204,7 +195,8 @@ export async function executeRouteRequest(options) {
204
195
  return {
205
196
  publicUrl,
206
197
  result: resolved.result,
207
- trace: resolved.trace
198
+ trace: resolved.trace,
199
+ status: resolved.status
208
200
  };
209
201
  }
210
202
  /**
@@ -214,16 +206,15 @@ export async function executeRouteRequest(options) {
214
206
  * params: Record<string, string>,
215
207
  * routeModulePath: string,
216
208
  * shellHtmlPath: string,
217
- * pageAssetPath?: string | null,
218
209
  * imageManifestPath?: string | null,
219
210
  * imageConfig?: Record<string, unknown>
220
211
  * }} options
221
212
  * @returns {Promise<Response>}
222
213
  */
223
214
  export async function renderRouteRequest(options) {
224
- const { request, route, params, routeModulePath, shellHtmlPath, pageAssetPath = null, imageManifestPath = null, imageConfig = {} } = options;
215
+ const { request, route, params, routeModulePath, shellHtmlPath, imageManifestPath = null, imageConfig = {} } = options;
225
216
  try {
226
- const { publicUrl, result } = await executeRouteRequest({
217
+ const { publicUrl, result, status } = await executeRouteRequest({
227
218
  request,
228
219
  route,
229
220
  params,
@@ -240,7 +231,7 @@ export async function renderRouteRequest(options) {
240
231
  }
241
232
  if (result.kind === 'deny') {
242
233
  const status = Number.isInteger(result.status) ? result.status : 403;
243
- return createTextResponse(status, result.message || defaultRouteDenyMessage(status));
234
+ return createTextResponse(status, clientFacingRouteMessage(status, result.message));
244
235
  }
245
236
  const ssrPayload = result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)
246
237
  ? result.data
@@ -248,26 +239,24 @@ export async function renderRouteRequest(options) {
248
239
  const localImages = await loadImageManifest(imageManifestPath);
249
240
  const imagePayload = createImageRuntimePayload(imageConfig, localImages, 'passthrough', route.base_path || '/');
250
241
  let html = await readFile(shellHtmlPath, 'utf8');
251
- if (pageAssetPath) {
252
- html = await materializeImageMarkup({
253
- html,
254
- pageAssetPath,
255
- payload: imagePayload,
256
- ssrData: ssrPayload,
257
- routePathname: publicUrl.pathname
258
- });
259
- }
242
+ html = await materializeImageMarkup({
243
+ html,
244
+ payload: imagePayload,
245
+ imageMaterialization: Array.isArray(route.image_materialization)
246
+ ? route.image_materialization
247
+ : []
248
+ });
260
249
  html = injectSsrPayload(html, ssrPayload);
261
250
  html = injectImageRuntimePayload(html, imagePayload);
262
251
  return new Response(html, {
263
- status: 200,
252
+ status: Number.isInteger(status) ? status : 200,
264
253
  headers: {
265
254
  'Content-Type': 'text/html; charset=utf-8'
266
255
  }
267
256
  });
268
257
  }
269
258
  catch (error) {
270
- const message = String(error);
271
- return createTextResponse(500, message || defaultRouteDenyMessage(500));
259
+ logServerException('node route render failed', error);
260
+ return createTextResponse(500, defaultRouteDenyMessage(500));
272
261
  }
273
262
  }
@@ -1,39 +1,45 @@
1
1
  /**
2
2
  * @param {{
3
3
  * sourceFile: string,
4
- * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null,
4
+ * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null,
5
5
  * adjacentGuardPath?: string | null,
6
- * adjacentLoadPath?: string | null
6
+ * adjacentLoadPath?: string | null,
7
+ * adjacentActionPath?: string | null
7
8
  * }} input
8
- * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null }}
9
+ * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
9
10
  */
10
- export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath }: {
11
+ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath, adjacentActionPath }: {
11
12
  sourceFile: string;
12
13
  inlineServerScript?: {
13
14
  source: string;
14
15
  prerender: boolean;
15
16
  has_guard: boolean;
16
17
  has_load: boolean;
18
+ has_action: boolean;
17
19
  source_path: string;
18
20
  } | null;
19
21
  adjacentGuardPath?: string | null;
20
22
  adjacentLoadPath?: string | null;
23
+ adjacentActionPath?: string | null;
21
24
  }): {
22
25
  serverScript: {
23
26
  source: string;
24
27
  prerender: boolean;
25
28
  has_guard: boolean;
26
29
  has_load: boolean;
30
+ has_action: boolean;
27
31
  source_path: string;
28
32
  } | null;
29
33
  guardPath: string | null;
30
34
  loadPath: string | null;
35
+ actionPath: string | null;
31
36
  };
32
37
  /**
33
38
  * @param {string} sourceFile
34
- * @returns {{ guardPath: string | null, loadPath: string | null }}
39
+ * @returns {{ guardPath: string | null, loadPath: string | null, actionPath: string | null }}
35
40
  */
36
41
  export function resolveAdjacentServerModules(sourceFile: string): {
37
42
  guardPath: string | null;
38
43
  loadPath: string | null;
44
+ actionPath: string | null;
39
45
  };