@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,2 +1 @@
1
1
  export function readRequestBodyBuffer(req: any): Promise<Buffer<ArrayBuffer>>;
2
- export function encodeRequestBodyBase64(bodyBuffer: any): string;
@@ -5,9 +5,3 @@ export async function readRequestBodyBuffer(req) {
5
5
  }
6
6
  return Buffer.concat(chunks);
7
7
  }
8
- export function encodeRequestBodyBase64(bodyBuffer) {
9
- if (!Buffer.isBuffer(bodyBuffer) || bodyBuffer.length === 0) {
10
- return '';
11
- }
12
- return bodyBuffer.toString('base64');
13
- }
@@ -0,0 +1,16 @@
1
+ export function writeResourceRouteManifest(staticDir: any, routeManifest: any, basePath?: string): Promise<({
2
+ path: any;
3
+ file: any;
4
+ route_kind: string;
5
+ server_script: any;
6
+ server_script_path: any;
7
+ has_guard: boolean;
8
+ has_load: boolean;
9
+ has_action: boolean;
10
+ params: any;
11
+ route_id: any;
12
+ } | null)[]>;
13
+ export function loadResourceRouteManifest(distDir: any, fallbackBasePath?: string): Promise<{
14
+ basePath: any;
15
+ routes: any;
16
+ }>;
@@ -0,0 +1,53 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { compareRouteSpecificity } from './server/resolve-request-route.js';
4
+ function sanitizeResourceRoute(entry) {
5
+ if (!entry || typeof entry !== 'object') {
6
+ return null;
7
+ }
8
+ if (typeof entry.path !== 'string' || typeof entry.server_script !== 'string') {
9
+ return null;
10
+ }
11
+ return {
12
+ path: entry.path,
13
+ file: typeof entry.file === 'string' ? entry.file : '',
14
+ route_kind: 'resource',
15
+ server_script: entry.server_script,
16
+ server_script_path: typeof entry.server_script_path === 'string' ? entry.server_script_path : '',
17
+ has_guard: entry.has_guard === true,
18
+ has_load: entry.has_load === true,
19
+ has_action: entry.has_action === true,
20
+ params: Array.isArray(entry.params) ? entry.params.filter((value) => typeof value === 'string') : [],
21
+ route_id: typeof entry.route_id === 'string' ? entry.route_id : null
22
+ };
23
+ }
24
+ export async function writeResourceRouteManifest(staticDir, routeManifest, basePath = '/') {
25
+ const routes = (Array.isArray(routeManifest) ? routeManifest : [])
26
+ .filter((entry) => entry?.route_kind === 'resource')
27
+ .map((entry) => sanitizeResourceRoute(entry))
28
+ .filter(Boolean)
29
+ .sort((left, right) => compareRouteSpecificity(left.path, right.path));
30
+ const manifestPath = join(staticDir, 'assets', 'resource-manifest.json');
31
+ await mkdir(join(staticDir, 'assets'), { recursive: true });
32
+ await writeFile(manifestPath, `${JSON.stringify({ base_path: basePath, routes }, null, 2)}\n`, 'utf8');
33
+ return routes;
34
+ }
35
+ export async function loadResourceRouteManifest(distDir, fallbackBasePath = '/') {
36
+ const manifestPath = join(distDir, 'assets', 'resource-manifest.json');
37
+ try {
38
+ const parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
39
+ return {
40
+ basePath: typeof parsed?.base_path === 'string' ? parsed.base_path : fallbackBasePath,
41
+ routes: (Array.isArray(parsed?.routes) ? parsed.routes : [])
42
+ .map((entry) => sanitizeResourceRoute(entry))
43
+ .filter(Boolean)
44
+ .sort((left, right) => compareRouteSpecificity(left.path, right.path))
45
+ };
46
+ }
47
+ catch {
48
+ return {
49
+ basePath: fallbackBasePath,
50
+ routes: []
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,49 @@
1
+ export function buildResourceResponseDescriptor(result: any, basePath?: string, setCookies?: any[]): {
2
+ status: any;
3
+ headers: {
4
+ Location: string;
5
+ 'Cache-Control': string;
6
+ 'Content-Type'?: undefined;
7
+ 'Content-Disposition'?: undefined;
8
+ 'Content-Length'?: undefined;
9
+ Connection?: undefined;
10
+ };
11
+ body: string;
12
+ setCookies: any[];
13
+ } | {
14
+ status: any;
15
+ headers: {
16
+ 'Content-Type': string;
17
+ Location?: undefined;
18
+ 'Cache-Control'?: undefined;
19
+ 'Content-Disposition'?: undefined;
20
+ 'Content-Length'?: undefined;
21
+ Connection?: undefined;
22
+ };
23
+ body: any;
24
+ setCookies: any[];
25
+ } | {
26
+ status: number;
27
+ headers: {
28
+ 'Content-Type': any;
29
+ 'Content-Disposition': string;
30
+ 'Content-Length': string;
31
+ Location?: undefined;
32
+ 'Cache-Control'?: undefined;
33
+ Connection?: undefined;
34
+ };
35
+ body: Buffer<ArrayBuffer>;
36
+ setCookies: any[];
37
+ } | {
38
+ status: any;
39
+ headers: {
40
+ 'Content-Type': any;
41
+ 'Cache-Control': string;
42
+ Connection: string;
43
+ Location?: undefined;
44
+ 'Content-Disposition'?: undefined;
45
+ 'Content-Length'?: undefined;
46
+ };
47
+ body: any;
48
+ setCookies: any[];
49
+ };
@@ -0,0 +1,160 @@
1
+ import { appLocalRedirectLocation } from './base-path.js';
2
+ import { buildAttachmentContentDisposition, decodeDownloadResultBody } from './download-result.js';
3
+ import { clientFacingRouteMessage, defaultRouteDenyMessage } from './server-error.js';
4
+ function serializeJsonBody(payload) {
5
+ return JSON.stringify(payload);
6
+ }
7
+ function createReadableStreamFromAsyncIterable(iterable) {
8
+ const iterator = iterable[Symbol.asyncIterator]();
9
+ return new ReadableStream({
10
+ async pull(controller) {
11
+ try {
12
+ const { value, done } = await iterator.next();
13
+ if (done) {
14
+ controller.close();
15
+ }
16
+ else {
17
+ controller.enqueue(typeof value === 'string' ? new TextEncoder().encode(value) : value);
18
+ }
19
+ }
20
+ catch (err) {
21
+ controller.error(err);
22
+ }
23
+ },
24
+ async cancel() {
25
+ if (typeof iterator.return === 'function') {
26
+ await iterator.return();
27
+ }
28
+ }
29
+ });
30
+ }
31
+ function createSseStream(events) {
32
+ const iterator = events[Symbol.asyncIterator]();
33
+ const encoder = new TextEncoder();
34
+ return new ReadableStream({
35
+ async pull(controller) {
36
+ try {
37
+ const { value, done } = await iterator.next();
38
+ if (done) {
39
+ controller.close();
40
+ return;
41
+ }
42
+ let chunk = '';
43
+ if (value.event)
44
+ chunk += `event: ${value.event}\n`;
45
+ if (value.id)
46
+ chunk += `id: ${value.id}\n`;
47
+ if (value.retry)
48
+ chunk += `retry: ${value.retry}\n`;
49
+ const data = typeof value.data === 'string' ? value.data : JSON.stringify(value.data);
50
+ const lines = data.split('\n');
51
+ for (const line of lines) {
52
+ chunk += `data: ${line}\n`;
53
+ }
54
+ chunk += '\n';
55
+ controller.enqueue(encoder.encode(chunk));
56
+ }
57
+ catch (err) {
58
+ controller.error(err);
59
+ }
60
+ },
61
+ async cancel() {
62
+ if (typeof iterator.return === 'function') {
63
+ await iterator.return();
64
+ }
65
+ }
66
+ });
67
+ }
68
+ export function buildResourceResponseDescriptor(result, basePath = '/', setCookies = []) {
69
+ if (!result || typeof result !== 'object') {
70
+ return {
71
+ status: 500,
72
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
73
+ body: defaultRouteDenyMessage(500),
74
+ setCookies
75
+ };
76
+ }
77
+ if (result.kind === 'redirect') {
78
+ return {
79
+ status: Number.isInteger(result.status) ? result.status : 302,
80
+ headers: {
81
+ Location: appLocalRedirectLocation(result.location, basePath),
82
+ 'Cache-Control': 'no-store'
83
+ },
84
+ body: '',
85
+ setCookies
86
+ };
87
+ }
88
+ if (result.kind === 'deny') {
89
+ const status = Number.isInteger(result.status) ? result.status : 403;
90
+ return {
91
+ status,
92
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
93
+ body: clientFacingRouteMessage(status, result.message),
94
+ setCookies
95
+ };
96
+ }
97
+ if (result.kind === 'json' || result.kind === 'invalid') {
98
+ const status = Number.isInteger(result.status) ? result.status : (result.kind === 'invalid' ? 400 : 200);
99
+ return {
100
+ status,
101
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
102
+ body: serializeJsonBody(result.data),
103
+ setCookies
104
+ };
105
+ }
106
+ if (result.kind === 'text') {
107
+ return {
108
+ status: Number.isInteger(result.status) ? result.status : 200,
109
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
110
+ body: result.body,
111
+ setCookies
112
+ };
113
+ }
114
+ if (result.kind === 'download') {
115
+ const body = decodeDownloadResultBody(result, 'resource download response');
116
+ return {
117
+ status: 200,
118
+ headers: {
119
+ 'Content-Type': result.contentType,
120
+ 'Content-Disposition': buildAttachmentContentDisposition(result.filename),
121
+ 'Content-Length': String(body.byteLength)
122
+ },
123
+ body,
124
+ setCookies
125
+ };
126
+ }
127
+ if (result.kind === 'stream') {
128
+ const body = typeof result.body?.getReader === 'function'
129
+ ? result.body
130
+ : createReadableStreamFromAsyncIterable(result.body);
131
+ return {
132
+ status: Number.isInteger(result.status) ? result.status : 200,
133
+ headers: {
134
+ 'Content-Type': result.contentType || 'application/octet-stream',
135
+ 'Cache-Control': 'no-cache',
136
+ 'Connection': 'keep-alive'
137
+ },
138
+ body,
139
+ setCookies
140
+ };
141
+ }
142
+ if (result.kind === 'sse') {
143
+ return {
144
+ status: 200,
145
+ headers: {
146
+ 'Content-Type': 'text/event-stream; charset=utf-8',
147
+ 'Cache-Control': 'no-cache',
148
+ 'Connection': 'keep-alive'
149
+ },
150
+ body: createSseStream(result.events),
151
+ setCookies
152
+ };
153
+ }
154
+ return {
155
+ status: 500,
156
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
157
+ body: defaultRouteDenyMessage(500),
158
+ setCookies
159
+ };
160
+ }
@@ -0,0 +1,15 @@
1
+ export function isResourceRouteFile(fileName: any): boolean;
2
+ export function resourceRouteFileToRoute(filePath: any, root: any): string;
3
+ export function analyzeResourceRouteModule(fullPath: any, root: any): {
4
+ path: string;
5
+ file: string;
6
+ path_kind: string;
7
+ render_mode: string;
8
+ route_kind: string;
9
+ params: string[];
10
+ server_script: string;
11
+ server_script_path: any;
12
+ has_guard: boolean;
13
+ has_load: boolean;
14
+ has_action: boolean;
15
+ };
@@ -0,0 +1,129 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { relative, sep } from 'node:path';
3
+ const RESOURCE_EXTENSIONS = ['.resource.ts', '.resource.js', '.resource.mts', '.resource.cts', '.resource.mjs', '.resource.cjs'];
4
+ const FORBIDDEN_RESOURCE_EXPORT_RE = /\bexport\s+const\s+(?:data|prerender|exportPaths|ssr_data|props|ssr)\b/;
5
+ function readSingleExport(source, name) {
6
+ const fnMatch = source.match(new RegExp(`\\bexport\\s+(?:async\\s+)?function\\s+${name}\\s*\\(([^)]*)\\)`));
7
+ const constParenMatch = source.match(new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*(?:async\\s*)?\\(([^)]*)\\)\\s*=>`));
8
+ const constSingleArgMatch = source.match(new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*(?:async\\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=>`));
9
+ const matchCount = Number(Boolean(fnMatch)) +
10
+ Number(Boolean(constParenMatch)) +
11
+ Number(Boolean(constSingleArgMatch));
12
+ return {
13
+ fnMatch,
14
+ constParenMatch,
15
+ constSingleArgMatch,
16
+ hasExport: matchCount > 0,
17
+ matchCount
18
+ };
19
+ }
20
+ function assertSingleCtxArg(sourceFile, name, exportMatch) {
21
+ const singleArg = String(exportMatch.constSingleArgMatch?.[1] || '').trim();
22
+ const paramsText = String((exportMatch.fnMatch || exportMatch.constParenMatch)?.[1] || '').trim();
23
+ const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
24
+ if (arity !== 1) {
25
+ throw new Error(`Zenith resource route contract violation:\n` +
26
+ ` File: ${sourceFile}\n` +
27
+ ` Reason: ${name}(ctx) must accept exactly one argument\n` +
28
+ ` Example: export async function ${name}(ctx) { ... }`);
29
+ }
30
+ }
31
+ function segmentsToRoute(segments) {
32
+ const routeSegments = segments.map((seg) => {
33
+ const optionalCatchAllMatch = seg.match(/^\[\[\.\.\.([a-zA-Z_][a-zA-Z0-9_]*)\]\]$/);
34
+ if (optionalCatchAllMatch) {
35
+ return `*${optionalCatchAllMatch[1]}?`;
36
+ }
37
+ const catchAllMatch = seg.match(/^\[\.\.\.([a-zA-Z_][a-zA-Z0-9_]*)\]$/);
38
+ if (catchAllMatch) {
39
+ return `*${catchAllMatch[1]}`;
40
+ }
41
+ const paramMatch = seg.match(/^\[([a-zA-Z_][a-zA-Z0-9_]*)\]$/);
42
+ if (paramMatch) {
43
+ return `:${paramMatch[1]}`;
44
+ }
45
+ return seg;
46
+ });
47
+ if (routeSegments.length > 0) {
48
+ const last = routeSegments[routeSegments.length - 1];
49
+ if (last === 'index' || last === 'page') {
50
+ routeSegments.pop();
51
+ }
52
+ }
53
+ return `/${routeSegments.join('/')}`;
54
+ }
55
+ export function isResourceRouteFile(fileName) {
56
+ return RESOURCE_EXTENSIONS.some((extension) => fileName.endsWith(extension));
57
+ }
58
+ export function resourceRouteFileToRoute(filePath, root) {
59
+ const rel = relative(root, filePath);
60
+ const extension = RESOURCE_EXTENSIONS.find((candidate) => rel.endsWith(candidate));
61
+ if (!extension) {
62
+ throw new Error(`[Zenith CLI] Resource route "${filePath}" does not use a supported .resource.* extension.`);
63
+ }
64
+ const withoutExt = rel.slice(0, -extension.length);
65
+ const segments = withoutExt.split(sep).filter(Boolean);
66
+ const route = segmentsToRoute(segments);
67
+ return route === '/' ? '/' : route.replace(/\/+/g, '/');
68
+ }
69
+ export function analyzeResourceRouteModule(fullPath, root) {
70
+ const source = readFileSync(fullPath, 'utf8').trim();
71
+ if (!source) {
72
+ throw new Error(`Zenith resource route contract violation:\n` +
73
+ ` File: ${fullPath}\n` +
74
+ ` Reason: resource route module is empty\n` +
75
+ ` Example: export async function load(ctx) { return ctx.json({ ok: true }); }`);
76
+ }
77
+ if (FORBIDDEN_RESOURCE_EXPORT_RE.test(source)) {
78
+ throw new Error(`Zenith resource route contract violation:\n` +
79
+ ` File: ${fullPath}\n` +
80
+ ` Reason: resource routes may only export guard(ctx), load(ctx), and action(ctx)\n` +
81
+ ` Example: remove page-only exports such as data/prerender/exportPaths`);
82
+ }
83
+ const guardExport = readSingleExport(source, 'guard');
84
+ const loadExport = readSingleExport(source, 'load');
85
+ const actionExport = readSingleExport(source, 'action');
86
+ for (const [name, exportMatch] of [
87
+ ['guard', guardExport],
88
+ ['load', loadExport],
89
+ ['action', actionExport]
90
+ ]) {
91
+ if (exportMatch.matchCount > 1) {
92
+ throw new Error(`Zenith resource route contract violation:\n` +
93
+ ` File: ${fullPath}\n` +
94
+ ` Reason: multiple ${name} exports detected\n` +
95
+ ` Example: keep exactly one export for ${name}(ctx)`);
96
+ }
97
+ if (exportMatch.hasExport) {
98
+ assertSingleCtxArg(fullPath, name, exportMatch);
99
+ }
100
+ }
101
+ if (!loadExport.hasExport && !actionExport.hasExport) {
102
+ throw new Error(`Zenith resource route contract violation:\n` +
103
+ ` File: ${fullPath}\n` +
104
+ ` Reason: resource routes must export load(ctx), action(ctx), or both\n` +
105
+ ` Example: export async function load(ctx) { return ctx.text('ok'); }`);
106
+ }
107
+ return {
108
+ path: resourceRouteFileToRoute(fullPath, root),
109
+ file: relative(root, fullPath).replaceAll('\\', '/'),
110
+ path_kind: resourceRouteFileToRoute(fullPath, root).split('/').some((segment) => segment.startsWith(':') || segment.startsWith('*'))
111
+ ? 'dynamic'
112
+ : 'static',
113
+ render_mode: 'server',
114
+ route_kind: 'resource',
115
+ params: resourceRouteFileToRoute(fullPath, root)
116
+ .split('/')
117
+ .filter(Boolean)
118
+ .filter((segment) => segment.startsWith(':') || segment.startsWith('*'))
119
+ .map((segment) => {
120
+ const raw = segment.slice(1);
121
+ return raw.endsWith('?') ? raw.slice(0, -1) : raw;
122
+ }),
123
+ server_script: source,
124
+ server_script_path: fullPath,
125
+ has_guard: guardExport.hasExport,
126
+ has_load: loadExport.hasExport,
127
+ has_action: actionExport.hasExport
128
+ };
129
+ }
@@ -1,4 +1,4 @@
1
- const ROUTE_CHECK_UNSUPPORTED_TARGETS = new Set(['vercel', 'netlify']);
1
+ const ROUTE_CHECK_UNSUPPORTED_TARGETS = new Set(['vercel', 'netlify', 'static-export']);
2
2
  export function supportsTargetRouteCheck(target) {
3
3
  return !ROUTE_CHECK_UNSUPPORTED_TARGETS.has(String(target || '').trim());
4
4
  }
@@ -0,0 +1,5 @@
1
+ export const ALLOWED_KEYS: Set<string>;
2
+ export const RESOURCE_ALLOWED_KEYS: Set<string>;
3
+ export const ROUTE_RESULT_KINDS: Set<string>;
4
+ export const AUTH_CONTROL_FLOW_FLAG: "__zenith_auth_control_flow";
5
+ export const STAGED_SET_COOKIES_KEY: "__zenith_staged_set_cookies";
@@ -0,0 +1,5 @@
1
+ export const ALLOWED_KEYS = new Set(['data', 'load', 'guard', 'action', 'prerender', 'exportPaths', 'ssr_data', 'props', 'ssr']);
2
+ export const RESOURCE_ALLOWED_KEYS = new Set(['load', 'guard', 'action']);
3
+ export const ROUTE_RESULT_KINDS = new Set(['allow', 'redirect', 'deny', 'data', 'invalid', 'json', 'text', 'download', 'stream', 'sse']);
4
+ export const AUTH_CONTROL_FLOW_FLAG = '__zenith_auth_control_flow';
5
+ export const STAGED_SET_COOKIES_KEY = '__zenith_staged_set_cookies';
@@ -0,0 +1,5 @@
1
+ export function validateServerExports({ exports, filePath, routeKind }: {
2
+ exports: any;
3
+ filePath: any;
4
+ routeKind?: string | undefined;
5
+ }): void;
@@ -0,0 +1,59 @@
1
+ import { ALLOWED_KEYS, RESOURCE_ALLOWED_KEYS } from './constants.js';
2
+ function assertOneArgRouteFunction({ filePath, exportName, value }) {
3
+ if (typeof value !== 'function') {
4
+ throw new Error(`[Zenith] ${filePath}: "${exportName}" must be a function.`);
5
+ }
6
+ if (value.length !== 1) {
7
+ throw new Error(`[Zenith] ${filePath}: "${exportName}(ctx)" must take exactly 1 argument.`);
8
+ }
9
+ const fnStr = value.toString();
10
+ const paramsMatch = fnStr.match(/^[^{=]+\(([^)]*)\)/);
11
+ if (paramsMatch && paramsMatch[1].includes('...')) {
12
+ throw new Error(`[Zenith] ${filePath}: "${exportName}(ctx)" must not contain rest parameters.`);
13
+ }
14
+ }
15
+ export function validateServerExports({ exports, filePath, routeKind = 'page' }) {
16
+ const exportKeys = Object.keys(exports);
17
+ const allowedKeys = routeKind === 'resource' ? RESOURCE_ALLOWED_KEYS : ALLOWED_KEYS;
18
+ const illegalKeys = exportKeys.filter((key) => !allowedKeys.has(key));
19
+ if (illegalKeys.length > 0) {
20
+ throw new Error(`[Zenith] ${filePath}: illegal export(s): ${illegalKeys.join(', ')}`);
21
+ }
22
+ const hasData = 'data' in exports;
23
+ const hasLoad = 'load' in exports;
24
+ const hasGuard = 'guard' in exports;
25
+ const hasAction = 'action' in exports;
26
+ const hasNew = hasData || hasLoad || hasAction;
27
+ const hasLegacy = ('ssr_data' in exports) || ('props' in exports) || ('ssr' in exports);
28
+ if (routeKind === 'resource') {
29
+ if (hasData) {
30
+ throw new Error(`[Zenith] ${filePath}: resource routes may not export "data". Use load(ctx) or action(ctx) with ctx.json()/ctx.text().`);
31
+ }
32
+ if (!hasLoad && !hasAction) {
33
+ throw new Error(`[Zenith] ${filePath}: resource routes must export load(ctx), action(ctx), or both.`);
34
+ }
35
+ }
36
+ if (hasData && hasLoad) {
37
+ throw new Error(`[Zenith] ${filePath}: cannot export both "data" and "load". Choose one.`);
38
+ }
39
+ if (routeKind === 'page' && hasNew && hasLegacy) {
40
+ throw new Error(`[Zenith] ${filePath}: cannot mix new ("data"/"load") with legacy ("ssr_data"/"props"/"ssr") exports.`);
41
+ }
42
+ if (routeKind === 'page' && 'prerender' in exports && typeof exports.prerender !== 'boolean') {
43
+ throw new Error(`[Zenith] ${filePath}: "prerender" must be a boolean.`);
44
+ }
45
+ if (routeKind === 'page' && 'exportPaths' in exports) {
46
+ if (!Array.isArray(exports.exportPaths) || exports.exportPaths.some((value) => typeof value !== 'string')) {
47
+ throw new Error(`[Zenith] ${filePath}: "exportPaths" must be an array of string pathnames.`);
48
+ }
49
+ }
50
+ if (hasLoad) {
51
+ assertOneArgRouteFunction({ filePath, exportName: 'load', value: exports.load });
52
+ }
53
+ if (hasGuard) {
54
+ assertOneArgRouteFunction({ filePath, exportName: 'guard', value: exports.guard });
55
+ }
56
+ if (hasAction) {
57
+ assertOneArgRouteFunction({ filePath, exportName: 'action', value: exports.action });
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export function assertJsonSerializable(value: any, where?: string): void;
@@ -0,0 +1,52 @@
1
+ export function assertJsonSerializable(value, where = 'payload') {
2
+ const seen = new Set();
3
+ function walk(v, path) {
4
+ const t = typeof v;
5
+ if (v === null)
6
+ return;
7
+ if (t === 'string' || t === 'number' || t === 'boolean')
8
+ return;
9
+ if (t === 'bigint' || t === 'function' || t === 'symbol') {
10
+ throw new Error(`[Zenith] ${where}: non-serializable ${t} at ${path}`);
11
+ }
12
+ if (t === 'undefined') {
13
+ throw new Error(`[Zenith] ${where}: undefined is not allowed at ${path}`);
14
+ }
15
+ if (v instanceof Date) {
16
+ throw new Error(`[Zenith] ${where}: Date is not allowed at ${path} (convert to ISO string)`);
17
+ }
18
+ if (v instanceof Map || v instanceof Set) {
19
+ throw new Error(`[Zenith] ${where}: Map/Set not allowed at ${path}`);
20
+ }
21
+ if (t === 'object') {
22
+ if (seen.has(v))
23
+ throw new Error(`[Zenith] ${where}: circular reference at ${path}`);
24
+ seen.add(v);
25
+ if (Array.isArray(v)) {
26
+ if (path === '$') {
27
+ throw new Error(`[Zenith] ${where}: top-level payload must be a plain object, not an array at ${path}`);
28
+ }
29
+ for (let i = 0; i < v.length; i += 1) {
30
+ walk(v[i], `${path}[${i}]`);
31
+ }
32
+ return;
33
+ }
34
+ const proto = Object.getPrototypeOf(v);
35
+ const isPlainObject = proto === null ||
36
+ proto === Object.prototype ||
37
+ (proto && proto.constructor && proto.constructor.name === 'Object');
38
+ if (!isPlainObject) {
39
+ throw new Error(`[Zenith] ${where}: non-plain object at ${path}`);
40
+ }
41
+ for (const key of Object.keys(v)) {
42
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
43
+ throw new Error(`[Zenith] ${where}: forbidden prototype pollution key "${key}" at ${path}.${key}`);
44
+ }
45
+ walk(v[key], `${path}.${key}`);
46
+ }
47
+ return;
48
+ }
49
+ throw new Error(`[Zenith] ${where}: unsupported type at ${path}`);
50
+ }
51
+ walk(value, '$');
52
+ }
@@ -0,0 +1,15 @@
1
+ export function resolveRouteResult({ exports, ctx, filePath, guardOnly, routeKind }: {
2
+ exports: any;
3
+ ctx: any;
4
+ filePath: any;
5
+ guardOnly?: boolean | undefined;
6
+ routeKind?: string | undefined;
7
+ }): Promise<{
8
+ result: any;
9
+ trace: any;
10
+ }>;
11
+ export function resolveServerPayload({ exports, ctx, filePath }: {
12
+ exports: any;
13
+ ctx: any;
14
+ filePath: any;
15
+ }): Promise<any>;