@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.
Files changed (112) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/adapter-netlify.d.ts +1 -1
  3. package/dist/adapters/adapter-netlify.js +48 -14
  4. package/dist/adapters/adapter-static-export.d.ts +5 -0
  5. package/dist/adapters/adapter-static-export.js +115 -0
  6. package/dist/adapters/adapter-types.d.ts +3 -1
  7. package/dist/adapters/adapter-types.js +5 -2
  8. package/dist/adapters/adapter-vercel.d.ts +1 -1
  9. package/dist/adapters/adapter-vercel.js +67 -19
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +50 -0
  12. package/dist/adapters/resolve-adapter.js +4 -0
  13. package/dist/adapters/route-rules.d.ts +5 -0
  14. package/dist/adapters/route-rules.js +9 -0
  15. package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
  16. package/dist/adapters/validate-hosted-resource-routes.js +13 -0
  17. package/dist/auth/route-auth.d.ts +6 -0
  18. package/dist/auth/route-auth.js +236 -0
  19. package/dist/build/compiler-runtime.d.ts +1 -1
  20. package/dist/build/compiler-runtime.js +8 -2
  21. package/dist/build/hoisted-code-transforms.d.ts +4 -1
  22. package/dist/build/hoisted-code-transforms.js +5 -3
  23. package/dist/build/page-ir-normalization.d.ts +1 -1
  24. package/dist/build/page-ir-normalization.js +33 -3
  25. package/dist/build/page-loop-state.js +1 -1
  26. package/dist/build/page-loop.js +46 -2
  27. package/dist/build/server-script.d.ts +2 -1
  28. package/dist/build/server-script.js +7 -3
  29. package/dist/build-output-manifest.d.ts +3 -2
  30. package/dist/build-output-manifest.js +3 -0
  31. package/dist/build.js +29 -17
  32. package/dist/dev-build-session/helpers.d.ts +29 -0
  33. package/dist/dev-build-session/helpers.js +223 -0
  34. package/dist/dev-build-session/session.d.ts +24 -0
  35. package/dist/dev-build-session/session.js +204 -0
  36. package/dist/dev-build-session/state.d.ts +37 -0
  37. package/dist/dev-build-session/state.js +17 -0
  38. package/dist/dev-build-session.d.ts +1 -24
  39. package/dist/dev-build-session.js +1 -434
  40. package/dist/dev-server/css-state.d.ts +7 -0
  41. package/dist/dev-server/css-state.js +92 -0
  42. package/dist/dev-server/not-found.d.ts +23 -0
  43. package/dist/dev-server/not-found.js +129 -0
  44. package/dist/dev-server/request-handler.d.ts +1 -0
  45. package/dist/dev-server/request-handler.js +376 -0
  46. package/dist/dev-server/route-check.d.ts +9 -0
  47. package/dist/dev-server/route-check.js +100 -0
  48. package/dist/dev-server/watcher.d.ts +5 -0
  49. package/dist/dev-server/watcher.js +216 -0
  50. package/dist/dev-server.js +136 -883
  51. package/dist/download-result.d.ts +14 -0
  52. package/dist/download-result.js +148 -0
  53. package/dist/images/payload.js +4 -0
  54. package/dist/images/service.d.ts +13 -1
  55. package/dist/images/service.js +45 -15
  56. package/dist/manifest.d.ts +15 -1
  57. package/dist/manifest.js +70 -6
  58. package/dist/preview/create-preview-server.d.ts +18 -0
  59. package/dist/preview/create-preview-server.js +71 -0
  60. package/dist/preview/manifest.d.ts +42 -0
  61. package/dist/preview/manifest.js +57 -0
  62. package/dist/preview/paths.d.ts +3 -0
  63. package/dist/preview/paths.js +38 -0
  64. package/dist/preview/payload.d.ts +6 -0
  65. package/dist/preview/payload.js +34 -0
  66. package/dist/preview/request-handler.d.ts +1 -0
  67. package/dist/preview/request-handler.js +300 -0
  68. package/dist/preview/server-runner.d.ts +49 -0
  69. package/dist/preview/server-runner.js +220 -0
  70. package/dist/preview/server-script-runner-template.d.ts +1 -0
  71. package/dist/preview/server-script-runner-template.js +425 -0
  72. package/dist/preview.d.ts +5 -104
  73. package/dist/preview.js +7 -993
  74. package/dist/request-body.d.ts +0 -1
  75. package/dist/request-body.js +0 -6
  76. package/dist/resource-manifest.d.ts +16 -0
  77. package/dist/resource-manifest.js +53 -0
  78. package/dist/resource-response.d.ts +49 -0
  79. package/dist/resource-response.js +160 -0
  80. package/dist/resource-route-module.d.ts +15 -0
  81. package/dist/resource-route-module.js +129 -0
  82. package/dist/route-check-support.js +1 -1
  83. package/dist/server-contract/constants.d.ts +5 -0
  84. package/dist/server-contract/constants.js +5 -0
  85. package/dist/server-contract/export-validation.d.ts +5 -0
  86. package/dist/server-contract/export-validation.js +59 -0
  87. package/dist/server-contract/json-serializable.d.ts +1 -0
  88. package/dist/server-contract/json-serializable.js +52 -0
  89. package/dist/server-contract/resolve.d.ts +15 -0
  90. package/dist/server-contract/resolve.js +271 -0
  91. package/dist/server-contract/result-helpers.d.ts +51 -0
  92. package/dist/server-contract/result-helpers.js +59 -0
  93. package/dist/server-contract/route-result-validation.d.ts +2 -0
  94. package/dist/server-contract/route-result-validation.js +73 -0
  95. package/dist/server-contract/stage.d.ts +6 -0
  96. package/dist/server-contract/stage.js +22 -0
  97. package/dist/server-contract.d.ts +6 -54
  98. package/dist/server-contract.js +9 -301
  99. package/dist/server-error.d.ts +1 -1
  100. package/dist/server-error.js +2 -0
  101. package/dist/server-middleware.d.ts +10 -0
  102. package/dist/server-middleware.js +30 -0
  103. package/dist/server-output.d.ts +2 -1
  104. package/dist/server-output.js +72 -12
  105. package/dist/server-runtime/node-server.js +59 -7
  106. package/dist/server-runtime/route-render.d.ts +25 -1
  107. package/dist/server-runtime/route-render.js +81 -29
  108. package/dist/server-script-composition.d.ts +4 -2
  109. package/dist/server-script-composition.js +6 -3
  110. package/dist/static-export-paths.d.ts +3 -0
  111. package/dist/static-export-paths.js +160 -0
  112. package/package.json +3 -3
@@ -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
- const NEW_KEYS = new Set(['data', 'load', 'guard', 'action', 'prerender']);
5
- const LEGACY_KEYS = new Set(['ssr_data', 'props', 'ssr', 'prerender']);
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
- export function allow() {
9
- return { kind: 'allow' };
10
- }
11
- export function redirect(location, status = 302) {
12
- return {
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 };
@@ -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;
@@ -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
+ }
@@ -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: string[];
25
+ params: any[];
25
26
  image_manifest_file: string | null;
26
27
  image_config: any;
27
28
  }[];
@@ -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, { force: true });
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 routes = Array.isArray(routerManifest.routes) ? routerManifest.routes : [];
242
- const serverRoutes = routes.filter((route) => route.server_script && route.prerender !== true);
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
- const htmlSourcePath = join(staticDir, String(route.output || '').replace(/^\//, ''));
252
- await copyOptionalFile(htmlSourcePath, join(routeDir, 'route', 'page.html'));
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
- output: route.output,
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: extractRouteParams(route.path),
286
- image_manifest_file: imageManifestFile,
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');