@zenithbuild/cli 0.7.4 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +48 -14
- 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 +67 -19
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +50 -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 +1 -1
- package/dist/build/compiler-runtime.js +8 -2
- package/dist/build/hoisted-code-transforms.d.ts +4 -1
- package/dist/build/hoisted-code-transforms.js +5 -3
- package/dist/build/page-ir-normalization.d.ts +1 -1
- package/dist/build/page-ir-normalization.js +33 -3
- package/dist/build/page-loop-state.js +1 -1
- package/dist/build/page-loop.js +46 -2
- package/dist/build/server-script.d.ts +2 -1
- package/dist/build/server-script.js +7 -3
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +29 -17
- package/dist/dev-build-session/helpers.d.ts +29 -0
- package/dist/dev-build-session/helpers.js +223 -0
- package/dist/dev-build-session/session.d.ts +24 -0
- package/dist/dev-build-session/session.js +204 -0
- package/dist/dev-build-session/state.d.ts +37 -0
- package/dist/dev-build-session/state.js +17 -0
- package/dist/dev-build-session.d.ts +1 -24
- package/dist/dev-build-session.js +1 -434
- package/dist/dev-server/css-state.d.ts +7 -0
- package/dist/dev-server/css-state.js +92 -0
- package/dist/dev-server/not-found.d.ts +23 -0
- package/dist/dev-server/not-found.js +129 -0
- package/dist/dev-server/request-handler.d.ts +1 -0
- package/dist/dev-server/request-handler.js +376 -0
- package/dist/dev-server/route-check.d.ts +9 -0
- package/dist/dev-server/route-check.js +100 -0
- package/dist/dev-server/watcher.d.ts +5 -0
- package/dist/dev-server/watcher.js +216 -0
- package/dist/dev-server.js +136 -883
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/images/payload.js +4 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +70 -6
- package/dist/preview/create-preview-server.d.ts +18 -0
- package/dist/preview/create-preview-server.js +71 -0
- package/dist/preview/manifest.d.ts +42 -0
- package/dist/preview/manifest.js +57 -0
- package/dist/preview/paths.d.ts +3 -0
- package/dist/preview/paths.js +38 -0
- package/dist/preview/payload.d.ts +6 -0
- package/dist/preview/payload.js +34 -0
- package/dist/preview/request-handler.d.ts +1 -0
- package/dist/preview/request-handler.js +300 -0
- package/dist/preview/server-runner.d.ts +49 -0
- package/dist/preview/server-runner.js +220 -0
- package/dist/preview/server-script-runner-template.d.ts +1 -0
- package/dist/preview/server-script-runner-template.js +425 -0
- package/dist/preview.d.ts +5 -104
- package/dist/preview.js +7 -993
- package/dist/request-body.d.ts +0 -1
- package/dist/request-body.js +0 -6
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +49 -0
- package/dist/resource-response.js +160 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.js +1 -1
- package/dist/server-contract/constants.d.ts +5 -0
- package/dist/server-contract/constants.js +5 -0
- package/dist/server-contract/export-validation.d.ts +5 -0
- package/dist/server-contract/export-validation.js +59 -0
- package/dist/server-contract/json-serializable.d.ts +1 -0
- package/dist/server-contract/json-serializable.js +52 -0
- package/dist/server-contract/resolve.d.ts +15 -0
- package/dist/server-contract/resolve.js +271 -0
- package/dist/server-contract/result-helpers.d.ts +51 -0
- package/dist/server-contract/result-helpers.js +59 -0
- package/dist/server-contract/route-result-validation.d.ts +2 -0
- package/dist/server-contract/route-result-validation.js +73 -0
- package/dist/server-contract/stage.d.ts +6 -0
- package/dist/server-contract/stage.js +22 -0
- package/dist/server-contract.d.ts +6 -54
- package/dist/server-contract.js +9 -301
- package/dist/server-error.d.ts +1 -1
- package/dist/server-error.js +2 -0
- package/dist/server-middleware.d.ts +10 -0
- package/dist/server-middleware.js +30 -0
- package/dist/server-output.d.ts +2 -1
- package/dist/server-output.js +72 -12
- package/dist/server-runtime/node-server.js +59 -7
- package/dist/server-runtime/route-render.d.ts +25 -1
- package/dist/server-runtime/route-render.js +81 -29
- package/dist/server-script-composition.d.ts +4 -2
- package/dist/server-script-composition.js +6 -3
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +3 -3
package/dist/server-contract.js
CHANGED
|
@@ -1,304 +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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
kind: 'redirect',
|
|
14
|
-
location: String(location || ''),
|
|
15
|
-
status: Number.isInteger(status) ? status : 302
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
export function deny(status = 403, message = undefined) {
|
|
19
|
-
return {
|
|
20
|
-
kind: 'deny',
|
|
21
|
-
status: Number.isInteger(status) ? status : 403,
|
|
22
|
-
message: typeof message === 'string' ? message : undefined
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
export function data(payload) {
|
|
26
|
-
return { kind: 'data', data: payload };
|
|
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
|
-
}
|
|
35
|
-
function isRouteResultLike(value) {
|
|
36
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
const kind = value.kind;
|
|
40
|
-
return typeof kind === 'string' && ROUTE_RESULT_KINDS.has(kind);
|
|
41
|
-
}
|
|
42
|
-
function assertValidRouteResultShape(value, where, allowedKinds) {
|
|
43
|
-
if (!isRouteResultLike(value)) {
|
|
44
|
-
throw new Error(`[Zenith] ${where}: invalid route result. Expected object with kind.`);
|
|
45
|
-
}
|
|
46
|
-
const kind = value.kind;
|
|
47
|
-
if (!allowedKinds.has(kind)) {
|
|
48
|
-
throw new Error(`[Zenith] ${where}: kind "${kind}" is not allowed here (allowed: ${Array.from(allowedKinds).join(', ')}).`);
|
|
49
|
-
}
|
|
50
|
-
if (kind === 'redirect') {
|
|
51
|
-
if (typeof value.location !== 'string' || value.location.length === 0) {
|
|
52
|
-
throw new Error(`[Zenith] ${where}: redirect requires non-empty string location.`);
|
|
53
|
-
}
|
|
54
|
-
if (value.status !== undefined && (!Number.isInteger(value.status) || value.status < 300 || value.status > 399)) {
|
|
55
|
-
throw new Error(`[Zenith] ${where}: redirect status must be an integer 3xx.`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (kind === 'deny') {
|
|
59
|
-
if (!Number.isInteger(value.status) ||
|
|
60
|
-
(value.status !== 401 && value.status !== 403 && value.status !== 404)) {
|
|
61
|
-
throw new Error(`[Zenith] ${where}: deny status must be 401, 403, or 404.`);
|
|
62
|
-
}
|
|
63
|
-
if (value.message !== undefined && typeof value.message !== 'string') {
|
|
64
|
-
throw new Error(`[Zenith] ${where}: deny message must be a string when provided.`);
|
|
65
|
-
}
|
|
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;
|
|
105
|
-
}
|
|
106
|
-
export function validateServerExports({ exports, filePath }) {
|
|
107
|
-
const exportKeys = Object.keys(exports);
|
|
108
|
-
const illegalKeys = exportKeys.filter(k => !ALLOWED_KEYS.has(k));
|
|
109
|
-
if (illegalKeys.length > 0) {
|
|
110
|
-
throw new Error(`[Zenith] ${filePath}: illegal export(s): ${illegalKeys.join(', ')}`);
|
|
111
|
-
}
|
|
112
|
-
const hasData = 'data' in exports;
|
|
113
|
-
const hasLoad = 'load' in exports;
|
|
114
|
-
const hasGuard = 'guard' in exports;
|
|
115
|
-
const hasAction = 'action' in exports;
|
|
116
|
-
const hasNew = hasData || hasLoad || hasAction;
|
|
117
|
-
const hasLegacy = ('ssr_data' in exports) || ('props' in exports) || ('ssr' in exports);
|
|
118
|
-
if (hasData && hasLoad) {
|
|
119
|
-
throw new Error(`[Zenith] ${filePath}: cannot export both "data" and "load". Choose one.`);
|
|
120
|
-
}
|
|
121
|
-
if (hasNew && hasLegacy) {
|
|
122
|
-
throw new Error(`[Zenith] ${filePath}: cannot mix new ("data"/"load") with legacy ("ssr_data"/"props"/"ssr") exports.`);
|
|
123
|
-
}
|
|
124
|
-
if ('prerender' in exports && typeof exports.prerender !== 'boolean') {
|
|
125
|
-
throw new Error(`[Zenith] ${filePath}: "prerender" must be a boolean.`);
|
|
126
|
-
}
|
|
127
|
-
if (hasLoad) {
|
|
128
|
-
assertOneArgRouteFunction({ filePath, exportName: 'load', value: exports.load });
|
|
129
|
-
}
|
|
130
|
-
if (hasGuard) {
|
|
131
|
-
assertOneArgRouteFunction({ filePath, exportName: 'guard', value: exports.guard });
|
|
132
|
-
}
|
|
133
|
-
if (hasAction) {
|
|
134
|
-
assertOneArgRouteFunction({ filePath, exportName: 'action', value: exports.action });
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
export function assertJsonSerializable(value, where = 'payload') {
|
|
138
|
-
const seen = new Set();
|
|
139
|
-
function walk(v, path) {
|
|
140
|
-
const t = typeof v;
|
|
141
|
-
if (v === null)
|
|
142
|
-
return;
|
|
143
|
-
if (t === 'string' || t === 'number' || t === 'boolean')
|
|
144
|
-
return;
|
|
145
|
-
if (t === 'bigint' || t === 'function' || t === 'symbol') {
|
|
146
|
-
throw new Error(`[Zenith] ${where}: non-serializable ${t} at ${path}`);
|
|
147
|
-
}
|
|
148
|
-
if (t === 'undefined') {
|
|
149
|
-
throw new Error(`[Zenith] ${where}: undefined is not allowed at ${path}`);
|
|
150
|
-
}
|
|
151
|
-
if (v instanceof Date) {
|
|
152
|
-
throw new Error(`[Zenith] ${where}: Date is not allowed at ${path} (convert to ISO string)`);
|
|
153
|
-
}
|
|
154
|
-
if (v instanceof Map || v instanceof Set) {
|
|
155
|
-
throw new Error(`[Zenith] ${where}: Map/Set not allowed at ${path}`);
|
|
156
|
-
}
|
|
157
|
-
if (t === 'object') {
|
|
158
|
-
if (seen.has(v))
|
|
159
|
-
throw new Error(`[Zenith] ${where}: circular reference at ${path}`);
|
|
160
|
-
seen.add(v);
|
|
161
|
-
if (Array.isArray(v)) {
|
|
162
|
-
if (path === '$') {
|
|
163
|
-
throw new Error(`[Zenith] ${where}: top-level payload must be a plain object, not an array at ${path}`);
|
|
164
|
-
}
|
|
165
|
-
for (let i = 0; i < v.length; i++)
|
|
166
|
-
walk(v[i], `${path}[${i}]`);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const proto = Object.getPrototypeOf(v);
|
|
170
|
-
const isPlainObject = proto === null ||
|
|
171
|
-
proto === Object.prototype ||
|
|
172
|
-
(proto && proto.constructor && proto.constructor.name === 'Object');
|
|
173
|
-
if (!isPlainObject) {
|
|
174
|
-
throw new Error(`[Zenith] ${where}: non-plain object at ${path}`);
|
|
175
|
-
}
|
|
176
|
-
for (const k of Object.keys(v)) {
|
|
177
|
-
if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
|
|
178
|
-
throw new Error(`[Zenith] ${where}: forbidden prototype pollution key "${k}" at ${path}.${k}`);
|
|
179
|
-
}
|
|
180
|
-
walk(v[k], `${path}.${k}`);
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
throw new Error(`[Zenith] ${where}: unsupported type at ${path}`);
|
|
185
|
-
}
|
|
186
|
-
walk(value, '$');
|
|
187
|
-
}
|
|
188
|
-
export async function resolveRouteResult({ exports, ctx, filePath, guardOnly = false }) {
|
|
189
|
-
validateServerExports({ exports, filePath });
|
|
190
|
-
const trace = {
|
|
191
|
-
guard: 'none',
|
|
192
|
-
action: 'none',
|
|
193
|
-
load: 'none'
|
|
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
|
-
}
|
|
201
|
-
if ('guard' in exports) {
|
|
202
|
-
const guardRaw = await exports.guard(ctx);
|
|
203
|
-
const guardResult = guardRaw == null ? allow() : guardRaw;
|
|
204
|
-
if (guardResult.kind === 'data') {
|
|
205
|
-
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.`);
|
|
206
|
-
}
|
|
207
|
-
assertValidRouteResultShape(guardResult, `${filePath}: guard(ctx) return`, new Set(['allow', 'redirect', 'deny']));
|
|
208
|
-
trace.guard = guardResult.kind;
|
|
209
|
-
if (guardResult.kind === 'redirect' || guardResult.kind === 'deny') {
|
|
210
|
-
return { result: guardResult, trace };
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (guardOnly) {
|
|
214
|
-
return { result: allow(), trace };
|
|
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
|
-
}
|
|
242
|
-
let payload;
|
|
243
|
-
if ('load' in exports) {
|
|
244
|
-
const loadRaw = await exports.load(ctx);
|
|
245
|
-
let loadResult = null;
|
|
246
|
-
if (isRouteResultLike(loadRaw)) {
|
|
247
|
-
loadResult = loadRaw;
|
|
248
|
-
assertValidRouteResultShape(loadResult, `${filePath}: load(ctx) return`, new Set(['data', 'redirect', 'deny']));
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
assertJsonSerializable(loadRaw, `${filePath}: load(ctx) return`);
|
|
252
|
-
loadResult = data(loadRaw);
|
|
253
|
-
}
|
|
254
|
-
trace.load = loadResult.kind;
|
|
255
|
-
return { result: loadResult, trace, status: loadResult.kind === 'data' ? responseStatus : undefined };
|
|
256
|
-
}
|
|
257
|
-
if ('data' in exports) {
|
|
258
|
-
payload = exports.data;
|
|
259
|
-
assertJsonSerializable(payload, `${filePath}: data export`);
|
|
260
|
-
trace.load = 'data';
|
|
261
|
-
return { result: data(payload), trace, status: responseStatus };
|
|
262
|
-
}
|
|
263
|
-
// legacy fallback
|
|
264
|
-
if ('ssr_data' in exports) {
|
|
265
|
-
payload = exports.ssr_data;
|
|
266
|
-
assertJsonSerializable(payload, `${filePath}: ssr_data export`);
|
|
267
|
-
trace.load = 'data';
|
|
268
|
-
return { result: data(payload), trace, status: responseStatus };
|
|
269
|
-
}
|
|
270
|
-
if ('props' in exports) {
|
|
271
|
-
payload = exports.props;
|
|
272
|
-
assertJsonSerializable(payload, `${filePath}: props export`);
|
|
273
|
-
trace.load = 'data';
|
|
274
|
-
return { result: data(payload), trace, status: responseStatus };
|
|
275
|
-
}
|
|
276
|
-
if ('ssr' in exports) {
|
|
277
|
-
payload = exports.ssr;
|
|
278
|
-
assertJsonSerializable(payload, `${filePath}: ssr export`);
|
|
279
|
-
trace.load = 'data';
|
|
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
|
-
};
|
|
289
|
-
}
|
|
290
|
-
return { result: data({}), trace, status: responseStatus };
|
|
291
|
-
}
|
|
292
|
-
export async function resolveServerPayload({ exports, ctx, filePath }) {
|
|
293
|
-
const resolved = await resolveRouteResult({ exports, ctx, filePath });
|
|
294
|
-
if (!resolved || !resolved.result || typeof resolved.result !== 'object') {
|
|
295
|
-
return {};
|
|
296
|
-
}
|
|
297
|
-
if (resolved.result.kind === 'data') {
|
|
298
|
-
return resolved.result.data;
|
|
299
|
-
}
|
|
300
|
-
if (resolved.result.kind === 'allow') {
|
|
301
|
-
return {};
|
|
302
|
-
}
|
|
303
|
-
throw new Error(`[Zenith] ${filePath}: resolveServerPayload() expected data but received ${resolved.result.kind}. Use resolveRouteResult() for guard/load flows.`);
|
|
304
|
-
}
|
|
4
|
+
//
|
|
5
|
+
// This file is intentionally a thin composition/export surface.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
import { withMiddleware } from './server-middleware.js';
|
|
8
|
+
export { allow, redirect, deny, data, invalid, json, text, download, stream, sse } from './server-contract/result-helpers.js';
|
|
9
|
+
export { validateServerExports } from './server-contract/export-validation.js';
|
|
10
|
+
export { assertJsonSerializable } from './server-contract/json-serializable.js';
|
|
11
|
+
export { resolveRouteResult, resolveServerPayload } from './server-contract/resolve.js';
|
|
12
|
+
export { withMiddleware };
|
package/dist/server-error.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function defaultRouteDenyMessage(status: any): "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
|
|
1
|
+
export function defaultRouteDenyMessage(status: any): "Unauthorized" | "Forbidden" | "Not Found" | "Method Not Allowed" | "Internal Server Error";
|
|
2
2
|
export function clientFacingRouteMessage(status: any, message: any): string;
|
|
3
3
|
export function sanitizeRouteResult(result: any): any;
|
|
4
4
|
export function logServerException(scope: any, error: any): void;
|
package/dist/server-error.js
CHANGED
|
@@ -5,6 +5,8 @@ export function defaultRouteDenyMessage(status) {
|
|
|
5
5
|
return 'Forbidden';
|
|
6
6
|
if (status === 404)
|
|
7
7
|
return 'Not Found';
|
|
8
|
+
if (status === 405)
|
|
9
|
+
return 'Method Not Allowed';
|
|
8
10
|
return 'Internal Server Error';
|
|
9
11
|
}
|
|
10
12
|
export function clientFacingRouteMessage(status, message) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose route middleware left-to-right:
|
|
3
|
+
* withMiddleware(handler, a, b) === a(b(handler))
|
|
4
|
+
*
|
|
5
|
+
* @template {(ctx: unknown) => unknown} T
|
|
6
|
+
* @param {T} handler
|
|
7
|
+
* @param {...((next: T) => T)} middleware
|
|
8
|
+
* @returns {T}
|
|
9
|
+
*/
|
|
10
|
+
export function withMiddleware<T extends (ctx: unknown) => unknown>(handler: T, ...middleware: ((next: T) => T)[]): T;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const WITH_MIDDLEWARE_PREFIX = '[Zenith] withMiddleware(handler, ...middleware)';
|
|
2
|
+
function assertFunction(value, message) {
|
|
3
|
+
if (typeof value !== 'function') {
|
|
4
|
+
throw new Error(message);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compose route middleware left-to-right:
|
|
9
|
+
* withMiddleware(handler, a, b) === a(b(handler))
|
|
10
|
+
*
|
|
11
|
+
* @template {(ctx: unknown) => unknown} T
|
|
12
|
+
* @param {T} handler
|
|
13
|
+
* @param {...((next: T) => T)} middleware
|
|
14
|
+
* @returns {T}
|
|
15
|
+
*/
|
|
16
|
+
export function withMiddleware(handler, ...middleware) {
|
|
17
|
+
assertFunction(handler, `${WITH_MIDDLEWARE_PREFIX}: handler must be a function.`);
|
|
18
|
+
if (middleware.length === 0) {
|
|
19
|
+
return handler;
|
|
20
|
+
}
|
|
21
|
+
let composed = handler;
|
|
22
|
+
for (let index = middleware.length - 1; index >= 0; index -= 1) {
|
|
23
|
+
const candidate = middleware[index];
|
|
24
|
+
assertFunction(candidate, `${WITH_MIDDLEWARE_PREFIX}: middleware at index ${index} must be a function.`);
|
|
25
|
+
const wrapped = candidate(composed);
|
|
26
|
+
assertFunction(wrapped, `${WITH_MIDDLEWARE_PREFIX}: middleware at index ${index} must return a function.`);
|
|
27
|
+
composed = wrapped;
|
|
28
|
+
}
|
|
29
|
+
return /** @type {T} */ (composed);
|
|
30
|
+
}
|
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;
|
|
@@ -21,7 +22,7 @@ export function writeServerOutput({ coreOutputDir, staticDir, projectRoot, confi
|
|
|
21
22
|
has_guard: boolean;
|
|
22
23
|
has_load: boolean;
|
|
23
24
|
has_action: boolean;
|
|
24
|
-
params:
|
|
25
|
+
params: any[];
|
|
25
26
|
image_manifest_file: string | null;
|
|
26
27
|
image_config: any;
|
|
27
28
|
}[];
|
package/dist/server-output.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { loadResourceRouteManifest } from './resource-manifest.js';
|
|
6
7
|
const PACKAGE_REQUIRE = createRequire(import.meta.url);
|
|
7
8
|
const RELATIVE_SPECIFIER_RE = /((?:import|export)\s+(?:[^'"]*?\s+from\s+)?|import\s*\()\s*(['"])([^'"]+)\2/g;
|
|
8
9
|
const SERVER_RUNTIME_FILES = [
|
|
@@ -14,6 +15,19 @@ const SERVER_RUNTIME_FILES = [
|
|
|
14
15
|
from: new URL('./server-contract.js', import.meta.url),
|
|
15
16
|
to: 'server-contract.js'
|
|
16
17
|
},
|
|
18
|
+
{
|
|
19
|
+
from: new URL('./server-contract', import.meta.url),
|
|
20
|
+
to: 'server-contract',
|
|
21
|
+
recursive: true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
from: new URL('./server-middleware.js', import.meta.url),
|
|
25
|
+
to: 'server-middleware.js'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
from: new URL('./auth/route-auth.js', import.meta.url),
|
|
29
|
+
to: 'auth/route-auth.js'
|
|
30
|
+
},
|
|
17
31
|
{
|
|
18
32
|
from: new URL('./base-path.js', import.meta.url),
|
|
19
33
|
to: 'base-path.js'
|
|
@@ -34,11 +48,27 @@ const SERVER_RUNTIME_FILES = [
|
|
|
34
48
|
from: new URL('./images/runtime.js', import.meta.url),
|
|
35
49
|
to: 'images/runtime.js'
|
|
36
50
|
},
|
|
51
|
+
{
|
|
52
|
+
from: new URL('./images/service.js', import.meta.url),
|
|
53
|
+
to: 'images/service.js'
|
|
54
|
+
},
|
|
37
55
|
{
|
|
38
56
|
from: new URL('./server-error.js', import.meta.url),
|
|
39
57
|
to: 'server-error.js'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
from: new URL('./resource-response.js', import.meta.url),
|
|
61
|
+
to: 'resource-response.js'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
from: new URL('./download-result.js', import.meta.url),
|
|
65
|
+
to: 'download-result.js'
|
|
40
66
|
}
|
|
41
67
|
];
|
|
68
|
+
const SPECIAL_SERVER_SPECIFIERS = new Map([
|
|
69
|
+
['zenith:server-contract', 'server-contract.js'],
|
|
70
|
+
['zenith:route-auth', 'auth/route-auth.js']
|
|
71
|
+
]);
|
|
42
72
|
function normalizeRouteName(routePath) {
|
|
43
73
|
if (routePath === '/') {
|
|
44
74
|
return 'index';
|
|
@@ -152,7 +182,7 @@ function outputPathForSource(projectRoot, modulesRoot, sourcePath) {
|
|
|
152
182
|
: relativePath.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
|
|
153
183
|
return join(modulesRoot, nextRelative);
|
|
154
184
|
}
|
|
155
|
-
async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts, seen }) {
|
|
185
|
+
async function compileImportedModule({ projectRoot, modulesRoot, serverDir, sourcePath, ts, seen }) {
|
|
156
186
|
if (seen.has(sourcePath)) {
|
|
157
187
|
return outputPathForSource(projectRoot, modulesRoot, sourcePath);
|
|
158
188
|
}
|
|
@@ -166,6 +196,12 @@ async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts,
|
|
|
166
196
|
const source = await readFile(sourcePath, 'utf8');
|
|
167
197
|
let output = transpileSource(ts, source, sourcePath);
|
|
168
198
|
for (const specifier of gatherSpecifiers(output)) {
|
|
199
|
+
const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
|
|
200
|
+
if (specialSpecifierPath) {
|
|
201
|
+
const nextSpecifier = relative(dirname(outPath), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
|
|
202
|
+
output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
169
205
|
if (!isRelativeSpecifier(specifier)) {
|
|
170
206
|
continue;
|
|
171
207
|
}
|
|
@@ -176,6 +212,7 @@ async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts,
|
|
|
176
212
|
const compiledDependencyPath = await compileImportedModule({
|
|
177
213
|
projectRoot,
|
|
178
214
|
modulesRoot,
|
|
215
|
+
serverDir,
|
|
179
216
|
sourcePath: resolvedPath,
|
|
180
217
|
ts,
|
|
181
218
|
seen
|
|
@@ -186,12 +223,18 @@ async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts,
|
|
|
186
223
|
await writeFile(outPath, output, 'utf8');
|
|
187
224
|
return outPath;
|
|
188
225
|
}
|
|
189
|
-
async function writeRouteModulePackage({ projectRoot, routeDir, route }) {
|
|
226
|
+
async function writeRouteModulePackage({ projectRoot, serverDir, routeDir, route }) {
|
|
190
227
|
const ts = resolveTypeScriptApi(projectRoot);
|
|
191
228
|
const modulesRoot = join(routeDir, 'modules');
|
|
192
229
|
const seen = new Set();
|
|
193
230
|
let entryOutput = transpileSource(ts, route.server_script || '', route.server_script_path || 'route-entry.ts');
|
|
194
231
|
for (const specifier of gatherSpecifiers(entryOutput)) {
|
|
232
|
+
const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
|
|
233
|
+
if (specialSpecifierPath) {
|
|
234
|
+
const nextSpecifier = relative(join(routeDir, 'route'), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
|
|
235
|
+
entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
195
238
|
if (!isRelativeSpecifier(specifier)) {
|
|
196
239
|
continue;
|
|
197
240
|
}
|
|
@@ -202,6 +245,7 @@ async function writeRouteModulePackage({ projectRoot, routeDir, route }) {
|
|
|
202
245
|
const compiledDependencyPath = await compileImportedModule({
|
|
203
246
|
projectRoot,
|
|
204
247
|
modulesRoot,
|
|
248
|
+
serverDir,
|
|
205
249
|
sourcePath: resolvedPath,
|
|
206
250
|
ts,
|
|
207
251
|
seen
|
|
@@ -217,7 +261,10 @@ async function copyRuntimeFiles(serverDir) {
|
|
|
217
261
|
for (const file of SERVER_RUNTIME_FILES) {
|
|
218
262
|
const targetPath = join(serverDir, file.to);
|
|
219
263
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
220
|
-
await cp(file.from, targetPath, {
|
|
264
|
+
await cp(file.from, targetPath, {
|
|
265
|
+
force: true,
|
|
266
|
+
recursive: file.recursive === true
|
|
267
|
+
});
|
|
221
268
|
}
|
|
222
269
|
}
|
|
223
270
|
async function copyOptionalFile(sourcePath, targetPath) {
|
|
@@ -238,8 +285,15 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
|
|
|
238
285
|
catch {
|
|
239
286
|
routerManifest = { routes: [] };
|
|
240
287
|
}
|
|
241
|
-
const
|
|
242
|
-
const
|
|
288
|
+
const resourceManifest = await loadResourceRouteManifest(staticDir, basePath);
|
|
289
|
+
const pageRoutes = Array.isArray(routerManifest.routes) ? routerManifest.routes : [];
|
|
290
|
+
const serverRoutes = pageRoutes
|
|
291
|
+
.filter((route) => route.server_script && route.prerender !== true)
|
|
292
|
+
.map((route) => ({ ...route, route_kind: 'page' }))
|
|
293
|
+
.concat((Array.isArray(resourceManifest.routes) ? resourceManifest.routes : []).map((route) => ({
|
|
294
|
+
...route,
|
|
295
|
+
route_kind: 'resource'
|
|
296
|
+
})));
|
|
243
297
|
await mkdir(serverDir, { recursive: true });
|
|
244
298
|
await copyRuntimeFiles(serverDir);
|
|
245
299
|
const imageManifestSource = join(staticDir, '_zenith', 'image', 'manifest.json');
|
|
@@ -248,8 +302,10 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
|
|
|
248
302
|
const name = normalizeRouteName(route.path);
|
|
249
303
|
const routeDir = join(serverDir, 'routes', name);
|
|
250
304
|
await mkdir(routeDir, { recursive: true });
|
|
251
|
-
|
|
252
|
-
|
|
305
|
+
if (route.route_kind !== 'resource') {
|
|
306
|
+
const htmlSourcePath = join(staticDir, String(route.output || '').replace(/^\//, ''));
|
|
307
|
+
await copyOptionalFile(htmlSourcePath, join(routeDir, 'route', 'page.html'));
|
|
308
|
+
}
|
|
253
309
|
let pageAssetFile = null;
|
|
254
310
|
if (typeof route.page_asset === 'string' && route.page_asset.length > 0) {
|
|
255
311
|
const assetSourcePath = join(staticDir, route.page_asset.replace(/^\//, ''));
|
|
@@ -259,18 +315,20 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
|
|
|
259
315
|
}
|
|
260
316
|
}
|
|
261
317
|
let imageManifestFile = null;
|
|
262
|
-
if (await copyOptionalFile(imageManifestSource, join(routeDir, 'route', 'image-manifest.json'))) {
|
|
318
|
+
if (route.route_kind !== 'resource' && await copyOptionalFile(imageManifestSource, join(routeDir, 'route', 'image-manifest.json'))) {
|
|
263
319
|
imageManifestFile = 'image-manifest.json';
|
|
264
320
|
}
|
|
265
321
|
await writeRouteModulePackage({
|
|
266
322
|
projectRoot,
|
|
323
|
+
serverDir,
|
|
267
324
|
routeDir,
|
|
268
325
|
route
|
|
269
326
|
});
|
|
270
327
|
const meta = {
|
|
271
328
|
name,
|
|
272
329
|
path: route.path,
|
|
273
|
-
|
|
330
|
+
route_kind: route.route_kind || 'page',
|
|
331
|
+
output: route.output || null,
|
|
274
332
|
base_path: basePath,
|
|
275
333
|
page_asset: route.page_asset || null,
|
|
276
334
|
page_asset_file: pageAssetFile,
|
|
@@ -282,11 +340,13 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
|
|
|
282
340
|
has_guard: route.has_guard === true,
|
|
283
341
|
has_load: route.has_load === true,
|
|
284
342
|
has_action: route.has_action === true,
|
|
285
|
-
params:
|
|
286
|
-
|
|
343
|
+
params: Array.isArray(route.params) && route.params.length > 0
|
|
344
|
+
? [...route.params]
|
|
345
|
+
: extractRouteParams(route.path),
|
|
346
|
+
image_manifest_file: route.route_kind === 'resource' ? null : imageManifestFile,
|
|
287
347
|
image_config: config?.images || {}
|
|
288
348
|
};
|
|
289
|
-
if (Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
|
|
349
|
+
if (route.route_kind !== 'resource' && Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
|
|
290
350
|
meta.image_materialization = route.image_materialization;
|
|
291
351
|
}
|
|
292
352
|
await writeFile(join(routeDir, 'route.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|