@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
@@ -0,0 +1,14 @@
1
+ export function buildAttachmentContentDisposition(filename: any): string;
2
+ export function createDownloadResult(body: any, options?: {}): {
3
+ kind: string;
4
+ body: any;
5
+ bodyEncoding: string;
6
+ bodySize: number;
7
+ filename: string;
8
+ contentType: string;
9
+ status: number;
10
+ };
11
+ export function assertValidDownloadResult(value: any, where?: string): void;
12
+ export function decodeDownloadResultBody(result: any, where?: string): Buffer<ArrayBuffer>;
13
+ export const DOWNLOAD_PAYLOAD_LIMIT_BYTES: number;
14
+ export const DOWNLOAD_DEFAULT_CONTENT_TYPE: "application/octet-stream";
@@ -0,0 +1,148 @@
1
+ export const DOWNLOAD_PAYLOAD_LIMIT_BYTES = 5 * 1024 * 1024;
2
+ export const DOWNLOAD_DEFAULT_CONTENT_TYPE = 'application/octet-stream';
3
+ const CONTROL_CHAR_RE = /[\0-\x1F\x7F]/;
4
+ const PATH_SEPARATOR_RE = /[\\/]/;
5
+ function formatWhere(where = 'download(...)') {
6
+ return String(where || 'download(...)');
7
+ }
8
+ function normalizeFilename(filename, where = 'download(...)') {
9
+ const label = formatWhere(where);
10
+ const value = String(filename ?? '').trim();
11
+ if (!value) {
12
+ throw new Error(`[Zenith] ${label}: download filename is required.`);
13
+ }
14
+ if (CONTROL_CHAR_RE.test(value) || PATH_SEPARATOR_RE.test(value)) {
15
+ throw new Error(`[Zenith] ${label}: download filename must not contain path separators or control characters.`);
16
+ }
17
+ return value;
18
+ }
19
+ function normalizeContentType(contentType, where = 'download(...)') {
20
+ const label = formatWhere(where);
21
+ if (contentType === undefined || contentType === null) {
22
+ return DOWNLOAD_DEFAULT_CONTENT_TYPE;
23
+ }
24
+ const value = String(contentType).trim();
25
+ if (!value) {
26
+ throw new Error(`[Zenith] ${label}: download contentType must be a non-empty string when provided.`);
27
+ }
28
+ if (CONTROL_CHAR_RE.test(value)) {
29
+ throw new Error(`[Zenith] ${label}: download contentType must not contain control characters.`);
30
+ }
31
+ return value;
32
+ }
33
+ function isBlobLike(value) {
34
+ return (typeof Blob !== 'undefined' && value instanceof Blob)
35
+ || (typeof File !== 'undefined' && value instanceof File);
36
+ }
37
+ function encodeBuffer(buffer, encoding) {
38
+ return encoding === 'utf8'
39
+ ? buffer.toString('utf8')
40
+ : buffer.toString('base64');
41
+ }
42
+ function decodeBody(body, bodyEncoding, where = 'download(...)') {
43
+ const label = formatWhere(where);
44
+ if (typeof body !== 'string') {
45
+ throw new Error(`[Zenith] ${label}: download body must be a string after normalization.`);
46
+ }
47
+ if (bodyEncoding === 'utf8') {
48
+ return Buffer.from(body, 'utf8');
49
+ }
50
+ if (bodyEncoding === 'base64') {
51
+ return Buffer.from(body, 'base64');
52
+ }
53
+ throw new Error(`[Zenith] ${label}: download bodyEncoding must be "utf8" or "base64".`);
54
+ }
55
+ function normalizeBody(body, where = 'download(...)') {
56
+ const label = formatWhere(where);
57
+ if (isBlobLike(body)) {
58
+ throw new Error(`[Zenith] ${label}: download body must be string, Uint8Array, ArrayBuffer, or Buffer-compatible bytes.`);
59
+ }
60
+ if (typeof body === 'string') {
61
+ const size = Buffer.byteLength(body, 'utf8');
62
+ if (size > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
63
+ throw new Error(`[Zenith] ${label}: download payload exceeds ${DOWNLOAD_PAYLOAD_LIMIT_BYTES} bytes.`);
64
+ }
65
+ return {
66
+ body: body,
67
+ bodyEncoding: 'utf8',
68
+ bodySize: size
69
+ };
70
+ }
71
+ if (body instanceof ArrayBuffer) {
72
+ const buffer = Buffer.from(body);
73
+ if (buffer.byteLength > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
74
+ throw new Error(`[Zenith] ${label}: download payload exceeds ${DOWNLOAD_PAYLOAD_LIMIT_BYTES} bytes.`);
75
+ }
76
+ return {
77
+ body: encodeBuffer(buffer, 'base64'),
78
+ bodyEncoding: 'base64',
79
+ bodySize: buffer.byteLength
80
+ };
81
+ }
82
+ if (ArrayBuffer.isView(body)) {
83
+ const buffer = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
84
+ if (buffer.byteLength > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
85
+ throw new Error(`[Zenith] ${label}: download payload exceeds ${DOWNLOAD_PAYLOAD_LIMIT_BYTES} bytes.`);
86
+ }
87
+ return {
88
+ body: encodeBuffer(buffer, 'base64'),
89
+ bodyEncoding: 'base64',
90
+ bodySize: buffer.byteLength
91
+ };
92
+ }
93
+ throw new Error(`[Zenith] ${label}: download body must be string, Uint8Array, ArrayBuffer, or Buffer-compatible bytes.`);
94
+ }
95
+ function buildAsciiFilename(filename) {
96
+ const replaced = Array.from(String(filename || '')).map((char) => {
97
+ const code = char.charCodeAt(0);
98
+ if (code < 0x20 || code > 0x7E || char === '"' || char === '\\') {
99
+ return '_';
100
+ }
101
+ return char;
102
+ }).join('');
103
+ return replaced || 'download';
104
+ }
105
+ export function buildAttachmentContentDisposition(filename) {
106
+ const safeFilename = normalizeFilename(filename, 'download result');
107
+ const asciiFilename = buildAsciiFilename(safeFilename);
108
+ const encodedFilename = encodeURIComponent(safeFilename)
109
+ .replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
110
+ .replace(/\*/g, '%2A');
111
+ return `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`;
112
+ }
113
+ export function createDownloadResult(body, options = {}) {
114
+ const filename = normalizeFilename(options?.filename, 'download(...)');
115
+ const contentType = normalizeContentType(options?.contentType, 'download(...)');
116
+ const normalized = normalizeBody(body, 'download(...)');
117
+ return {
118
+ kind: 'download',
119
+ body: normalized.body,
120
+ bodyEncoding: normalized.bodyEncoding,
121
+ bodySize: normalized.bodySize,
122
+ filename,
123
+ contentType,
124
+ status: 200
125
+ };
126
+ }
127
+ export function assertValidDownloadResult(value, where = 'download result') {
128
+ const label = formatWhere(where);
129
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
130
+ throw new Error(`[Zenith] ${label}: download result must be an object.`);
131
+ }
132
+ normalizeFilename(value.filename, label);
133
+ normalizeContentType(value.contentType, label);
134
+ if (value.status !== 200) {
135
+ throw new Error(`[Zenith] ${label}: download status is fixed to 200 in this milestone.`);
136
+ }
137
+ if (!Number.isInteger(value.bodySize) || value.bodySize < 0 || value.bodySize > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
138
+ throw new Error(`[Zenith] ${label}: download bodySize must be an integer between 0 and ${DOWNLOAD_PAYLOAD_LIMIT_BYTES}.`);
139
+ }
140
+ const buffer = decodeBody(value.body, value.bodyEncoding, label);
141
+ if (buffer.byteLength !== value.bodySize) {
142
+ throw new Error(`[Zenith] ${label}: download bodySize does not match the normalized body.`);
143
+ }
144
+ }
145
+ export function decodeDownloadResultBody(result, where = 'download result') {
146
+ assertValidDownloadResult(result, where);
147
+ return decodeBody(result.body, result.bodyEncoding, where);
148
+ }
@@ -18,6 +18,10 @@ function serializeInlineScriptJson(payload) {
18
18
  .replace(/\u2029/g, '\\u2029');
19
19
  }
20
20
  export function injectImageRuntimePayload(html, payload) {
21
+ // Only inject if the HTML contains Zenith image markers or unsafeHTML
22
+ if (!/data-zx-(data-zenith-image|unsafeHTML)/.test(html)) {
23
+ return html;
24
+ }
21
25
  const safePayload = createImageRuntimePayload(payload?.config || {}, payload?.localImages || {}, payload?.mode || 'passthrough', payload?.basePath || '/');
22
26
  const globalName = imageRuntimeGlobalName();
23
27
  const serialized = serializeInlineScriptJson(safePayload);
@@ -1,4 +1,16 @@
1
1
  export function buildImageArtifacts(options: any): Promise<{
2
2
  manifest: {};
3
3
  }>;
4
- export function handleImageRequest(req: any, res: any, options: any): Promise<boolean>;
4
+ /**
5
+ * @param {Request | { url?: string } | null | undefined} request
6
+ * @param {{ requestUrl?: URL | string, projectRoot: string, config?: Record<string, unknown> }} options
7
+ * @returns {Promise<Response>}
8
+ */
9
+ export function handleImageFetchRequest(request: Request | {
10
+ url?: string;
11
+ } | null | undefined, options: {
12
+ requestUrl?: URL | string;
13
+ projectRoot: string;
14
+ config?: Record<string, unknown>;
15
+ }): Promise<Response>;
16
+ export function handleImageRequest(_req: any, res: any, options: any): Promise<boolean>;
@@ -220,7 +220,29 @@ function remoteCachePaths(cacheDir, cacheKey) {
220
220
  metaPath: join(cacheDir, `${cacheKey}.json`)
221
221
  };
222
222
  }
223
- export async function handleImageRequest(req, res, options) {
223
+ function createJsonResponse(status, payload) {
224
+ return new Response(JSON.stringify(payload), {
225
+ status,
226
+ headers: {
227
+ 'Content-Type': 'application/json'
228
+ }
229
+ });
230
+ }
231
+ function createBufferResponse(status, contentType, buffer, cacheSeconds) {
232
+ return new Response(buffer, {
233
+ status,
234
+ headers: {
235
+ 'Content-Type': contentType,
236
+ 'Cache-Control': `public, max-age=${cacheSeconds}`
237
+ }
238
+ });
239
+ }
240
+ async function sendResponse(res, response) {
241
+ res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
242
+ const body = await response.arrayBuffer();
243
+ res.end(Buffer.from(body));
244
+ }
245
+ async function createImageResponse(options) {
224
246
  const { requestUrl, projectRoot, config: rawConfig } = options;
225
247
  const config = normalizeImageConfig(rawConfig);
226
248
  const url = requestUrl instanceof URL ? requestUrl : new URL(String(requestUrl));
@@ -230,14 +252,10 @@ export async function handleImageRequest(req, res, options) {
230
252
  const format = normalizeImageFormat(url.searchParams.get('f') || '');
231
253
  const quality = Number.isInteger(requestedQuality) && requestedQuality > 0 ? requestedQuality : config.quality;
232
254
  if (!remoteUrl) {
233
- res.writeHead(400, { 'Content-Type': 'application/json' });
234
- res.end(JSON.stringify({ error: 'missing_url' }));
235
- return true;
255
+ return createJsonResponse(400, { error: 'missing_url' });
236
256
  }
237
257
  if (!Number.isInteger(width) || width <= 0) {
238
- res.writeHead(400, { 'Content-Type': 'application/json' });
239
- res.end(JSON.stringify({ error: 'invalid_width' }));
240
- return true;
258
+ return createJsonResponse(400, { error: 'invalid_width' });
241
259
  }
242
260
  try {
243
261
  const remote = await validateRemoteTarget(remoteUrl, config);
@@ -253,8 +271,7 @@ export async function handleImageRequest(req, res, options) {
253
271
  const contentType = typeof parsedMeta?.contentType === 'string'
254
272
  ? parsedMeta.contentType
255
273
  : mimeTypeForFormat(format || 'jpg');
256
- sendBuffer(res, 200, contentType, cached, config.minimumCacheTTL);
257
- return true;
274
+ return createBufferResponse(200, contentType, cached, config.minimumCacheTTL);
258
275
  }
259
276
  const response = await fetch(remote, {
260
277
  headers: {
@@ -288,15 +305,28 @@ export async function handleImageRequest(req, res, options) {
288
305
  format: targetFormat
289
306
  }, null, 2)}\n`, 'utf8')
290
307
  ]);
291
- sendBuffer(res, 200, mimeTypeForFormat(targetFormat), output, config.minimumCacheTTL);
292
- return true;
308
+ return createBufferResponse(200, mimeTypeForFormat(targetFormat), output, config.minimumCacheTTL);
293
309
  }
294
310
  catch (error) {
295
- res.writeHead(400, { 'Content-Type': 'application/json' });
296
- res.end(JSON.stringify({
311
+ return createJsonResponse(400, {
297
312
  error: 'image_request_failed',
298
313
  message: error instanceof Error ? error.message : String(error)
299
- }));
300
- return true;
314
+ });
301
315
  }
302
316
  }
317
+ /**
318
+ * @param {Request | { url?: string } | null | undefined} request
319
+ * @param {{ requestUrl?: URL | string, projectRoot: string, config?: Record<string, unknown> }} options
320
+ * @returns {Promise<Response>}
321
+ */
322
+ export async function handleImageFetchRequest(request, options) {
323
+ return createImageResponse({
324
+ ...options,
325
+ requestUrl: request?.url || options?.requestUrl
326
+ });
327
+ }
328
+ export async function handleImageRequest(_req, res, options) {
329
+ const response = await createImageResponse(options);
330
+ await sendResponse(res, response);
331
+ return true;
332
+ }
@@ -2,9 +2,16 @@
2
2
  * @typedef {{
3
3
  * path: string,
4
4
  * file: string,
5
+ * route_kind?: 'page' | 'resource',
5
6
  * path_kind: 'static' | 'dynamic',
6
7
  * render_mode: 'prerender' | 'server',
7
- * params: string[]
8
+ * params: string[],
9
+ * server_script?: string,
10
+ * server_script_path?: string,
11
+ * has_guard?: boolean,
12
+ * has_load?: boolean,
13
+ * has_action?: boolean,
14
+ * export_paths?: string[]
8
15
  * }} ManifestEntry
9
16
  */
10
17
  /**
@@ -29,7 +36,14 @@ export function serializeManifest(entries: ManifestEntry[]): string;
29
36
  export type ManifestEntry = {
30
37
  path: string;
31
38
  file: string;
39
+ route_kind?: "page" | "resource";
32
40
  path_kind: "static" | "dynamic";
33
41
  render_mode: "prerender" | "server";
34
42
  params: string[];
43
+ server_script?: string;
44
+ server_script_path?: string;
45
+ has_guard?: boolean;
46
+ has_load?: boolean;
47
+ has_action?: boolean;
48
+ export_paths?: string[];
35
49
  };
package/dist/manifest.js CHANGED
@@ -16,16 +16,25 @@
16
16
  // ---------------------------------------------------------------------------
17
17
  import { readFileSync } from 'node:fs';
18
18
  import { readdir, stat } from 'node:fs/promises';
19
- import { join, relative, sep, basename, extname, dirname } from 'node:path';
19
+ import { join, relative, sep, basename, extname, dirname, resolve } from 'node:path';
20
20
  import { extractServerScript } from './build/server-script.js';
21
+ import { analyzeResourceRouteModule, isResourceRouteFile } from './resource-route-module.js';
21
22
  import { composeServerScriptEnvelope, resolveAdjacentServerModules } from './server-script-composition.js';
23
+ import { validateStaticExportPaths } from './static-export-paths.js';
22
24
  /**
23
25
  * @typedef {{
24
26
  * path: string,
25
27
  * file: string,
28
+ * route_kind?: 'page' | 'resource',
26
29
  * path_kind: 'static' | 'dynamic',
27
30
  * render_mode: 'prerender' | 'server',
28
- * params: string[]
31
+ * params: string[],
32
+ * server_script?: string,
33
+ * server_script_path?: string,
34
+ * has_guard?: boolean,
35
+ * has_load?: boolean,
36
+ * has_action?: boolean,
37
+ * export_paths?: string[]
29
38
  * }} ManifestEntry
30
39
  */
31
40
  /**
@@ -38,6 +47,11 @@ import { composeServerScriptEnvelope, resolveAdjacentServerModules } from './ser
38
47
  */
39
48
  export async function generateManifest(pagesDir, extension = '.zen', options = {}) {
40
49
  const entries = await _scanDir(pagesDir, pagesDir, extension, options.compilerOpts || {});
50
+ const apiAliasState = _resolveSrcApiAliasState(pagesDir);
51
+ if (apiAliasState) {
52
+ const aliasEntries = await _scanResourceDir(apiAliasState.aliasDir, apiAliasState.srcDir);
53
+ entries.push(...aliasEntries);
54
+ }
41
55
  // Validate: no repeated param names in any single route
42
56
  for (const entry of entries) {
43
57
  _validateParams(entry.path);
@@ -75,17 +89,60 @@ async function _scanDir(dir, root, ext, compilerOpts) {
75
89
  }
76
90
  else if (item.endsWith(ext)) {
77
91
  const routePath = _fileToRoute(fullPath, root, ext);
78
- entries.push(buildManifestEntry({
92
+ entries.push(buildPageManifestEntry({
79
93
  fullPath,
80
94
  root,
81
95
  routePath,
82
96
  compilerOpts
83
97
  }));
84
98
  }
99
+ else if (isResourceRouteFile(item)) {
100
+ entries.push(analyzeResourceRouteModule(fullPath, root));
101
+ }
85
102
  }
86
103
  return entries;
87
104
  }
88
- function buildManifestEntry({ fullPath, root, routePath, compilerOpts }) {
105
+ async function _scanResourceDir(dir, root) {
106
+ /** @type {ManifestEntry[]} */
107
+ const entries = [];
108
+ let items;
109
+ try {
110
+ items = await readdir(dir);
111
+ }
112
+ catch {
113
+ return entries;
114
+ }
115
+ items.sort();
116
+ for (const item of items) {
117
+ const fullPath = join(dir, item);
118
+ const info = await stat(fullPath);
119
+ if (info.isDirectory()) {
120
+ const nested = await _scanResourceDir(fullPath, root);
121
+ entries.push(...nested);
122
+ }
123
+ else if (isResourceRouteFile(item)) {
124
+ entries.push(analyzeResourceRouteModule(fullPath, root));
125
+ }
126
+ }
127
+ return entries;
128
+ }
129
+ function _resolveSrcApiAliasState(pagesDir) {
130
+ const resolvedPagesDir = resolve(pagesDir);
131
+ const srcDir = dirname(resolvedPagesDir);
132
+ if (basename(srcDir) !== 'src') {
133
+ return null;
134
+ }
135
+ const aliasDir = join(srcDir, 'api');
136
+ if (aliasDir === resolvedPagesDir ||
137
+ aliasDir.startsWith(`${resolvedPagesDir}${sep}`)) {
138
+ return null;
139
+ }
140
+ return {
141
+ aliasDir,
142
+ srcDir
143
+ };
144
+ }
145
+ function buildPageManifestEntry({ fullPath, root, routePath, compilerOpts }) {
89
146
  const rawSource = readFileSync(fullPath, 'utf8');
90
147
  const inlineServerScript = extractServerScript(rawSource, fullPath, compilerOpts).serverScript;
91
148
  const { guardPath, loadPath, actionPath } = resolveAdjacentServerModules(fullPath);
@@ -96,12 +153,17 @@ function buildManifestEntry({ fullPath, root, routePath, compilerOpts }) {
96
153
  adjacentLoadPath: loadPath,
97
154
  adjacentActionPath: actionPath
98
155
  });
156
+ const exportPaths = Array.isArray(composed.serverScript?.export_paths)
157
+ ? validateStaticExportPaths(routePath, composed.serverScript.export_paths, fullPath)
158
+ : [];
99
159
  return {
100
160
  path: routePath,
101
161
  file: relative(root, fullPath),
162
+ route_kind: 'page',
102
163
  path_kind: _isDynamic(routePath) ? 'dynamic' : 'static',
103
164
  render_mode: composed.serverScript && composed.serverScript.prerender !== true ? 'server' : 'prerender',
104
- params: extractRouteParams(routePath)
165
+ params: extractRouteParams(routePath),
166
+ ...(exportPaths.length > 0 ? { export_paths: exportPaths } : {})
105
167
  };
106
168
  }
107
169
  function extractRouteParams(routePath) {
@@ -325,7 +387,9 @@ function segmentWeight(segment) {
325
387
  * @returns {string}
326
388
  */
327
389
  export function serializeManifest(entries) {
328
- const lines = entries.map((e) => {
390
+ const lines = entries
391
+ .filter((entry) => entry?.route_kind !== 'resource')
392
+ .map((e) => {
329
393
  const hasParams = _isDynamic(e.path);
330
394
  const loader = hasParams
331
395
  ? `(params) => import('./pages/${e.file}')`
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Create and start a preview server.
3
+ *
4
+ * @param {{ distDir: string, port?: number, host?: string, logger?: object | null, config?: object, projectRoot?: string }} options
5
+ * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
6
+ */
7
+ export function createPreviewServer(options: {
8
+ distDir: string;
9
+ port?: number;
10
+ host?: string;
11
+ logger?: object | null;
12
+ config?: object;
13
+ projectRoot?: string;
14
+ }): Promise<{
15
+ server: import("http").Server;
16
+ port: number;
17
+ close: () => void;
18
+ }>;
@@ -0,0 +1,71 @@
1
+ import { createServer } from 'node:http';
2
+ import { resolve } from 'node:path';
3
+ import { normalizeBasePath } from '../base-path.js';
4
+ import { resolveBuildAdapter } from '../adapters/resolve-adapter.js';
5
+ import { isConfigKeyExplicit, isLoadedConfig, loadConfig, validateConfig } from '../config.js';
6
+ import { createTrustedOriginResolver } from '../request-origin.js';
7
+ import { supportsTargetRouteCheck } from '../route-check-support.js';
8
+ import { createSilentLogger } from '../ui/logger.js';
9
+ import { createPreviewRequestHandler } from './request-handler.js';
10
+ /**
11
+ * Create and start a preview server.
12
+ *
13
+ * @param {{ distDir: string, port?: number, host?: string, logger?: object | null, config?: object, projectRoot?: string }} options
14
+ * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
15
+ */
16
+ export async function createPreviewServer(options) {
17
+ const resolvedProjectRoot = options?.projectRoot ? resolve(options.projectRoot) : resolve(options.distDir, '..');
18
+ const loadedConfig = await loadConfig(resolvedProjectRoot);
19
+ const resolvedConfig = options?.config && typeof options.config === 'object'
20
+ ? (() => {
21
+ const overrideConfig = isLoadedConfig(options.config)
22
+ ? options.config
23
+ : validateConfig(options.config);
24
+ const mergedConfig = { ...loadedConfig };
25
+ for (const key of Object.keys(overrideConfig)) {
26
+ if (isConfigKeyExplicit(overrideConfig, key)) {
27
+ mergedConfig[key] = overrideConfig[key];
28
+ }
29
+ }
30
+ return mergedConfig;
31
+ })()
32
+ : loadedConfig;
33
+ const { distDir, port = 4000, host = '127.0.0.1', logger: providedLogger = null } = options;
34
+ const projectRoot = resolvedProjectRoot;
35
+ const config = resolvedConfig;
36
+ const logger = providedLogger || createSilentLogger();
37
+ const verboseLogging = logger.mode?.logLevel === 'verbose';
38
+ const configuredBasePath = normalizeBasePath(config.basePath || '/');
39
+ const resolvedTarget = resolveBuildAdapter(config).target;
40
+ const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
41
+ const isStaticExportTarget = resolvedTarget === 'static-export';
42
+ let actualPort = port;
43
+ const resolveServerOrigin = createTrustedOriginResolver({
44
+ host,
45
+ getPort: () => actualPort,
46
+ label: 'preview server'
47
+ });
48
+ const server = createServer(createPreviewRequestHandler({
49
+ distDir,
50
+ projectRoot,
51
+ config,
52
+ logger,
53
+ verboseLogging,
54
+ configuredBasePath,
55
+ routeCheckEnabled,
56
+ isStaticExportTarget,
57
+ serverOrigin: resolveServerOrigin
58
+ }));
59
+ return new Promise((resolveServer) => {
60
+ server.listen(port, host, () => {
61
+ actualPort = server.address().port;
62
+ resolveServer({
63
+ server,
64
+ port: actualPort,
65
+ close: () => {
66
+ server.close();
67
+ }
68
+ });
69
+ });
70
+ });
71
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @typedef {{
3
+ * path: string;
4
+ * output: string;
5
+ * server_script?: string | null;
6
+ * server_script_path?: string | null;
7
+ * prerender?: boolean;
8
+ * route_id?: string;
9
+ * pattern?: string;
10
+ * params_shape?: Record<string, string>;
11
+ * has_guard?: boolean;
12
+ * has_load?: boolean;
13
+ * guard_module_ref?: string | null;
14
+ * load_module_ref?: string | null;
15
+ * }} PreviewRoute
16
+ */
17
+ /**
18
+ * @param {string} distDir
19
+ * @returns {Promise<PreviewRoute[]>}
20
+ */
21
+ export function loadRouteManifest(distDir: string): Promise<PreviewRoute[]>;
22
+ export function loadRouteSurfaceState(distDir: any, fallbackBasePath?: string): Promise<{
23
+ basePath: string;
24
+ pageRoutes: any;
25
+ resourceRoutes: any[];
26
+ }>;
27
+ export const matchRoute: typeof matchManifestRoute;
28
+ export type PreviewRoute = {
29
+ path: string;
30
+ output: string;
31
+ server_script?: string | null;
32
+ server_script_path?: string | null;
33
+ prerender?: boolean;
34
+ route_id?: string;
35
+ pattern?: string;
36
+ params_shape?: Record<string, string>;
37
+ has_guard?: boolean;
38
+ has_load?: boolean;
39
+ guard_module_ref?: string | null;
40
+ load_module_ref?: string | null;
41
+ };
42
+ import { matchRoute as matchManifestRoute } from '../server/resolve-request-route.js';
@@ -0,0 +1,57 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { normalizeBasePath } from '../base-path.js';
4
+ import { loadResourceRouteManifest } from '../resource-manifest.js';
5
+ import { compareRouteSpecificity, matchRoute as matchManifestRoute } from '../server/resolve-request-route.js';
6
+ /**
7
+ * @typedef {{
8
+ * path: string;
9
+ * output: string;
10
+ * server_script?: string | null;
11
+ * server_script_path?: string | null;
12
+ * prerender?: boolean;
13
+ * route_id?: string;
14
+ * pattern?: string;
15
+ * params_shape?: Record<string, string>;
16
+ * has_guard?: boolean;
17
+ * has_load?: boolean;
18
+ * guard_module_ref?: string | null;
19
+ * load_module_ref?: string | null;
20
+ * }} PreviewRoute
21
+ */
22
+ /**
23
+ * @param {string} distDir
24
+ * @returns {Promise<PreviewRoute[]>}
25
+ */
26
+ export async function loadRouteManifest(distDir) {
27
+ const state = await loadRouteSurfaceState(distDir, '/');
28
+ return state.pageRoutes;
29
+ }
30
+ export async function loadRouteSurfaceState(distDir, fallbackBasePath = '/') {
31
+ const manifestPath = join(distDir, 'assets', 'router-manifest.json');
32
+ const resourceState = await loadResourceRouteManifest(distDir, normalizeBasePath(fallbackBasePath || '/'));
33
+ try {
34
+ const source = await readFile(manifestPath, 'utf8');
35
+ const parsed = JSON.parse(source);
36
+ const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
37
+ const basePath = normalizeBasePath(parsed?.base_path || resourceState.basePath || fallbackBasePath || '/');
38
+ return {
39
+ basePath,
40
+ pageRoutes: routes
41
+ .filter((entry) => entry &&
42
+ typeof entry === 'object' &&
43
+ typeof entry.path === 'string' &&
44
+ typeof entry.output === 'string')
45
+ .sort((a, b) => compareRouteSpecificity(a.path, b.path)),
46
+ resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
47
+ };
48
+ }
49
+ catch {
50
+ return {
51
+ basePath: normalizeBasePath(resourceState.basePath || fallbackBasePath || '/'),
52
+ pageRoutes: [],
53
+ resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
54
+ };
55
+ }
56
+ }
57
+ export const matchRoute = matchManifestRoute;
@@ -0,0 +1,3 @@
1
+ export function toStaticFilePath(distDir: any, pathname: any): string | null;
2
+ export function resolveWithinDist(distDir: any, requestPath: any): string | null;
3
+ export function fileExists(fullPath: any): Promise<boolean>;