@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
@@ -7,7 +7,7 @@ import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCh
7
7
  import { handleImageRequest } from '../images/service.js';
8
8
  import { createTrustedOriginResolver } from '../request-origin.js';
9
9
  import { defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from '../server-error.js';
10
- import { executeRouteRequest, renderRouteRequest } from './route-render.js';
10
+ import { executeRouteRequest, renderResourceRouteRequest, renderRouteRequest } from './route-render.js';
11
11
  import { resolveRequestRoute } from './resolve-request-route.js';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
@@ -97,17 +97,53 @@ async function createWebRequest(req, url) {
97
97
  }
98
98
  return new Request(url.toString(), init);
99
99
  }
100
+ function getSetCookieValues(response) {
101
+ if (typeof response?.headers?.getSetCookie === 'function') {
102
+ return response.headers.getSetCookie();
103
+ }
104
+ const value = response?.headers?.get?.('set-cookie');
105
+ return typeof value === 'string' && value.length > 0 ? [value] : [];
106
+ }
100
107
  async function sendFetchResponse(res, response, method) {
101
108
  res.statusCode = response.status;
109
+ const setCookies = getSetCookieValues(response);
110
+ if (setCookies.length > 0) {
111
+ res.setHeader('set-cookie', setCookies);
112
+ }
102
113
  for (const [key, value] of response.headers.entries()) {
114
+ if (key.toLowerCase() === 'set-cookie') {
115
+ continue;
116
+ }
103
117
  res.setHeader(key, value);
104
118
  }
105
- if (String(method || 'GET').toUpperCase() === 'HEAD') {
119
+ if (String(method || 'GET').toUpperCase() === 'HEAD' || !response.body) {
106
120
  res.end();
107
121
  return;
108
122
  }
109
- const body = await response.arrayBuffer();
110
- res.end(Buffer.from(body));
123
+ try {
124
+ const bodyStream = Readable.fromWeb(response.body);
125
+ bodyStream.pipe(res);
126
+ bodyStream.on('error', (err) => {
127
+ logServerException('node response stream failed', err);
128
+ if (!res.headersSent) {
129
+ res.statusCode = 500;
130
+ res.end();
131
+ }
132
+ else {
133
+ res.destroy();
134
+ }
135
+ });
136
+ }
137
+ catch (err) {
138
+ logServerException('node response pipe creation failed', err);
139
+ if (!res.headersSent) {
140
+ res.statusCode = 500;
141
+ res.end();
142
+ }
143
+ else {
144
+ res.destroy();
145
+ }
146
+ }
111
147
  }
112
148
  async function sendStaticFile(res, filePath, method) {
113
149
  const body = await readFile(filePath);
@@ -155,13 +191,16 @@ async function loadRuntimeContext(options = {}) {
155
191
  base_path: '/'
156
192
  });
157
193
  const serverManifest = await readJson(join(serverDir, 'manifest.json'), { routes: [] });
194
+ const allServerRoutes = Array.isArray(serverManifest.routes) ? serverManifest.routes : [];
158
195
  return {
159
196
  distDir,
160
197
  serverDir,
161
198
  staticDir: resolve(serverDir, config.static_dir || '../static'),
162
199
  buildManifest,
163
200
  buildRoutes: Array.isArray(buildManifest.routes) ? buildManifest.routes : [],
164
- serverRoutes: Array.isArray(serverManifest.routes) ? serverManifest.routes : [],
201
+ serverRoutes: allServerRoutes,
202
+ pageServerRoutes: allServerRoutes.filter((route) => route?.route_kind !== 'resource'),
203
+ resourceServerRoutes: allServerRoutes.filter((route) => route?.route_kind === 'resource'),
165
204
  images: config.images || {},
166
205
  basePath: normalizeBasePath(config.base_path || '/')
167
206
  };
@@ -206,7 +245,7 @@ async function handleRouteCheck(req, res, url, context) {
206
245
  }
207
246
  let result = { kind: 'allow' };
208
247
  let routeId = buildResolved.route.path || '';
209
- const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.serverRoutes);
248
+ const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.pageServerRoutes);
210
249
  if (serverResolved.matched && serverResolved.route) {
211
250
  routeId = serverResolved.route.route_id || serverResolved.route.name || serverResolved.route.path || routeId;
212
251
  try {
@@ -275,7 +314,20 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
275
314
  await sendStaticFile(res, assetPath, req.method);
276
315
  return;
277
316
  }
278
- const serverResolved = resolveRequestRoute(canonicalUrl, context.serverRoutes);
317
+ const resourceResolved = resolveRequestRoute(canonicalUrl, context.resourceServerRoutes);
318
+ if (resourceResolved.matched && resourceResolved.route) {
319
+ const routeDir = join(context.serverDir, 'routes', resourceResolved.route.name);
320
+ const request = await createWebRequest(req, url);
321
+ const response = await renderResourceRouteRequest({
322
+ request,
323
+ route: resourceResolved.route,
324
+ params: resourceResolved.params,
325
+ routeModulePath: join(routeDir, 'route', 'entry.js')
326
+ });
327
+ await sendFetchResponse(res, response, req.method);
328
+ return;
329
+ }
330
+ const serverResolved = resolveRequestRoute(canonicalUrl, context.pageServerRoutes);
279
331
  if (serverResolved.matched && serverResolved.route) {
280
332
  const routeDir = join(context.serverDir, 'routes', serverResolved.route.name);
281
333
  const request = await createWebRequest(req, url);
@@ -8,7 +8,7 @@ export function extractInternalParams(requestUrl: any, route: any): {};
8
8
  * routeModulePath: string,
9
9
  * guardOnly?: boolean
10
10
  * }} options
11
- * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
11
+ * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
12
12
  */
13
13
  export function executeRouteRequest(options: {
14
14
  request: Request;
@@ -34,6 +34,7 @@ export function executeRouteRequest(options: {
34
34
  load: string;
35
35
  };
36
36
  status?: number;
37
+ setCookies?: string[];
37
38
  }>;
38
39
  /**
39
40
  * @param {{
@@ -62,3 +63,26 @@ export function renderRouteRequest(options: {
62
63
  imageManifestPath?: string | null;
63
64
  imageConfig?: Record<string, unknown>;
64
65
  }): Promise<Response>;
66
+ /**
67
+ * @param {{
68
+ * request: Request,
69
+ * 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 },
70
+ * params: Record<string, string>,
71
+ * routeModulePath: string
72
+ * }} options
73
+ * @returns {Promise<Response>}
74
+ */
75
+ export function renderResourceRouteRequest(options: {
76
+ request: Request;
77
+ route: {
78
+ path: string;
79
+ params?: string[];
80
+ route_id?: string | null;
81
+ server_script_path?: string | null;
82
+ file?: string | null;
83
+ route_kind?: string | null;
84
+ base_path?: string | null;
85
+ };
86
+ params: Record<string, string>;
87
+ routeModulePath: string;
88
+ }): Promise<Response>;
@@ -1,10 +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';
7
+ import { buildResourceResponseDescriptor } from '../resource-response.js';
6
8
  import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException } from '../server-error.js';
7
- import { allow, data, deny, invalid, redirect, resolveRouteResult } from '../server-contract.js';
9
+ import { allow, data, deny, download, invalid, json, redirect, resolveRouteResult, text } from '../server-contract.js';
8
10
  const MODULE_CACHE = new Map();
9
11
  const INTERNAL_QUERY_PREFIX = '__zenith_param_';
10
12
  function parseCookies(rawCookieHeader) {
@@ -41,12 +43,29 @@ function escapeInlineJson(payload) {
41
43
  .replace(/\u2028/g, '\\u2028')
42
44
  .replace(/\u2029/g, '\\u2029');
43
45
  }
44
- function createTextResponse(status, message) {
46
+ function appendSetCookieHeaders(headers, setCookies = []) {
47
+ for (const value of Array.isArray(setCookies) ? setCookies : []) {
48
+ headers.append('Set-Cookie', value);
49
+ }
50
+ return headers;
51
+ }
52
+ function createTextResponse(status, message, setCookies = []) {
53
+ const headers = new Headers({
54
+ 'Content-Type': 'text/plain; charset=utf-8'
55
+ });
56
+ appendSetCookieHeaders(headers, setCookies);
45
57
  return new Response(message || defaultRouteDenyMessage(status), {
46
58
  status,
47
- headers: {
48
- 'Content-Type': 'text/plain; charset=utf-8'
49
- }
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
50
69
  });
51
70
  }
52
71
  function injectSsrPayload(html, payload) {
@@ -140,9 +159,9 @@ async function loadRouteExports(routeModulePath) {
140
159
  MODULE_CACHE.set(cacheKey, value);
141
160
  return value;
142
161
  }
143
- function createRouteContext({ request, route, params, publicUrl }) {
162
+ function createRouteContext({ request, route, params, publicUrl, guardOnly = false }) {
144
163
  const requestHeaders = Object.fromEntries(request.headers.entries());
145
- return {
164
+ const ctx = {
146
165
  params: { ...params },
147
166
  url: publicUrl,
148
167
  headers: { ...requestHeaders },
@@ -156,20 +175,22 @@ function createRouteContext({ request, route, params, publicUrl }) {
156
175
  },
157
176
  env: {},
158
177
  action: null,
159
- auth: {
160
- async getSession() {
161
- return null;
162
- },
163
- async requireSession() {
164
- throw redirect('/login', 302);
165
- }
166
- },
167
178
  allow,
168
179
  redirect,
169
180
  deny,
170
181
  invalid,
171
- data
182
+ data,
183
+ json,
184
+ text,
185
+ download
172
186
  };
187
+ attachRouteAuth(ctx, {
188
+ requestUrl: publicUrl,
189
+ guardOnly,
190
+ redirect,
191
+ deny
192
+ });
193
+ return ctx;
173
194
  }
174
195
  /**
175
196
  * @param {{
@@ -179,24 +200,26 @@ function createRouteContext({ request, route, params, publicUrl }) {
179
200
  * routeModulePath: string,
180
201
  * guardOnly?: boolean
181
202
  * }} options
182
- * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
203
+ * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
183
204
  */
184
205
  export async function executeRouteRequest(options) {
185
206
  const { request, route, params, routeModulePath, guardOnly = false } = options;
186
207
  const publicUrl = buildPublicUrl(request.url, route, params);
187
- const ctx = createRouteContext({ request, route, params, publicUrl });
208
+ const ctx = createRouteContext({ request, route, params, publicUrl, guardOnly });
188
209
  const exports = await loadRouteExports(routeModulePath);
189
210
  const resolved = await resolveRouteResult({
190
211
  exports,
191
212
  ctx,
192
213
  filePath: route.file || route.server_script_path || route.path,
193
- guardOnly
214
+ guardOnly,
215
+ routeKind: route.route_kind === 'resource' ? 'resource' : 'page'
194
216
  });
195
217
  return {
196
218
  publicUrl,
197
219
  result: resolved.result,
198
220
  trace: resolved.trace,
199
- status: resolved.status
221
+ status: resolved.status,
222
+ setCookies: Array.isArray(resolved.setCookies) ? resolved.setCookies : []
200
223
  };
201
224
  }
202
225
  /**
@@ -214,24 +237,26 @@ export async function executeRouteRequest(options) {
214
237
  export async function renderRouteRequest(options) {
215
238
  const { request, route, params, routeModulePath, shellHtmlPath, imageManifestPath = null, imageConfig = {} } = options;
216
239
  try {
217
- const { publicUrl, result, status } = await executeRouteRequest({
240
+ const { publicUrl, result, status, setCookies = [] } = await executeRouteRequest({
218
241
  request,
219
242
  route,
220
243
  params,
221
244
  routeModulePath
222
245
  });
223
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);
224
252
  return new Response('', {
225
253
  status: Number.isInteger(result.status) ? result.status : 302,
226
- headers: {
227
- Location: appLocalRedirectLocation(result.location, route.base_path || '/'),
228
- 'Cache-Control': 'no-store'
229
- }
254
+ headers
230
255
  });
231
256
  }
232
257
  if (result.kind === 'deny') {
233
258
  const status = Number.isInteger(result.status) ? result.status : 403;
234
- return createTextResponse(status, clientFacingRouteMessage(status, result.message));
259
+ return createTextResponse(status, clientFacingRouteMessage(status, result.message), setCookies);
235
260
  }
236
261
  const ssrPayload = result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)
237
262
  ? result.data
@@ -248,11 +273,13 @@ export async function renderRouteRequest(options) {
248
273
  });
249
274
  html = injectSsrPayload(html, ssrPayload);
250
275
  html = injectImageRuntimePayload(html, imagePayload);
276
+ const headers = new Headers({
277
+ 'Content-Type': 'text/html; charset=utf-8'
278
+ });
279
+ appendSetCookieHeaders(headers, setCookies);
251
280
  return new Response(html, {
252
281
  status: Number.isInteger(status) ? status : 200,
253
- headers: {
254
- 'Content-Type': 'text/html; charset=utf-8'
255
- }
282
+ headers
256
283
  });
257
284
  }
258
285
  catch (error) {
@@ -260,3 +287,28 @@ export async function renderRouteRequest(options) {
260
287
  return createTextResponse(500, defaultRouteDenyMessage(500));
261
288
  }
262
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
307
+ });
308
+ return createResourceResponse(result, route.base_path || '/', setCookies);
309
+ }
310
+ catch (error) {
311
+ logServerException('node resource route render failed', error);
312
+ return createTextResponse(500, defaultRouteDenyMessage(500));
313
+ }
314
+ }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * @param {{
3
3
  * sourceFile: string,
4
- * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: 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
6
  * adjacentLoadPath?: string | null,
7
7
  * adjacentActionPath?: string | null
8
8
  * }} input
9
- * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
9
+ * @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 }}
10
10
  */
11
11
  export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, adjacentGuardPath, adjacentLoadPath, adjacentActionPath }: {
12
12
  sourceFile: string;
@@ -17,6 +17,7 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, ad
17
17
  has_load: boolean;
18
18
  has_action: boolean;
19
19
  source_path: string;
20
+ export_paths?: string[];
20
21
  } | null;
21
22
  adjacentGuardPath?: string | null;
22
23
  adjacentLoadPath?: string | null;
@@ -29,6 +30,7 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript, ad
29
30
  has_load: boolean;
30
31
  has_action: boolean;
31
32
  source_path: string;
33
+ export_paths?: string[];
32
34
  } | null;
33
35
  guardPath: string | null;
34
36
  loadPath: string | null;
@@ -65,12 +65,12 @@ function classifyInlineServerSource(source) {
65
65
  /**
66
66
  * @param {{
67
67
  * sourceFile: string,
68
- * inlineServerScript?: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: 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
70
  * adjacentLoadPath?: string | null,
71
71
  * adjacentActionPath?: string | null
72
72
  * }} input
73
- * @returns {{ serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null, guardPath: string | null, loadPath: string | null, actionPath: string | null }}
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 }}
74
74
  */
75
75
  export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = null, adjacentGuardPath = null, adjacentLoadPath = null, adjacentActionPath = null }) {
76
76
  const inlineSource = String(inlineServerScript?.source || '').trim();
@@ -128,7 +128,10 @@ export function composeServerScriptEnvelope({ sourceFile, inlineServerScript = n
128
128
  has_guard: inlineHasGuard || Boolean(adjacentGuardPath),
129
129
  has_load: inlineHasLoad || Boolean(adjacentLoadPath),
130
130
  has_action: inlineHasAction || Boolean(adjacentActionPath),
131
- source_path: sourceFile
131
+ source_path: sourceFile,
132
+ export_paths: Array.isArray(inlineServerScript?.export_paths)
133
+ ? [...inlineServerScript.export_paths]
134
+ : []
132
135
  },
133
136
  guardPath: adjacentGuardPath,
134
137
  loadPath: adjacentLoadPath,
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.7.4",
3
+ "version": "0.7.7",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,8 +38,8 @@
38
38
  "prepublishOnly": "npm run build"
39
39
  },
40
40
  "dependencies": {
41
- "@zenithbuild/compiler": "0.7.4",
42
- "@zenithbuild/bundler": "0.7.4",
41
+ "@zenithbuild/compiler": "0.7.7",
42
+ "@zenithbuild/bundler": "0.7.7",
43
43
  "picocolors": "^1.1.1",
44
44
  "sharp": "^0.34.4"
45
45
  },