@zenithbuild/cli 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +18 -13
  2. package/dist/adapters/adapter-netlify.d.ts +1 -1
  3. package/dist/adapters/adapter-netlify.js +56 -13
  4. package/dist/adapters/adapter-node.js +8 -0
  5. package/dist/adapters/adapter-static-export.d.ts +5 -0
  6. package/dist/adapters/adapter-static-export.js +115 -0
  7. package/dist/adapters/adapter-types.d.ts +3 -1
  8. package/dist/adapters/adapter-types.js +5 -2
  9. package/dist/adapters/adapter-vercel.d.ts +1 -1
  10. package/dist/adapters/adapter-vercel.js +70 -13
  11. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  12. package/dist/adapters/copy-hosted-page-runtime.js +49 -0
  13. package/dist/adapters/resolve-adapter.js +4 -0
  14. package/dist/adapters/route-rules.d.ts +5 -0
  15. package/dist/adapters/route-rules.js +9 -0
  16. package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
  17. package/dist/adapters/validate-hosted-resource-routes.js +13 -0
  18. package/dist/auth/route-auth.d.ts +6 -0
  19. package/dist/auth/route-auth.js +236 -0
  20. package/dist/build/compiler-runtime.d.ts +10 -9
  21. package/dist/build/compiler-runtime.js +58 -2
  22. package/dist/build/compiler-signal-expression.d.ts +1 -0
  23. package/dist/build/compiler-signal-expression.js +155 -0
  24. package/dist/build/expression-rewrites.d.ts +1 -6
  25. package/dist/build/expression-rewrites.js +61 -65
  26. package/dist/build/page-component-loop.d.ts +3 -13
  27. package/dist/build/page-component-loop.js +21 -46
  28. package/dist/build/page-ir-normalization.d.ts +0 -8
  29. package/dist/build/page-ir-normalization.js +13 -234
  30. package/dist/build/page-loop-state.d.ts +6 -9
  31. package/dist/build/page-loop-state.js +9 -8
  32. package/dist/build/page-loop.js +27 -22
  33. package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
  34. package/dist/build/scoped-identifier-rewrite.js +28 -128
  35. package/dist/build/server-script.d.ts +3 -1
  36. package/dist/build/server-script.js +35 -5
  37. package/dist/build-output-manifest.d.ts +3 -2
  38. package/dist/build-output-manifest.js +3 -0
  39. package/dist/build.js +32 -18
  40. package/dist/component-instance-ir.js +158 -52
  41. package/dist/dev-build-session.js +20 -6
  42. package/dist/dev-server.js +152 -55
  43. package/dist/download-result.d.ts +14 -0
  44. package/dist/download-result.js +148 -0
  45. package/dist/framework-components/Image.zen +1 -1
  46. package/dist/images/materialization-plan.d.ts +1 -0
  47. package/dist/images/materialization-plan.js +6 -0
  48. package/dist/images/materialize.d.ts +5 -3
  49. package/dist/images/materialize.js +24 -109
  50. package/dist/images/router-manifest.d.ts +1 -0
  51. package/dist/images/router-manifest.js +49 -0
  52. package/dist/images/service.d.ts +13 -1
  53. package/dist/images/service.js +45 -15
  54. package/dist/index.js +8 -2
  55. package/dist/manifest.d.ts +15 -1
  56. package/dist/manifest.js +27 -7
  57. package/dist/preview.d.ts +13 -4
  58. package/dist/preview.js +261 -101
  59. package/dist/request-body.d.ts +1 -0
  60. package/dist/request-body.js +7 -0
  61. package/dist/request-origin.d.ts +2 -0
  62. package/dist/request-origin.js +45 -0
  63. package/dist/resource-manifest.d.ts +16 -0
  64. package/dist/resource-manifest.js +53 -0
  65. package/dist/resource-response.d.ts +34 -0
  66. package/dist/resource-response.js +71 -0
  67. package/dist/resource-route-module.d.ts +15 -0
  68. package/dist/resource-route-module.js +129 -0
  69. package/dist/route-check-support.d.ts +1 -0
  70. package/dist/route-check-support.js +4 -0
  71. package/dist/server-contract.d.ts +29 -6
  72. package/dist/server-contract.js +304 -42
  73. package/dist/server-error.d.ts +4 -0
  74. package/dist/server-error.js +36 -0
  75. package/dist/server-output.d.ts +4 -1
  76. package/dist/server-output.js +71 -10
  77. package/dist/server-runtime/node-server.js +67 -31
  78. package/dist/server-runtime/route-render.d.ts +27 -3
  79. package/dist/server-runtime/route-render.js +94 -53
  80. package/dist/server-script-composition.d.ts +13 -5
  81. package/dist/server-script-composition.js +29 -11
  82. package/dist/static-export-paths.d.ts +3 -0
  83. package/dist/static-export-paths.js +160 -0
  84. package/package.json +6 -3
@@ -1,9 +1,12 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { pathToFileURL } from 'node:url';
3
+ import { attachRouteAuth } from '../auth/route-auth.js';
3
4
  import { appLocalRedirectLocation, normalizeBasePath, prependBasePath } from '../base-path.js';
4
5
  import { createImageRuntimePayload, injectImageRuntimePayload } from '../images/payload.js';
5
6
  import { materializeImageMarkup } from '../images/materialize.js';
6
- import { allow, data, deny, redirect, resolveRouteResult } from '../server-contract.js';
7
+ import { buildResourceResponseDescriptor } from '../resource-response.js';
8
+ import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException } from '../server-error.js';
9
+ import { allow, data, deny, download, invalid, json, redirect, resolveRouteResult, text } from '../server-contract.js';
7
10
  const MODULE_CACHE = new Map();
8
11
  const INTERNAL_QUERY_PREFIX = '__zenith_param_';
9
12
  function parseCookies(rawCookieHeader) {
@@ -40,24 +43,29 @@ function escapeInlineJson(payload) {
40
43
  .replace(/\u2028/g, '\\u2028')
41
44
  .replace(/\u2029/g, '\\u2029');
42
45
  }
43
- function defaultRouteDenyMessage(status) {
44
- if (status === 401) {
45
- return 'Unauthorized';
46
+ function appendSetCookieHeaders(headers, setCookies = []) {
47
+ for (const value of Array.isArray(setCookies) ? setCookies : []) {
48
+ headers.append('Set-Cookie', value);
46
49
  }
47
- if (status === 403) {
48
- return 'Forbidden';
49
- }
50
- if (status === 404) {
51
- return 'Not Found';
52
- }
53
- return 'Internal Server Error';
50
+ return headers;
54
51
  }
55
- function createTextResponse(status, message) {
52
+ function createTextResponse(status, message, setCookies = []) {
53
+ const headers = new Headers({
54
+ 'Content-Type': 'text/plain; charset=utf-8'
55
+ });
56
+ appendSetCookieHeaders(headers, setCookies);
56
57
  return new Response(message || defaultRouteDenyMessage(status), {
57
58
  status,
58
- headers: {
59
- 'Content-Type': 'text/plain; charset=utf-8'
60
- }
59
+ headers
60
+ });
61
+ }
62
+ function createResourceResponse(result, basePath, setCookies = []) {
63
+ const descriptor = buildResourceResponseDescriptor(result, basePath, setCookies);
64
+ const headers = new Headers(descriptor.headers);
65
+ appendSetCookieHeaders(headers, descriptor.setCookies);
66
+ return new Response(descriptor.body, {
67
+ status: descriptor.status,
68
+ headers
61
69
  });
62
70
  }
63
71
  function injectSsrPayload(html, payload) {
@@ -151,9 +159,9 @@ async function loadRouteExports(routeModulePath) {
151
159
  MODULE_CACHE.set(cacheKey, value);
152
160
  return value;
153
161
  }
154
- function createRouteContext({ request, route, params, publicUrl }) {
162
+ function createRouteContext({ request, route, params, publicUrl, guardOnly = false }) {
155
163
  const requestHeaders = Object.fromEntries(request.headers.entries());
156
- return {
164
+ const ctx = {
157
165
  params: { ...params },
158
166
  url: publicUrl,
159
167
  headers: { ...requestHeaders },
@@ -166,19 +174,23 @@ function createRouteContext({ request, route, params, publicUrl }) {
166
174
  file: route.file || route.server_script_path || route.route_id || route.path
167
175
  },
168
176
  env: {},
169
- auth: {
170
- async getSession() {
171
- return null;
172
- },
173
- async requireSession() {
174
- throw redirect('/login', 302);
175
- }
176
- },
177
+ action: null,
177
178
  allow,
178
179
  redirect,
179
180
  deny,
180
- data
181
+ invalid,
182
+ data,
183
+ json,
184
+ text,
185
+ download
181
186
  };
187
+ attachRouteAuth(ctx, {
188
+ requestUrl: publicUrl,
189
+ guardOnly,
190
+ redirect,
191
+ deny
192
+ });
193
+ return ctx;
182
194
  }
183
195
  /**
184
196
  * @param {{
@@ -188,23 +200,26 @@ function createRouteContext({ request, route, params, publicUrl }) {
188
200
  * routeModulePath: string,
189
201
  * guardOnly?: boolean
190
202
  * }} options
191
- * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
203
+ * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
192
204
  */
193
205
  export async function executeRouteRequest(options) {
194
206
  const { request, route, params, routeModulePath, guardOnly = false } = options;
195
207
  const publicUrl = buildPublicUrl(request.url, route, params);
196
- const ctx = createRouteContext({ request, route, params, publicUrl });
208
+ const ctx = createRouteContext({ request, route, params, publicUrl, guardOnly });
197
209
  const exports = await loadRouteExports(routeModulePath);
198
210
  const resolved = await resolveRouteResult({
199
211
  exports,
200
212
  ctx,
201
213
  filePath: route.file || route.server_script_path || route.path,
202
- guardOnly
214
+ guardOnly,
215
+ routeKind: route.route_kind === 'resource' ? 'resource' : 'page'
203
216
  });
204
217
  return {
205
218
  publicUrl,
206
219
  result: resolved.result,
207
- trace: resolved.trace
220
+ trace: resolved.trace,
221
+ status: resolved.status,
222
+ setCookies: Array.isArray(resolved.setCookies) ? resolved.setCookies : []
208
223
  };
209
224
  }
210
225
  /**
@@ -214,33 +229,34 @@ export async function executeRouteRequest(options) {
214
229
  * params: Record<string, string>,
215
230
  * routeModulePath: string,
216
231
  * shellHtmlPath: string,
217
- * pageAssetPath?: string | null,
218
232
  * imageManifestPath?: string | null,
219
233
  * imageConfig?: Record<string, unknown>
220
234
  * }} options
221
235
  * @returns {Promise<Response>}
222
236
  */
223
237
  export async function renderRouteRequest(options) {
224
- const { request, route, params, routeModulePath, shellHtmlPath, pageAssetPath = null, imageManifestPath = null, imageConfig = {} } = options;
238
+ const { request, route, params, routeModulePath, shellHtmlPath, imageManifestPath = null, imageConfig = {} } = options;
225
239
  try {
226
- const { publicUrl, result } = await executeRouteRequest({
240
+ const { publicUrl, result, status, setCookies = [] } = await executeRouteRequest({
227
241
  request,
228
242
  route,
229
243
  params,
230
244
  routeModulePath
231
245
  });
232
246
  if (result.kind === 'redirect') {
247
+ const headers = new Headers({
248
+ Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
249
+ 'Cache-Control': 'no-store'
250
+ });
251
+ appendSetCookieHeaders(headers, setCookies);
233
252
  return new Response('', {
234
253
  status: Number.isInteger(result.status) ? result.status : 302,
235
- headers: {
236
- Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
237
- 'Cache-Control': 'no-store'
238
- }
254
+ headers
239
255
  });
240
256
  }
241
257
  if (result.kind === 'deny') {
242
258
  const status = Number.isInteger(result.status) ? result.status : 403;
243
- return createTextResponse(status, result.message || defaultRouteDenyMessage(status));
259
+ return createTextResponse(status, clientFacingRouteMessage(status, result.message), setCookies);
244
260
  }
245
261
  const ssrPayload = result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)
246
262
  ? result.data
@@ -248,26 +264,51 @@ export async function renderRouteRequest(options) {
248
264
  const localImages = await loadImageManifest(imageManifestPath);
249
265
  const imagePayload = createImageRuntimePayload(imageConfig, localImages, 'passthrough', route.base_path || '/');
250
266
  let html = await readFile(shellHtmlPath, 'utf8');
251
- if (pageAssetPath) {
252
- html = await materializeImageMarkup({
253
- html,
254
- pageAssetPath,
255
- payload: imagePayload,
256
- ssrData: ssrPayload,
257
- routePathname: publicUrl.pathname
258
- });
259
- }
267
+ html = await materializeImageMarkup({
268
+ html,
269
+ payload: imagePayload,
270
+ imageMaterialization: Array.isArray(route.image_materialization)
271
+ ? route.image_materialization
272
+ : []
273
+ });
260
274
  html = injectSsrPayload(html, ssrPayload);
261
275
  html = injectImageRuntimePayload(html, imagePayload);
276
+ const headers = new Headers({
277
+ 'Content-Type': 'text/html; charset=utf-8'
278
+ });
279
+ appendSetCookieHeaders(headers, setCookies);
262
280
  return new Response(html, {
263
- status: 200,
264
- headers: {
265
- 'Content-Type': 'text/html; charset=utf-8'
266
- }
281
+ status: Number.isInteger(status) ? status : 200,
282
+ headers
283
+ });
284
+ }
285
+ catch (error) {
286
+ logServerException('node route render failed', error);
287
+ return createTextResponse(500, defaultRouteDenyMessage(500));
288
+ }
289
+ }
290
+ /**
291
+ * @param {{
292
+ * request: Request,
293
+ * route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null, route_kind?: string | null, base_path?: string | null },
294
+ * params: Record<string, string>,
295
+ * routeModulePath: string
296
+ * }} options
297
+ * @returns {Promise<Response>}
298
+ */
299
+ export async function renderResourceRouteRequest(options) {
300
+ const { request, route, params, routeModulePath } = options;
301
+ try {
302
+ const { result, setCookies = [] } = await executeRouteRequest({
303
+ request,
304
+ route: { ...route, route_kind: 'resource' },
305
+ params,
306
+ routeModulePath
267
307
  });
308
+ return createResourceResponse(result, route.base_path || '/', setCookies);
268
309
  }
269
310
  catch (error) {
270
- const message = String(error);
271
- return createTextResponse(500, message || defaultRouteDenyMessage(500));
311
+ logServerException('node resource route render failed', error);
312
+ return createTextResponse(500, defaultRouteDenyMessage(500));
272
313
  }
273
314
  }
@@ -1,39 +1,47 @@
1
1
  /**
2
2
  * @param {{
3
3
  * sourceFile: string,
4
- * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null,
4
+ * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null,
5
5
  * adjacentGuardPath?: string | null,
6
- * adjacentLoadPath?: string | null
6
+ * adjacentLoadPath?: string | null,
7
+ * adjacentActionPath?: string | null
7
8
  * }} input
8
- * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null }}
9
+ * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
9
10
  */
10
- export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath }: {
11
+ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath, adjacentActionPath }: {
11
12
  sourceFile: string;
12
13
  inlineServerScript?: {
13
14
  source: string;
14
15
  prerender: boolean;
15
16
  has_guard: boolean;
16
17
  has_load: boolean;
18
+ has_action: boolean;
17
19
  source_path: string;
20
+ export_paths?: string[];
18
21
  } | null;
19
22
  adjacentGuardPath?: string | null;
20
23
  adjacentLoadPath?: string | null;
24
+ adjacentActionPath?: string | null;
21
25
  }): {
22
26
  serverScript: {
23
27
  source: string;
24
28
  prerender: boolean;
25
29
  has_guard: boolean;
26
30
  has_load: boolean;
31
+ has_action: boolean;
27
32
  source_path: string;
33
+ export_paths?: string[];
28
34
  } | null;
29
35
  guardPath: string | null;
30
36
  loadPath: string | null;
37
+ actionPath: string | null;
31
38
  };
32
39
  /**
33
40
  * @param {string} sourceFile
34
- * @returns {{ guardPath: string | null, loadPath: string | null }}
41
+ * @returns {{ guardPath: string | null, loadPath: string | null, actionPath: string | null }}
35
42
  */
36
43
  export function resolveAdjacentServerModules(sourceFile: string): {
37
44
  guardPath: string | null;
38
45
  loadPath: string | null;
46
+ actionPath: string | null;
39
47
  };
@@ -4,7 +4,7 @@ const DATA_EXPORT_RE = /\bexport\s+const\s+data\b/;
4
4
  const LEGACY_EXPORT_RE = /\bexport\s+const\s+(?:ssr_data|props|ssr)\b/;
5
5
  /**
6
6
  * @param {string} sourceFile
7
- * @param {'guard' | 'load'} kind
7
+ * @param {'guard' | 'load' | 'action'} kind
8
8
  * @returns {string[]}
9
9
  */
10
10
  function adjacentModuleCandidates(sourceFile, kind) {
@@ -22,7 +22,7 @@ function adjacentModuleCandidates(sourceFile, kind) {
22
22
  }
23
23
  /**
24
24
  * @param {string} sourceFile
25
- * @param {'guard' | 'load'} kind
25
+ * @param {'guard' | 'load' | 'action'} kind
26
26
  * @returns {string | null}
27
27
  */
28
28
  function resolveAdjacentModule(sourceFile, kind) {
@@ -65,16 +65,18 @@ function classifyInlineServerSource(source) {
65
65
  /**
66
66
  * @param {{
67
67
  * sourceFile: string,
68
- * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null,
68
+ * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null,
69
69
  * adjacentGuardPath?: string | null,
70
- * adjacentLoadPath?: string | null
70
+ * adjacentLoadPath?: string | null,
71
+ * adjacentActionPath?: string | null
71
72
  * }} input
72
- * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null }}
73
+ * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
73
74
  */
74
- export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = null, adjacentGuardPath = null, adjacentLoadPath = null }) {
75
+ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = null, adjacentGuardPath = null, adjacentLoadPath = null, adjacentActionPath = null }) {
75
76
  const inlineSource = String(inlineServerScript?.source || '').trim();
76
77
  const inlineHasGuard = inlineServerScript?.has_guard === true;
77
78
  const inlineHasLoad = inlineServerScript?.has_load === true;
79
+ const inlineHasAction = inlineServerScript?.has_action === true;
78
80
  const { hasData, hasLegacy } = classifyInlineServerSource(inlineSource);
79
81
  if (inlineHasGuard && adjacentGuardPath) {
80
82
  throw new Error(`Zenith server script contract violation:\n` +
@@ -88,6 +90,12 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
88
90
  ` Reason: load is defined both inline and in an adjacent module\n` +
89
91
  ` Example: keep load in either <script server> or ${basename(adjacentLoadPath)}, not both`);
90
92
  }
93
+ if (inlineHasAction && adjacentActionPath) {
94
+ throw new Error(`Zenith server script contract violation:\n` +
95
+ ` File: ${sourceFile}\n` +
96
+ ` Reason: action is defined both inline and in an adjacent module\n` +
97
+ ` Example: keep action in either <script server> or ${basename(adjacentActionPath)}, not both`);
98
+ }
91
99
  if (adjacentLoadPath && (hasData || hasLegacy)) {
92
100
  throw new Error(`Zenith server script contract violation:\n` +
93
101
  ` File: ${sourceFile}\n` +
@@ -101,12 +109,16 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
101
109
  if (adjacentLoadPath) {
102
110
  prologue.push(`export { load } from '${renderRelativeSpecifier(sourceFile, adjacentLoadPath)}';`);
103
111
  }
112
+ if (adjacentActionPath) {
113
+ prologue.push(`export { action } from '${renderRelativeSpecifier(sourceFile, adjacentActionPath)}';`);
114
+ }
104
115
  const mergedSource = [...prologue, inlineSource].filter(Boolean).join('\n');
105
116
  if (!mergedSource.trim()) {
106
117
  return {
107
118
  serverScript: null,
108
119
  guardPath: adjacentGuardPath,
109
- loadPath: adjacentLoadPath
120
+ loadPath: adjacentLoadPath,
121
+ actionPath: adjacentActionPath
110
122
  };
111
123
  }
112
124
  return {
@@ -115,19 +127,25 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
115
127
  prerender: inlineServerScript?.prerender === true,
116
128
  has_guard: inlineHasGuard || Boolean(adjacentGuardPath),
117
129
  has_load: inlineHasLoad || Boolean(adjacentLoadPath),
118
- source_path: sourceFile
130
+ has_action: inlineHasAction || Boolean(adjacentActionPath),
131
+ source_path: sourceFile,
132
+ export_paths: Array.isArray(inlineServerScript?.export_paths)
133
+ ? [...inlineServerScript.export_paths]
134
+ : []
119
135
  },
120
136
  guardPath: adjacentGuardPath,
121
- loadPath: adjacentLoadPath
137
+ loadPath: adjacentLoadPath,
138
+ actionPath: adjacentActionPath
122
139
  };
123
140
  }
124
141
  /**
125
142
  * @param {string} sourceFile
126
- * @returns {{ guardPath: string | null, loadPath: string | null }}
143
+ * @returns {{ guardPath: string | null, loadPath: string | null, actionPath: string | null }}
127
144
  */
128
145
  export function resolveAdjacentServerModules(sourceFile) {
129
146
  return {
130
147
  guardPath: resolveAdjacentModule(sourceFile, 'guard'),
131
- loadPath: resolveAdjacentModule(sourceFile, 'load')
148
+ loadPath: resolveAdjacentModule(sourceFile, 'load'),
149
+ actionPath: resolveAdjacentModule(sourceFile, 'action')
132
150
  };
133
151
  }
@@ -0,0 +1,3 @@
1
+ export function extractStaticExportPaths(source: any, sourceFile: any): string[] | null;
2
+ export function validateStaticExportPaths(routePath: any, exportPaths: any, sourceFile: any): string[];
3
+ export function toStaticHtmlFilePath(pathname: any): string;
@@ -0,0 +1,160 @@
1
+ import { resolveRequestRoute } from './server/resolve-request-route.js';
2
+ function skipWhitespace(source, start) {
3
+ let index = start;
4
+ while (index < source.length) {
5
+ const char = source[index];
6
+ if (/\s/.test(char)) {
7
+ index += 1;
8
+ continue;
9
+ }
10
+ if (source.startsWith('//', index)) {
11
+ const nextLine = source.indexOf('\n', index + 2);
12
+ return nextLine === -1 ? source.length : skipWhitespace(source, nextLine + 1);
13
+ }
14
+ if (source.startsWith('/*', index)) {
15
+ const close = source.indexOf('*/', index + 2);
16
+ if (close === -1) {
17
+ throw new Error('[Zenith] Unterminated block comment in exportPaths literal.');
18
+ }
19
+ index = close + 2;
20
+ continue;
21
+ }
22
+ break;
23
+ }
24
+ return index;
25
+ }
26
+ function parseQuotedStringLiteral(source, start, sourceFile) {
27
+ const quote = source[start];
28
+ if (quote !== '"' && quote !== '\'') {
29
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths must be a literal array of string paths.`);
30
+ }
31
+ let index = start + 1;
32
+ let value = '';
33
+ while (index < source.length) {
34
+ const char = source[index];
35
+ if (char === '\\') {
36
+ const next = source[index + 1];
37
+ if (next === undefined) {
38
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths contains an invalid escape sequence.`);
39
+ }
40
+ value += char + next;
41
+ index += 2;
42
+ continue;
43
+ }
44
+ if (char === quote) {
45
+ try {
46
+ return {
47
+ value: JSON.parse(`"${value.replace(/"/g, '\\"')}"`),
48
+ nextIndex: index + 1
49
+ };
50
+ }
51
+ catch {
52
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths contains an invalid string literal.`);
53
+ }
54
+ }
55
+ if (char === '\n' || char === '\r') {
56
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths string literals must stay on one line.`);
57
+ }
58
+ value += char;
59
+ index += 1;
60
+ }
61
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths contains an unterminated string literal.`);
62
+ }
63
+ function parseStringArrayLiteral(source, start, sourceFile) {
64
+ if (source[start] !== '[') {
65
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths must be assigned a literal array of string paths.`);
66
+ }
67
+ const values = [];
68
+ let index = start + 1;
69
+ while (index < source.length) {
70
+ index = skipWhitespace(source, index);
71
+ if (source[index] === ']') {
72
+ return { values, nextIndex: index + 1 };
73
+ }
74
+ const parsed = parseQuotedStringLiteral(source, index, sourceFile);
75
+ values.push(parsed.value);
76
+ index = skipWhitespace(source, parsed.nextIndex);
77
+ if (source[index] === ',') {
78
+ index += 1;
79
+ continue;
80
+ }
81
+ if (source[index] === ']') {
82
+ return { values, nextIndex: index + 1 };
83
+ }
84
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths must be a comma-delimited literal array of string paths.`);
85
+ }
86
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths array is missing a closing "]".`);
87
+ }
88
+ function normalizeConcretePath(value, sourceFile) {
89
+ if (typeof value !== 'string') {
90
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be strings.`);
91
+ }
92
+ const trimmed = value.trim();
93
+ if (!trimmed.startsWith('/')) {
94
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must start with "/".`);
95
+ }
96
+ if (trimmed.includes('://') || trimmed.startsWith('//')) {
97
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be same-origin pathnames.`);
98
+ }
99
+ if (trimmed.includes('?') || trimmed.includes('#') || /[\r\n]/.test(trimmed)) {
100
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be pathnames without query or hash.`);
101
+ }
102
+ const segments = trimmed
103
+ .split('/')
104
+ .filter(Boolean)
105
+ .map((segment) => segment.trim())
106
+ .filter((segment) => segment.length > 0);
107
+ for (const segment of segments) {
108
+ if (segment === '.' || segment === '..') {
109
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must not contain path traversal segments.`);
110
+ }
111
+ if (segment.startsWith(':') || segment.startsWith('*')) {
112
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entries must be concrete public URLs.`);
113
+ }
114
+ }
115
+ return segments.length === 0 ? '/' : `/${segments.join('/')}`;
116
+ }
117
+ export function extractStaticExportPaths(source, sourceFile) {
118
+ const match = /\bexport\s+const\s+exportPaths\b/.exec(String(source || ''));
119
+ if (!match) {
120
+ return null;
121
+ }
122
+ const equalsIndex = String(source || '').indexOf('=', match.index + match[0].length);
123
+ if (equalsIndex === -1) {
124
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths must use the form export const exportPaths = [...].`);
125
+ }
126
+ const valueStart = skipWhitespace(String(source || ''), equalsIndex + 1);
127
+ const { values } = parseStringArrayLiteral(String(source || ''), valueStart, sourceFile);
128
+ return values.map((value) => normalizeConcretePath(value, sourceFile));
129
+ }
130
+ export function validateStaticExportPaths(routePath, exportPaths, sourceFile) {
131
+ if (!Array.isArray(exportPaths)) {
132
+ return [];
133
+ }
134
+ const deduped = [];
135
+ const seen = new Set();
136
+ for (const rawPath of exportPaths) {
137
+ const concretePath = normalizeConcretePath(rawPath, sourceFile);
138
+ if (seen.has(concretePath)) {
139
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths contains a duplicate path "${concretePath}".`);
140
+ }
141
+ seen.add(concretePath);
142
+ const resolved = resolveRequestRoute(new URL(concretePath, 'http://localhost'), [{ path: routePath }]);
143
+ if (!resolved.matched || resolved.route?.path !== routePath) {
144
+ throw new Error(`[Zenith] ${sourceFile}: exportPaths entry "${concretePath}" does not match route "${routePath}".`);
145
+ }
146
+ deduped.push(concretePath);
147
+ }
148
+ return deduped;
149
+ }
150
+ export function toStaticHtmlFilePath(pathname) {
151
+ const normalized = normalizeConcretePath(pathname, 'static-export');
152
+ if (normalized === '/') {
153
+ return 'index.html';
154
+ }
155
+ const relativePath = normalized.replace(/^\//, '');
156
+ if (/\.[a-zA-Z0-9]+$/.test(relativePath)) {
157
+ return relativePath;
158
+ }
159
+ return `${relativePath}/index.html`;
160
+ }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
+ "bin": {
9
+ "zenith": "./dist/index.js"
10
+ },
8
11
  "exports": {
9
12
  ".": "./dist/index.js"
10
13
  },
@@ -35,8 +38,8 @@
35
38
  "prepublishOnly": "npm run build"
36
39
  },
37
40
  "dependencies": {
38
- "@zenithbuild/compiler": "0.7.3",
39
- "@zenithbuild/bundler": "0.7.3",
41
+ "@zenithbuild/compiler": "0.7.5",
42
+ "@zenithbuild/bundler": "0.7.5",
40
43
  "picocolors": "^1.1.1",
41
44
  "sharp": "^0.34.4"
42
45
  },