@zenithbuild/cli 0.7.4 → 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 (58) 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 +56 -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 +70 -14
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +49 -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/page-loop-state.js +1 -1
  22. package/dist/build/server-script.d.ts +2 -1
  23. package/dist/build/server-script.js +7 -3
  24. package/dist/build-output-manifest.d.ts +3 -2
  25. package/dist/build-output-manifest.js +3 -0
  26. package/dist/build.js +29 -17
  27. package/dist/dev-server.js +79 -25
  28. package/dist/download-result.d.ts +14 -0
  29. package/dist/download-result.js +148 -0
  30. package/dist/images/service.d.ts +13 -1
  31. package/dist/images/service.js +45 -15
  32. package/dist/manifest.d.ts +15 -1
  33. package/dist/manifest.js +24 -5
  34. package/dist/preview.d.ts +11 -3
  35. package/dist/preview.js +188 -62
  36. package/dist/request-body.d.ts +0 -1
  37. package/dist/request-body.js +0 -6
  38. package/dist/resource-manifest.d.ts +16 -0
  39. package/dist/resource-manifest.js +53 -0
  40. package/dist/resource-response.d.ts +34 -0
  41. package/dist/resource-response.js +71 -0
  42. package/dist/resource-route-module.d.ts +15 -0
  43. package/dist/resource-route-module.js +129 -0
  44. package/dist/route-check-support.js +1 -1
  45. package/dist/server-contract.d.ts +24 -16
  46. package/dist/server-contract.js +217 -25
  47. package/dist/server-error.d.ts +1 -1
  48. package/dist/server-error.js +2 -0
  49. package/dist/server-output.d.ts +2 -1
  50. package/dist/server-output.js +59 -11
  51. package/dist/server-runtime/node-server.js +34 -4
  52. package/dist/server-runtime/route-render.d.ts +25 -1
  53. package/dist/server-runtime/route-render.js +81 -29
  54. package/dist/server-script-composition.d.ts +4 -2
  55. package/dist/server-script-composition.js +6 -3
  56. package/dist/static-export-paths.d.ts +3 -0
  57. package/dist/static-export-paths.js +160 -0
  58. package/package.json +3 -3
@@ -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,10 @@ 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('./auth/route-auth.js', import.meta.url),
20
+ to: 'auth/route-auth.js'
21
+ },
17
22
  {
18
23
  from: new URL('./base-path.js', import.meta.url),
19
24
  to: 'base-path.js'
@@ -34,11 +39,27 @@ const SERVER_RUNTIME_FILES = [
34
39
  from: new URL('./images/runtime.js', import.meta.url),
35
40
  to: 'images/runtime.js'
36
41
  },
42
+ {
43
+ from: new URL('./images/service.js', import.meta.url),
44
+ to: 'images/service.js'
45
+ },
37
46
  {
38
47
  from: new URL('./server-error.js', import.meta.url),
39
48
  to: 'server-error.js'
49
+ },
50
+ {
51
+ from: new URL('./resource-response.js', import.meta.url),
52
+ to: 'resource-response.js'
53
+ },
54
+ {
55
+ from: new URL('./download-result.js', import.meta.url),
56
+ to: 'download-result.js'
40
57
  }
41
58
  ];
59
+ const SPECIAL_SERVER_SPECIFIERS = new Map([
60
+ ['zenith:server-contract', 'server-contract.js'],
61
+ ['zenith:route-auth', 'auth/route-auth.js']
62
+ ]);
42
63
  function normalizeRouteName(routePath) {
43
64
  if (routePath === '/') {
44
65
  return 'index';
@@ -152,7 +173,7 @@ function outputPathForSource(projectRoot, modulesRoot, sourcePath) {
152
173
  : relativePath.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
153
174
  return join(modulesRoot, nextRelative);
154
175
  }
155
- async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts, seen }) {
176
+ async function compileImportedModule({ projectRoot, modulesRoot, serverDir, sourcePath, ts, seen }) {
156
177
  if (seen.has(sourcePath)) {
157
178
  return outputPathForSource(projectRoot, modulesRoot, sourcePath);
158
179
  }
@@ -166,6 +187,12 @@ async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts,
166
187
  const source = await readFile(sourcePath, 'utf8');
167
188
  let output = transpileSource(ts, source, sourcePath);
168
189
  for (const specifier of gatherSpecifiers(output)) {
190
+ const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
191
+ if (specialSpecifierPath) {
192
+ const nextSpecifier = relative(dirname(outPath), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
193
+ output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
194
+ continue;
195
+ }
169
196
  if (!isRelativeSpecifier(specifier)) {
170
197
  continue;
171
198
  }
@@ -176,6 +203,7 @@ async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts,
176
203
  const compiledDependencyPath = await compileImportedModule({
177
204
  projectRoot,
178
205
  modulesRoot,
206
+ serverDir,
179
207
  sourcePath: resolvedPath,
180
208
  ts,
181
209
  seen
@@ -186,12 +214,18 @@ async function compileImportedModule({ projectRoot, modulesRoot, sourcePath, ts,
186
214
  await writeFile(outPath, output, 'utf8');
187
215
  return outPath;
188
216
  }
189
- async function writeRouteModulePackage({ projectRoot, routeDir, route }) {
217
+ async function writeRouteModulePackage({ projectRoot, serverDir, routeDir, route }) {
190
218
  const ts = resolveTypeScriptApi(projectRoot);
191
219
  const modulesRoot = join(routeDir, 'modules');
192
220
  const seen = new Set();
193
221
  let entryOutput = transpileSource(ts, route.server_script || '', route.server_script_path || 'route-entry.ts');
194
222
  for (const specifier of gatherSpecifiers(entryOutput)) {
223
+ const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
224
+ if (specialSpecifierPath) {
225
+ const nextSpecifier = relative(join(routeDir, 'route'), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
226
+ entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
227
+ continue;
228
+ }
195
229
  if (!isRelativeSpecifier(specifier)) {
196
230
  continue;
197
231
  }
@@ -202,6 +236,7 @@ async function writeRouteModulePackage({ projectRoot, routeDir, route }) {
202
236
  const compiledDependencyPath = await compileImportedModule({
203
237
  projectRoot,
204
238
  modulesRoot,
239
+ serverDir,
205
240
  sourcePath: resolvedPath,
206
241
  ts,
207
242
  seen
@@ -238,8 +273,15 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
238
273
  catch {
239
274
  routerManifest = { routes: [] };
240
275
  }
241
- const routes = Array.isArray(routerManifest.routes) ? routerManifest.routes : [];
242
- const serverRoutes = routes.filter((route) => route.server_script && route.prerender !== true);
276
+ const resourceManifest = await loadResourceRouteManifest(staticDir, basePath);
277
+ const pageRoutes = Array.isArray(routerManifest.routes) ? routerManifest.routes : [];
278
+ const serverRoutes = pageRoutes
279
+ .filter((route) => route.server_script && route.prerender !== true)
280
+ .map((route) => ({ ...route, route_kind: 'page' }))
281
+ .concat((Array.isArray(resourceManifest.routes) ? resourceManifest.routes : []).map((route) => ({
282
+ ...route,
283
+ route_kind: 'resource'
284
+ })));
243
285
  await mkdir(serverDir, { recursive: true });
244
286
  await copyRuntimeFiles(serverDir);
245
287
  const imageManifestSource = join(staticDir, '_zenith', 'image', 'manifest.json');
@@ -248,8 +290,10 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
248
290
  const name = normalizeRouteName(route.path);
249
291
  const routeDir = join(serverDir, 'routes', name);
250
292
  await mkdir(routeDir, { recursive: true });
251
- const htmlSourcePath = join(staticDir, String(route.output || '').replace(/^\//, ''));
252
- await copyOptionalFile(htmlSourcePath, join(routeDir, 'route', 'page.html'));
293
+ if (route.route_kind !== 'resource') {
294
+ const htmlSourcePath = join(staticDir, String(route.output || '').replace(/^\//, ''));
295
+ await copyOptionalFile(htmlSourcePath, join(routeDir, 'route', 'page.html'));
296
+ }
253
297
  let pageAssetFile = null;
254
298
  if (typeof route.page_asset === 'string' && route.page_asset.length > 0) {
255
299
  const assetSourcePath = join(staticDir, route.page_asset.replace(/^\//, ''));
@@ -259,18 +303,20 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
259
303
  }
260
304
  }
261
305
  let imageManifestFile = null;
262
- if (await copyOptionalFile(imageManifestSource, join(routeDir, 'route', 'image-manifest.json'))) {
306
+ if (route.route_kind !== 'resource' && await copyOptionalFile(imageManifestSource, join(routeDir, 'route', 'image-manifest.json'))) {
263
307
  imageManifestFile = 'image-manifest.json';
264
308
  }
265
309
  await writeRouteModulePackage({
266
310
  projectRoot,
311
+ serverDir,
267
312
  routeDir,
268
313
  route
269
314
  });
270
315
  const meta = {
271
316
  name,
272
317
  path: route.path,
273
- output: route.output,
318
+ route_kind: route.route_kind || 'page',
319
+ output: route.output || null,
274
320
  base_path: basePath,
275
321
  page_asset: route.page_asset || null,
276
322
  page_asset_file: pageAssetFile,
@@ -282,11 +328,13 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
282
328
  has_guard: route.has_guard === true,
283
329
  has_load: route.has_load === true,
284
330
  has_action: route.has_action === true,
285
- params: extractRouteParams(route.path),
286
- image_manifest_file: imageManifestFile,
331
+ params: Array.isArray(route.params) && route.params.length > 0
332
+ ? [...route.params]
333
+ : extractRouteParams(route.path),
334
+ image_manifest_file: route.route_kind === 'resource' ? null : imageManifestFile,
287
335
  image_config: config?.images || {}
288
336
  };
289
- if (Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
337
+ if (route.route_kind !== 'resource' && Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
290
338
  meta.image_materialization = route.image_materialization;
291
339
  }
292
340
  await writeFile(join(routeDir, 'route.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
@@ -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,9 +97,23 @@ 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
119
  if (String(method || 'GET').toUpperCase() === 'HEAD') {
@@ -155,13 +169,16 @@ async function loadRuntimeContext(options = {}) {
155
169
  base_path: '/'
156
170
  });
157
171
  const serverManifest = await readJson(join(serverDir, 'manifest.json'), { routes: [] });
172
+ const allServerRoutes = Array.isArray(serverManifest.routes) ? serverManifest.routes : [];
158
173
  return {
159
174
  distDir,
160
175
  serverDir,
161
176
  staticDir: resolve(serverDir, config.static_dir || '../static'),
162
177
  buildManifest,
163
178
  buildRoutes: Array.isArray(buildManifest.routes) ? buildManifest.routes : [],
164
- serverRoutes: Array.isArray(serverManifest.routes) ? serverManifest.routes : [],
179
+ serverRoutes: allServerRoutes,
180
+ pageServerRoutes: allServerRoutes.filter((route) => route?.route_kind !== 'resource'),
181
+ resourceServerRoutes: allServerRoutes.filter((route) => route?.route_kind === 'resource'),
165
182
  images: config.images || {},
166
183
  basePath: normalizeBasePath(config.base_path || '/')
167
184
  };
@@ -206,7 +223,7 @@ async function handleRouteCheck(req, res, url, context) {
206
223
  }
207
224
  let result = { kind: 'allow' };
208
225
  let routeId = buildResolved.route.path || '';
209
- const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.serverRoutes);
226
+ const serverResolved = resolveRequestRoute(canonicalTargetUrl, context.pageServerRoutes);
210
227
  if (serverResolved.matched && serverResolved.route) {
211
228
  routeId = serverResolved.route.route_id || serverResolved.route.name || serverResolved.route.path || routeId;
212
229
  try {
@@ -275,7 +292,20 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
275
292
  await sendStaticFile(res, assetPath, req.method);
276
293
  return;
277
294
  }
278
- const serverResolved = resolveRequestRoute(canonicalUrl, context.serverRoutes);
295
+ const resourceResolved = resolveRequestRoute(canonicalUrl, context.resourceServerRoutes);
296
+ if (resourceResolved.matched && resourceResolved.route) {
297
+ const routeDir = join(context.serverDir, 'routes', resourceResolved.route.name);
298
+ const request = await createWebRequest(req, url);
299
+ const response = await renderResourceRouteRequest({
300
+ request,
301
+ route: resourceResolved.route,
302
+ params: resourceResolved.params,
303
+ routeModulePath: join(routeDir, 'route', 'entry.js')
304
+ });
305
+ await sendFetchResponse(res, response, req.method);
306
+ return;
307
+ }
308
+ const serverResolved = resolveRequestRoute(canonicalUrl, context.pageServerRoutes);
279
309
  if (serverResolved.matched && serverResolved.route) {
280
310
  const routeDir = join(context.serverDir, 'routes', serverResolved.route.name);
281
311
  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;