@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.
- package/README.md +14 -11
- package/dist/adapters/adapter-netlify.js +1 -0
- package/dist/adapters/adapter-node.js +8 -0
- package/dist/adapters/adapter-vercel.js +1 -0
- package/dist/build/compiler-runtime.d.ts +10 -9
- package/dist/build/compiler-runtime.js +51 -1
- 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 +2 -1
- package/dist/build/server-script.js +29 -3
- package/dist/build.js +5 -3
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +82 -39
- 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/index.js +8 -2
- package/dist/manifest.js +3 -2
- package/dist/preview.d.ts +4 -3
- package/dist/preview.js +87 -53
- package/dist/request-body.d.ts +2 -0
- package/dist/request-body.js +13 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +15 -0
- package/dist/server-contract.js +102 -32
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +34 -0
- package/dist/server-output.d.ts +2 -0
- package/dist/server-output.js +13 -0
- package/dist/server-runtime/node-server.js +33 -27
- package/dist/server-runtime/route-render.d.ts +3 -3
- package/dist/server-runtime/route-render.js +20 -31
- package/dist/server-script-composition.d.ts +11 -5
- package/dist/server-script-composition.js +25 -10
- package/package.json +6 -2
package/dist/server-contract.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
package/dist/server-output.d.ts
CHANGED
|
@@ -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;
|
package/dist/server-output.js
CHANGED
|
@@ -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:
|
|
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 || '/',
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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:
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
252
|
-
html
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
271
|
-
return createTextResponse(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
|
};
|