@zenithbuild/cli 0.7.3 → 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.
- package/README.md +18 -13
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +56 -13
- package/dist/adapters/adapter-node.js +8 -0
- package/dist/adapters/adapter-static-export.d.ts +5 -0
- package/dist/adapters/adapter-static-export.js +115 -0
- package/dist/adapters/adapter-types.d.ts +3 -1
- package/dist/adapters/adapter-types.js +5 -2
- package/dist/adapters/adapter-vercel.d.ts +1 -1
- package/dist/adapters/adapter-vercel.js +70 -13
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +49 -0
- package/dist/adapters/resolve-adapter.js +4 -0
- package/dist/adapters/route-rules.d.ts +5 -0
- package/dist/adapters/route-rules.js +9 -0
- package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
- package/dist/adapters/validate-hosted-resource-routes.js +13 -0
- package/dist/auth/route-auth.d.ts +6 -0
- package/dist/auth/route-auth.js +236 -0
- package/dist/build/compiler-runtime.d.ts +10 -9
- package/dist/build/compiler-runtime.js +58 -2
- package/dist/build/compiler-signal-expression.d.ts +1 -0
- package/dist/build/compiler-signal-expression.js +155 -0
- package/dist/build/expression-rewrites.d.ts +1 -6
- package/dist/build/expression-rewrites.js +61 -65
- package/dist/build/page-component-loop.d.ts +3 -13
- package/dist/build/page-component-loop.js +21 -46
- package/dist/build/page-ir-normalization.d.ts +0 -8
- package/dist/build/page-ir-normalization.js +13 -234
- package/dist/build/page-loop-state.d.ts +6 -9
- package/dist/build/page-loop-state.js +9 -8
- package/dist/build/page-loop.js +27 -22
- package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
- package/dist/build/scoped-identifier-rewrite.js +28 -128
- package/dist/build/server-script.d.ts +3 -1
- package/dist/build/server-script.js +35 -5
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +32 -18
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +152 -55
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/framework-components/Image.zen +1 -1
- package/dist/images/materialization-plan.d.ts +1 -0
- package/dist/images/materialization-plan.js +6 -0
- package/dist/images/materialize.d.ts +5 -3
- package/dist/images/materialize.js +24 -109
- package/dist/images/router-manifest.d.ts +1 -0
- package/dist/images/router-manifest.js +49 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/index.js +8 -2
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +27 -7
- package/dist/preview.d.ts +13 -4
- package/dist/preview.js +261 -101
- package/dist/request-body.d.ts +1 -0
- package/dist/request-body.js +7 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +34 -0
- package/dist/resource-response.js +71 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +29 -6
- package/dist/server-contract.js +304 -42
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +36 -0
- package/dist/server-output.d.ts +4 -1
- package/dist/server-output.js +71 -10
- package/dist/server-runtime/node-server.js +67 -31
- package/dist/server-runtime/route-render.d.ts +27 -3
- package/dist/server-runtime/route-render.js +94 -53
- package/dist/server-script-composition.d.ts +13 -5
- package/dist/server-script-composition.js +29 -11
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +6 -3
package/dist/server-contract.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// server-contract.js — Zenith CLI V0
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
3
|
// Shared validation and payload resolution logic for <script server> blocks.
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const ROUTE_RESULT_KINDS = new Set(['allow', 'redirect', 'deny', 'data']);
|
|
4
|
+
import { assertValidDownloadResult, createDownloadResult } from './download-result.js';
|
|
5
|
+
const ALLOWED_KEYS = new Set(['data', 'load', 'guard', 'action', 'prerender', 'exportPaths', 'ssr_data', 'props', 'ssr']);
|
|
6
|
+
const RESOURCE_ALLOWED_KEYS = new Set(['load', 'guard', 'action']);
|
|
7
|
+
const ROUTE_RESULT_KINDS = new Set(['allow', 'redirect', 'deny', 'data', 'invalid', 'json', 'text', 'download']);
|
|
8
|
+
const AUTH_CONTROL_FLOW_FLAG = '__zenith_auth_control_flow';
|
|
9
|
+
const STAGED_SET_COOKIES_KEY = '__zenith_staged_set_cookies';
|
|
8
10
|
export function allow() {
|
|
9
11
|
return { kind: 'allow' };
|
|
10
12
|
}
|
|
@@ -25,6 +27,30 @@ export function deny(status = 403, message = undefined) {
|
|
|
25
27
|
export function data(payload) {
|
|
26
28
|
return { kind: 'data', data: payload };
|
|
27
29
|
}
|
|
30
|
+
export function invalid(payload, status = 400) {
|
|
31
|
+
return {
|
|
32
|
+
kind: 'invalid',
|
|
33
|
+
data: payload,
|
|
34
|
+
status: Number.isInteger(status) ? status : 400
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function json(payload, status = 200) {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'json',
|
|
40
|
+
data: payload,
|
|
41
|
+
status: Number.isInteger(status) ? status : 200
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function text(body, status = 200) {
|
|
45
|
+
return {
|
|
46
|
+
kind: 'text',
|
|
47
|
+
body: typeof body === 'string' ? body : String(body ?? ''),
|
|
48
|
+
status: Number.isInteger(status) ? status : 200
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function download(body, options = {}) {
|
|
52
|
+
return createDownloadResult(body, options);
|
|
53
|
+
}
|
|
28
54
|
function isRouteResultLike(value) {
|
|
29
55
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
30
56
|
return false;
|
|
@@ -57,52 +83,132 @@ function assertValidRouteResultShape(value, where, allowedKinds) {
|
|
|
57
83
|
throw new Error(`[Zenith] ${where}: deny message must be a string when provided.`);
|
|
58
84
|
}
|
|
59
85
|
}
|
|
86
|
+
if (kind === 'invalid') {
|
|
87
|
+
if (!Number.isInteger(value.status) || (value.status !== 400 && value.status !== 422)) {
|
|
88
|
+
throw new Error(`[Zenith] ${where}: invalid status must be 400 or 422.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (kind === 'json' || kind === 'text') {
|
|
92
|
+
if (!Number.isInteger(value.status) || value.status < 200 || value.status > 599 || (value.status >= 300 && value.status <= 399)) {
|
|
93
|
+
throw new Error(`[Zenith] ${where}: ${kind} status must be an integer between 200-599 and may not be 3xx.`);
|
|
94
|
+
}
|
|
95
|
+
if (kind === 'text' && typeof value.body !== 'string') {
|
|
96
|
+
throw new Error(`[Zenith] ${where}: text body must be a string.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (kind === 'download') {
|
|
100
|
+
assertValidDownloadResult(value, where);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function assertOneArgRouteFunction({ filePath, exportName, value }) {
|
|
104
|
+
if (typeof value !== 'function') {
|
|
105
|
+
throw new Error(`[Zenith] ${filePath}: "${exportName}" must be a function.`);
|
|
106
|
+
}
|
|
107
|
+
if (value.length !== 1) {
|
|
108
|
+
throw new Error(`[Zenith] ${filePath}: "${exportName}(ctx)" must take exactly 1 argument.`);
|
|
109
|
+
}
|
|
110
|
+
const fnStr = value.toString();
|
|
111
|
+
const paramsMatch = fnStr.match(/^[^{=]+\(([^)]*)\)/);
|
|
112
|
+
if (paramsMatch && paramsMatch[1].includes('...')) {
|
|
113
|
+
throw new Error(`[Zenith] ${filePath}: "${exportName}(ctx)" must not contain rest parameters.`);
|
|
114
|
+
}
|
|
60
115
|
}
|
|
61
|
-
|
|
116
|
+
function buildActionState(result) {
|
|
117
|
+
if (!result || typeof result !== 'object') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (result.kind === 'data') {
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
status: 200,
|
|
124
|
+
data: result.data
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (result.kind === 'invalid') {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
status: Number.isInteger(result.status) ? result.status : 400,
|
|
131
|
+
data: result.data
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function unwrapAuthControlFlow(error, where, allowedKinds) {
|
|
137
|
+
if (!error || typeof error !== 'object' || error[AUTH_CONTROL_FLOW_FLAG] !== true) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const result = error.result;
|
|
141
|
+
assertValidRouteResultShape(result, where, allowedKinds);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
async function invokeRouteStage({ fn, ctx, where, allowedKinds }) {
|
|
145
|
+
try {
|
|
146
|
+
return await fn(ctx);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const authResult = unwrapAuthControlFlow(error, where, allowedKinds);
|
|
150
|
+
if (authResult) {
|
|
151
|
+
return authResult;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function buildResolvedEnvelope({ result, trace, status, ctx }) {
|
|
157
|
+
const envelope = { result, trace };
|
|
158
|
+
if (status !== undefined) {
|
|
159
|
+
envelope.status = status;
|
|
160
|
+
}
|
|
161
|
+
const setCookies = Array.isArray(ctx?.[STAGED_SET_COOKIES_KEY])
|
|
162
|
+
? ctx[STAGED_SET_COOKIES_KEY].slice()
|
|
163
|
+
: [];
|
|
164
|
+
if (setCookies.length > 0) {
|
|
165
|
+
envelope.setCookies = setCookies;
|
|
166
|
+
}
|
|
167
|
+
return envelope;
|
|
168
|
+
}
|
|
169
|
+
export function validateServerExports({ exports, filePath, routeKind = 'page' }) {
|
|
62
170
|
const exportKeys = Object.keys(exports);
|
|
63
|
-
const
|
|
171
|
+
const allowedKeys = routeKind === 'resource' ? RESOURCE_ALLOWED_KEYS : ALLOWED_KEYS;
|
|
172
|
+
const illegalKeys = exportKeys.filter(k => !allowedKeys.has(k));
|
|
64
173
|
if (illegalKeys.length > 0) {
|
|
65
174
|
throw new Error(`[Zenith] ${filePath}: illegal export(s): ${illegalKeys.join(', ')}`);
|
|
66
175
|
}
|
|
67
176
|
const hasData = 'data' in exports;
|
|
68
177
|
const hasLoad = 'load' in exports;
|
|
69
178
|
const hasGuard = 'guard' in exports;
|
|
70
|
-
const
|
|
179
|
+
const hasAction = 'action' in exports;
|
|
180
|
+
const hasNew = hasData || hasLoad || hasAction;
|
|
71
181
|
const hasLegacy = ('ssr_data' in exports) || ('props' in exports) || ('ssr' in exports);
|
|
182
|
+
if (routeKind === 'resource') {
|
|
183
|
+
if (hasData) {
|
|
184
|
+
throw new Error(`[Zenith] ${filePath}: resource routes may not export "data". Use load(ctx) or action(ctx) with ctx.json()/ctx.text().`);
|
|
185
|
+
}
|
|
186
|
+
if (!hasLoad && !hasAction) {
|
|
187
|
+
throw new Error(`[Zenith] ${filePath}: resource routes must export load(ctx), action(ctx), or both.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
72
190
|
if (hasData && hasLoad) {
|
|
73
191
|
throw new Error(`[Zenith] ${filePath}: cannot export both "data" and "load". Choose one.`);
|
|
74
192
|
}
|
|
75
|
-
if (hasNew && hasLegacy) {
|
|
193
|
+
if (routeKind === 'page' && hasNew && hasLegacy) {
|
|
76
194
|
throw new Error(`[Zenith] ${filePath}: cannot mix new ("data"/"load") with legacy ("ssr_data"/"props"/"ssr") exports.`);
|
|
77
195
|
}
|
|
78
|
-
if ('prerender' in exports && typeof exports.prerender !== 'boolean') {
|
|
196
|
+
if (routeKind === 'page' && 'prerender' in exports && typeof exports.prerender !== 'boolean') {
|
|
79
197
|
throw new Error(`[Zenith] ${filePath}: "prerender" must be a boolean.`);
|
|
80
198
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
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.`);
|
|
199
|
+
if (routeKind === 'page' && 'exportPaths' in exports) {
|
|
200
|
+
if (!Array.isArray(exports.exportPaths) || exports.exportPaths.some((value) => typeof value !== 'string')) {
|
|
201
|
+
throw new Error(`[Zenith] ${filePath}: "exportPaths" must be an array of string pathnames.`);
|
|
92
202
|
}
|
|
93
203
|
}
|
|
94
|
-
if (
|
|
95
|
-
|
|
204
|
+
if (hasLoad) {
|
|
205
|
+
assertOneArgRouteFunction({ filePath, exportName: 'load', value: exports.load });
|
|
96
206
|
}
|
|
97
207
|
if (hasGuard) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
}
|
|
208
|
+
assertOneArgRouteFunction({ filePath, exportName: 'guard', value: exports.guard });
|
|
209
|
+
}
|
|
210
|
+
if (hasAction) {
|
|
211
|
+
assertOneArgRouteFunction({ filePath, exportName: 'action', value: exports.action });
|
|
106
212
|
}
|
|
107
213
|
}
|
|
108
214
|
export function assertJsonSerializable(value, where = 'payload') {
|
|
@@ -156,14 +262,29 @@ export function assertJsonSerializable(value, where = 'payload') {
|
|
|
156
262
|
}
|
|
157
263
|
walk(value, '$');
|
|
158
264
|
}
|
|
159
|
-
export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = false }) {
|
|
160
|
-
validateServerExports({ exports, filePath });
|
|
265
|
+
export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = false, routeKind = 'page' }) {
|
|
266
|
+
validateServerExports({ exports, filePath, routeKind });
|
|
267
|
+
if (routeKind === 'resource') {
|
|
268
|
+
return resolveResourceRouteResult({ exports, ctx, filePath, guardOnly });
|
|
269
|
+
}
|
|
161
270
|
const trace = {
|
|
162
271
|
guard: 'none',
|
|
272
|
+
action: 'none',
|
|
163
273
|
load: 'none'
|
|
164
274
|
};
|
|
275
|
+
let responseStatus = 200;
|
|
276
|
+
const requestMethod = String(ctx?.method || ctx?.request?.method || 'GET').toUpperCase();
|
|
277
|
+
const isActionRequest = !guardOnly && requestMethod === 'POST';
|
|
278
|
+
if (ctx && typeof ctx === 'object') {
|
|
279
|
+
ctx.action = null;
|
|
280
|
+
}
|
|
165
281
|
if ('guard' in exports) {
|
|
166
|
-
const guardRaw = await
|
|
282
|
+
const guardRaw = await invokeRouteStage({
|
|
283
|
+
fn: exports.guard,
|
|
284
|
+
ctx,
|
|
285
|
+
where: `${filePath}: guard(ctx)`,
|
|
286
|
+
allowedKinds: new Set(['allow', 'redirect', 'deny'])
|
|
287
|
+
});
|
|
167
288
|
const guardResult = guardRaw == null ? allow() : guardRaw;
|
|
168
289
|
if (guardResult.kind === 'data') {
|
|
169
290
|
throw new Error(`[Zenith] ${filePath}: guard(ctx) returned data(payload) which is a critical invariant violation. guard() can only return allow(), redirect(), or deny(). Use load(ctx) for data injection.`);
|
|
@@ -171,15 +292,51 @@ export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = f
|
|
|
171
292
|
assertValidRouteResultShape(guardResult, `${filePath}: guard(ctx) return`, new Set(['allow', 'redirect', 'deny']));
|
|
172
293
|
trace.guard = guardResult.kind;
|
|
173
294
|
if (guardResult.kind === 'redirect' || guardResult.kind === 'deny') {
|
|
174
|
-
return { result: guardResult, trace };
|
|
295
|
+
return buildResolvedEnvelope({ result: guardResult, trace, ctx });
|
|
175
296
|
}
|
|
176
297
|
}
|
|
177
298
|
if (guardOnly) {
|
|
178
|
-
return { result: allow(), trace };
|
|
299
|
+
return buildResolvedEnvelope({ result: allow(), trace, ctx });
|
|
300
|
+
}
|
|
301
|
+
if (isActionRequest && 'action' in exports) {
|
|
302
|
+
const actionRaw = await invokeRouteStage({
|
|
303
|
+
fn: exports.action,
|
|
304
|
+
ctx,
|
|
305
|
+
where: `${filePath}: action(ctx)`,
|
|
306
|
+
allowedKinds: new Set(['data', 'invalid', 'redirect', 'deny'])
|
|
307
|
+
});
|
|
308
|
+
let actionResult = null;
|
|
309
|
+
if (isRouteResultLike(actionRaw)) {
|
|
310
|
+
actionResult = actionRaw;
|
|
311
|
+
assertValidRouteResultShape(actionResult, `${filePath}: action(ctx) return`, new Set(['data', 'invalid', 'redirect', 'deny']));
|
|
312
|
+
if (actionResult.kind === 'data' || actionResult.kind === 'invalid') {
|
|
313
|
+
assertJsonSerializable(actionResult.data, `${filePath}: action(ctx) return`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
assertJsonSerializable(actionRaw, `${filePath}: action(ctx) return`);
|
|
318
|
+
actionResult = data(actionRaw);
|
|
319
|
+
}
|
|
320
|
+
trace.action = actionResult.kind;
|
|
321
|
+
if (actionResult.kind === 'redirect' || actionResult.kind === 'deny') {
|
|
322
|
+
return buildResolvedEnvelope({ result: actionResult, trace, ctx });
|
|
323
|
+
}
|
|
324
|
+
const actionState = buildActionState(actionResult);
|
|
325
|
+
if (ctx && typeof ctx === 'object') {
|
|
326
|
+
ctx.action = actionState;
|
|
327
|
+
}
|
|
328
|
+
if (actionState && actionState.ok === false) {
|
|
329
|
+
responseStatus = actionState.status;
|
|
330
|
+
}
|
|
179
331
|
}
|
|
180
332
|
let payload;
|
|
181
333
|
if ('load' in exports) {
|
|
182
|
-
const loadRaw = await
|
|
334
|
+
const loadRaw = await invokeRouteStage({
|
|
335
|
+
fn: exports.load,
|
|
336
|
+
ctx,
|
|
337
|
+
where: `${filePath}: load(ctx)`,
|
|
338
|
+
allowedKinds: new Set(['data', 'redirect', 'deny'])
|
|
339
|
+
});
|
|
183
340
|
let loadResult = null;
|
|
184
341
|
if (isRouteResultLike(loadRaw)) {
|
|
185
342
|
loadResult = loadRaw;
|
|
@@ -190,34 +347,139 @@ export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = f
|
|
|
190
347
|
loadResult = data(loadRaw);
|
|
191
348
|
}
|
|
192
349
|
trace.load = loadResult.kind;
|
|
193
|
-
return {
|
|
350
|
+
return buildResolvedEnvelope({
|
|
351
|
+
result: loadResult,
|
|
352
|
+
trace,
|
|
353
|
+
status: loadResult.kind === 'data' ? responseStatus : undefined,
|
|
354
|
+
ctx
|
|
355
|
+
});
|
|
194
356
|
}
|
|
195
357
|
if ('data' in exports) {
|
|
196
358
|
payload = exports.data;
|
|
197
359
|
assertJsonSerializable(payload, `${filePath}: data export`);
|
|
198
360
|
trace.load = 'data';
|
|
199
|
-
return { result: data(payload), trace };
|
|
361
|
+
return buildResolvedEnvelope({ result: data(payload), trace, status: responseStatus, ctx });
|
|
200
362
|
}
|
|
201
363
|
// legacy fallback
|
|
202
364
|
if ('ssr_data' in exports) {
|
|
203
365
|
payload = exports.ssr_data;
|
|
204
366
|
assertJsonSerializable(payload, `${filePath}: ssr_data export`);
|
|
205
367
|
trace.load = 'data';
|
|
206
|
-
return { result: data(payload), trace };
|
|
368
|
+
return buildResolvedEnvelope({ result: data(payload), trace, status: responseStatus, ctx });
|
|
207
369
|
}
|
|
208
370
|
if ('props' in exports) {
|
|
209
371
|
payload = exports.props;
|
|
210
372
|
assertJsonSerializable(payload, `${filePath}: props export`);
|
|
211
373
|
trace.load = 'data';
|
|
212
|
-
return { result: data(payload), trace };
|
|
374
|
+
return buildResolvedEnvelope({ result: data(payload), trace, status: responseStatus, ctx });
|
|
213
375
|
}
|
|
214
376
|
if ('ssr' in exports) {
|
|
215
377
|
payload = exports.ssr;
|
|
216
378
|
assertJsonSerializable(payload, `${filePath}: ssr export`);
|
|
217
379
|
trace.load = 'data';
|
|
218
|
-
return { result: data(payload), trace };
|
|
380
|
+
return buildResolvedEnvelope({ result: data(payload), trace, status: responseStatus, ctx });
|
|
381
|
+
}
|
|
382
|
+
if (isActionRequest && ctx?.action) {
|
|
383
|
+
trace.load = 'data';
|
|
384
|
+
return buildResolvedEnvelope({
|
|
385
|
+
result: data({ action: ctx.action }),
|
|
386
|
+
trace,
|
|
387
|
+
status: responseStatus,
|
|
388
|
+
ctx
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return buildResolvedEnvelope({ result: data({}), trace, status: responseStatus, ctx });
|
|
392
|
+
}
|
|
393
|
+
async function resolveResourceRouteResult({ exports, ctx, filePath, guardOnly = false }) {
|
|
394
|
+
const trace = {
|
|
395
|
+
guard: 'none',
|
|
396
|
+
action: 'none',
|
|
397
|
+
load: 'none'
|
|
398
|
+
};
|
|
399
|
+
const requestMethod = String(ctx?.method || ctx?.request?.method || 'GET').toUpperCase();
|
|
400
|
+
if (ctx && typeof ctx === 'object') {
|
|
401
|
+
ctx.action = null;
|
|
402
|
+
}
|
|
403
|
+
if ('guard' in exports) {
|
|
404
|
+
const guardRaw = await invokeRouteStage({
|
|
405
|
+
fn: exports.guard,
|
|
406
|
+
ctx,
|
|
407
|
+
where: `${filePath}: guard(ctx)`,
|
|
408
|
+
allowedKinds: new Set(['allow', 'redirect', 'deny'])
|
|
409
|
+
});
|
|
410
|
+
const guardResult = guardRaw == null ? allow() : guardRaw;
|
|
411
|
+
assertValidRouteResultShape(guardResult, `${filePath}: guard(ctx) return`, new Set(['allow', 'redirect', 'deny']));
|
|
412
|
+
trace.guard = guardResult.kind;
|
|
413
|
+
if (guardResult.kind === 'redirect' || guardResult.kind === 'deny') {
|
|
414
|
+
return buildResolvedEnvelope({ result: guardResult, trace, ctx });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (guardOnly) {
|
|
418
|
+
return buildResolvedEnvelope({ result: allow(), trace, ctx });
|
|
419
|
+
}
|
|
420
|
+
if (requestMethod === 'GET' || requestMethod === 'HEAD') {
|
|
421
|
+
if (!('load' in exports)) {
|
|
422
|
+
trace.load = 'text';
|
|
423
|
+
return buildResolvedEnvelope({ result: text('Method Not Allowed', 405), trace, status: 405, ctx });
|
|
424
|
+
}
|
|
425
|
+
const loadResult = await resolveResourceStage({
|
|
426
|
+
exports,
|
|
427
|
+
exportName: 'load',
|
|
428
|
+
ctx,
|
|
429
|
+
filePath,
|
|
430
|
+
trace,
|
|
431
|
+
traceKey: 'load'
|
|
432
|
+
});
|
|
433
|
+
return buildResolvedEnvelope({
|
|
434
|
+
result: loadResult,
|
|
435
|
+
trace,
|
|
436
|
+
status: loadResult.status,
|
|
437
|
+
ctx
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (requestMethod === 'POST') {
|
|
441
|
+
if (!('action' in exports)) {
|
|
442
|
+
trace.action = 'text';
|
|
443
|
+
return buildResolvedEnvelope({ result: text('Method Not Allowed', 405), trace, status: 405, ctx });
|
|
444
|
+
}
|
|
445
|
+
const actionResult = await resolveResourceStage({
|
|
446
|
+
exports,
|
|
447
|
+
exportName: 'action',
|
|
448
|
+
ctx,
|
|
449
|
+
filePath,
|
|
450
|
+
trace,
|
|
451
|
+
traceKey: 'action'
|
|
452
|
+
});
|
|
453
|
+
return buildResolvedEnvelope({
|
|
454
|
+
result: actionResult,
|
|
455
|
+
trace,
|
|
456
|
+
status: actionResult.status,
|
|
457
|
+
ctx
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return buildResolvedEnvelope({
|
|
461
|
+
result: text('Method Not Allowed', 405),
|
|
462
|
+
trace,
|
|
463
|
+
status: 405,
|
|
464
|
+
ctx
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async function resolveResourceStage({ exports, exportName, ctx, filePath, trace, traceKey }) {
|
|
468
|
+
const raw = await invokeRouteStage({
|
|
469
|
+
fn: exports[exportName],
|
|
470
|
+
ctx,
|
|
471
|
+
where: `${filePath}: ${exportName}(ctx)`,
|
|
472
|
+
allowedKinds: new Set(['json', 'text', 'download', 'redirect', 'deny'])
|
|
473
|
+
});
|
|
474
|
+
if (!isRouteResultLike(raw)) {
|
|
475
|
+
throw new Error(`[Zenith] ${filePath}: ${exportName}(ctx) on a resource route must return json(...), text(...), download(...), redirect(...), or deny(...).`);
|
|
476
|
+
}
|
|
477
|
+
assertValidRouteResultShape(raw, `${filePath}: ${exportName}(ctx) return`, new Set(['json', 'text', 'download', 'redirect', 'deny']));
|
|
478
|
+
if (raw.kind === 'json') {
|
|
479
|
+
assertJsonSerializable(raw.data, `${filePath}: ${exportName}(ctx) return`);
|
|
219
480
|
}
|
|
220
|
-
|
|
481
|
+
trace[traceKey] = raw.kind;
|
|
482
|
+
return raw;
|
|
221
483
|
}
|
|
222
484
|
export async function resolveServerPayload({ exports, ctx, filePath }) {
|
|
223
485
|
const resolved = await resolveRouteResult({ exports, ctx, filePath });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function defaultRouteDenyMessage(status: any): "Unauthorized" | "Forbidden" | "Not Found" | "Method Not Allowed" | "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,36 @@
|
|
|
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
|
+
if (status === 405)
|
|
9
|
+
return 'Method Not Allowed';
|
|
10
|
+
return 'Internal Server Error';
|
|
11
|
+
}
|
|
12
|
+
export function clientFacingRouteMessage(status, message) {
|
|
13
|
+
const resolvedStatus = Number.isInteger(status) ? status : 500;
|
|
14
|
+
if (resolvedStatus >= 500) {
|
|
15
|
+
return defaultRouteDenyMessage(resolvedStatus);
|
|
16
|
+
}
|
|
17
|
+
const resolvedMessage = typeof message === 'string' ? message : '';
|
|
18
|
+
return resolvedMessage || defaultRouteDenyMessage(resolvedStatus);
|
|
19
|
+
}
|
|
20
|
+
export function sanitizeRouteResult(result) {
|
|
21
|
+
if (!result || typeof result !== 'object' || Array.isArray(result) || result.kind !== 'deny') {
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
25
|
+
return {
|
|
26
|
+
...result,
|
|
27
|
+
status,
|
|
28
|
+
message: clientFacingRouteMessage(status, result.message)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function logServerException(scope, error) {
|
|
32
|
+
const details = error instanceof Error
|
|
33
|
+
? (typeof error.stack === 'string' && error.stack.length > 0 ? error.stack : error.message)
|
|
34
|
+
: String(error);
|
|
35
|
+
console.error(`[Zenith:Server] ${scope}\n${details}`);
|
|
36
|
+
}
|
package/dist/server-output.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export function writeServerOutput({ coreOutputDir, staticDir, projectRoot, confi
|
|
|
9
9
|
routes: {
|
|
10
10
|
name: any;
|
|
11
11
|
path: any;
|
|
12
|
+
route_kind: any;
|
|
12
13
|
output: any;
|
|
13
14
|
base_path: string;
|
|
14
15
|
page_asset: any;
|
|
@@ -17,9 +18,11 @@ export function writeServerOutput({ coreOutputDir, staticDir, projectRoot, confi
|
|
|
17
18
|
server_script_path: any;
|
|
18
19
|
guard_module_ref: any;
|
|
19
20
|
load_module_ref: any;
|
|
21
|
+
action_module_ref: any;
|
|
20
22
|
has_guard: boolean;
|
|
21
23
|
has_load: boolean;
|
|
22
|
-
|
|
24
|
+
has_action: boolean;
|
|
25
|
+
params: any[];
|
|
23
26
|
image_manifest_file: string | null;
|
|
24
27
|
image_config: any;
|
|
25
28
|
}[];
|