@zenithbuild/cli 0.7.5 → 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 (68) hide show
  1. package/dist/adapters/adapter-netlify.js +0 -8
  2. package/dist/adapters/adapter-vercel.js +6 -14
  3. package/dist/adapters/copy-hosted-page-runtime.js +2 -1
  4. package/dist/build/hoisted-code-transforms.d.ts +4 -1
  5. package/dist/build/hoisted-code-transforms.js +5 -3
  6. package/dist/build/page-ir-normalization.d.ts +1 -1
  7. package/dist/build/page-ir-normalization.js +33 -3
  8. package/dist/build/page-loop.js +46 -2
  9. package/dist/dev-build-session/helpers.d.ts +29 -0
  10. package/dist/dev-build-session/helpers.js +223 -0
  11. package/dist/dev-build-session/session.d.ts +24 -0
  12. package/dist/dev-build-session/session.js +204 -0
  13. package/dist/dev-build-session/state.d.ts +37 -0
  14. package/dist/dev-build-session/state.js +17 -0
  15. package/dist/dev-build-session.d.ts +1 -24
  16. package/dist/dev-build-session.js +1 -434
  17. package/dist/dev-server/css-state.d.ts +7 -0
  18. package/dist/dev-server/css-state.js +92 -0
  19. package/dist/dev-server/not-found.d.ts +23 -0
  20. package/dist/dev-server/not-found.js +129 -0
  21. package/dist/dev-server/request-handler.d.ts +1 -0
  22. package/dist/dev-server/request-handler.js +376 -0
  23. package/dist/dev-server/route-check.d.ts +9 -0
  24. package/dist/dev-server/route-check.js +100 -0
  25. package/dist/dev-server/watcher.d.ts +5 -0
  26. package/dist/dev-server/watcher.js +216 -0
  27. package/dist/dev-server.js +123 -924
  28. package/dist/images/payload.js +4 -0
  29. package/dist/manifest.js +46 -1
  30. package/dist/preview/create-preview-server.d.ts +18 -0
  31. package/dist/preview/create-preview-server.js +71 -0
  32. package/dist/preview/manifest.d.ts +42 -0
  33. package/dist/preview/manifest.js +57 -0
  34. package/dist/preview/paths.d.ts +3 -0
  35. package/dist/preview/paths.js +38 -0
  36. package/dist/preview/payload.d.ts +6 -0
  37. package/dist/preview/payload.js +34 -0
  38. package/dist/preview/request-handler.d.ts +1 -0
  39. package/dist/preview/request-handler.js +300 -0
  40. package/dist/preview/server-runner.d.ts +49 -0
  41. package/dist/preview/server-runner.js +220 -0
  42. package/dist/preview/server-script-runner-template.d.ts +1 -0
  43. package/dist/preview/server-script-runner-template.js +425 -0
  44. package/dist/preview.d.ts +5 -112
  45. package/dist/preview.js +7 -1119
  46. package/dist/resource-response.d.ts +15 -0
  47. package/dist/resource-response.js +91 -2
  48. package/dist/server-contract/constants.d.ts +5 -0
  49. package/dist/server-contract/constants.js +5 -0
  50. package/dist/server-contract/export-validation.d.ts +5 -0
  51. package/dist/server-contract/export-validation.js +59 -0
  52. package/dist/server-contract/json-serializable.d.ts +1 -0
  53. package/dist/server-contract/json-serializable.js +52 -0
  54. package/dist/server-contract/resolve.d.ts +15 -0
  55. package/dist/server-contract/resolve.js +271 -0
  56. package/dist/server-contract/result-helpers.d.ts +51 -0
  57. package/dist/server-contract/result-helpers.js +59 -0
  58. package/dist/server-contract/route-result-validation.d.ts +2 -0
  59. package/dist/server-contract/route-result-validation.js +73 -0
  60. package/dist/server-contract/stage.d.ts +6 -0
  61. package/dist/server-contract/stage.js +22 -0
  62. package/dist/server-contract.d.ts +6 -62
  63. package/dist/server-contract.js +9 -493
  64. package/dist/server-middleware.d.ts +10 -0
  65. package/dist/server-middleware.js +30 -0
  66. package/dist/server-output.js +13 -1
  67. package/dist/server-runtime/node-server.js +25 -3
  68. package/package.json +3 -3
package/dist/preview.js CHANGED
@@ -3,1123 +3,11 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  // Preview server with manifest-driven route resolution.
5
5
  //
6
- // - Serves /dist assets directly.
7
- // - Resolves static and dynamic page routes via router-manifest.json.
8
- // - Executes non-prerender <script server> blocks per request and injects
9
- // serialized SSR payload via an inline script (`window.__zenith_ssr_data`).
6
+ // This file is intentionally a composition facade. Implementation details
7
+ // live in ./preview/* modules.
10
8
  // ---------------------------------------------------------------------------
11
- import { spawn } from 'node:child_process';
12
- import { createServer } from 'node:http';
13
- import { access, readFile } from 'node:fs/promises';
14
- import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
- import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from './base-path.js';
17
- import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
18
- import { isConfigKeyExplicit, isLoadedConfig, loadConfig, validateConfig } from './config.js';
19
- import { materializeImageMarkup } from './images/materialize.js';
20
- import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
21
- import { handleImageRequest } from './images/service.js';
22
- import { readRequestBodyBuffer } from './request-body.js';
23
- import { createTrustedOriginResolver } from './request-origin.js';
24
- import { buildResourceResponseDescriptor } from './resource-response.js';
25
- import { loadResourceRouteManifest } from './resource-manifest.js';
26
- import { supportsTargetRouteCheck } from './route-check-support.js';
27
- import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
28
- import { createSilentLogger } from './ui/logger.js';
29
- import { compareRouteSpecificity, matchRoute as matchManifestRoute, resolveRequestRoute } from './server/resolve-request-route.js';
30
- const __dirname = dirname(fileURLToPath(import.meta.url));
31
- const MIME_TYPES = {
32
- '.html': 'text/html',
33
- '.js': 'application/javascript',
34
- '.css': 'text/css',
35
- '.json': 'application/json',
36
- '.png': 'image/png',
37
- '.jpeg': 'image/jpeg',
38
- '.jpg': 'image/jpeg',
39
- '.svg': 'image/svg+xml',
40
- '.webp': 'image/webp',
41
- '.avif': 'image/avif',
42
- '.gif': 'image/gif'
43
- };
44
- const IMAGE_RUNTIME_TAG_RE = /<script\b[^>]*\bid=(["'])zenith-image-runtime\1[^>]*>[\s\S]*?<\/script>/i;
45
- function appendSetCookieHeaders(headers, setCookies = []) {
46
- if (Array.isArray(setCookies) && setCookies.length > 0) {
47
- headers['Set-Cookie'] = setCookies.slice();
48
- }
49
- return headers;
50
- }
51
- const SERVER_SCRIPT_RUNNER = String.raw `
52
- import vm from 'node:vm';
53
- import fs from 'node:fs/promises';
54
- import path from 'node:path';
55
- import { pathToFileURL, fileURLToPath } from 'node:url';
56
-
57
- const source = process.env.ZENITH_SERVER_SOURCE || '';
58
- const sourcePath = process.env.ZENITH_SERVER_SOURCE_PATH || '';
59
- const params = JSON.parse(process.env.ZENITH_SERVER_PARAMS || '{}');
60
- const requestUrl = process.env.ZENITH_SERVER_REQUEST_URL || 'http://localhost/';
61
- const requestMethod = String(process.env.ZENITH_SERVER_REQUEST_METHOD || 'GET').toUpperCase();
62
- const requestHeaders = JSON.parse(process.env.ZENITH_SERVER_REQUEST_HEADERS || '{}');
63
- const routePattern = process.env.ZENITH_SERVER_ROUTE_PATTERN || '';
64
- const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
65
- const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
66
- const routeKind = process.env.ZENITH_SERVER_ROUTE_KIND || 'page';
67
- const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
68
-
69
- if (!source.trim()) {
70
- process.stdout.write('null');
71
- process.exit(0);
72
- }
73
-
74
- let cachedTypeScript = undefined;
75
- async function loadTypeScript() {
76
- if (cachedTypeScript !== undefined) {
77
- return cachedTypeScript;
78
- }
79
- try {
80
- const mod = await import('typescript');
81
- cachedTypeScript = mod.default || mod;
82
- } catch {
83
- cachedTypeScript = null;
84
- }
85
- return cachedTypeScript;
86
- }
87
-
88
- async function transpileIfNeeded(filename, code) {
89
- const lower = String(filename || '').toLowerCase();
90
- const isTs =
91
- lower.endsWith('.ts') ||
92
- lower.endsWith('.tsx') ||
93
- lower.endsWith('.mts') ||
94
- lower.endsWith('.cts');
95
- if (!isTs) {
96
- return code;
97
- }
98
- const ts = await loadTypeScript();
99
- if (!ts || typeof ts.transpileModule !== 'function') {
100
- throw new Error('[zenith-preview] TypeScript is required to execute server modules that import .ts files');
101
- }
102
- const output = ts.transpileModule(code, {
103
- fileName: filename || 'server-script.ts',
104
- compilerOptions: {
105
- target: ts.ScriptTarget.ES2022,
106
- module: ts.ModuleKind.ESNext,
107
- moduleResolution: ts.ModuleResolutionKind.NodeNext
108
- },
109
- reportDiagnostics: false
110
- });
111
- return output.outputText;
112
- }
113
-
114
- async function exists(filePath) {
115
- try {
116
- await fs.access(filePath);
117
- return true;
118
- } catch {
119
- return false;
120
- }
121
- }
122
-
123
- async function resolveRelativeSpecifier(specifier, parentIdentifier) {
124
- let basePath = sourcePath;
125
- if (parentIdentifier && parentIdentifier.startsWith('file:')) {
126
- basePath = fileURLToPath(parentIdentifier);
127
- }
128
-
129
- const baseDir = basePath ? path.dirname(basePath) : process.cwd();
130
- const candidateBase = specifier.startsWith('file:')
131
- ? fileURLToPath(specifier)
132
- : path.resolve(baseDir, specifier);
133
-
134
- const candidates = [];
135
- if (path.extname(candidateBase)) {
136
- candidates.push(candidateBase);
137
- } else {
138
- candidates.push(candidateBase);
139
- candidates.push(candidateBase + '.ts');
140
- candidates.push(candidateBase + '.tsx');
141
- candidates.push(candidateBase + '.mts');
142
- candidates.push(candidateBase + '.cts');
143
- candidates.push(candidateBase + '.js');
144
- candidates.push(candidateBase + '.mjs');
145
- candidates.push(candidateBase + '.cjs');
146
- candidates.push(path.join(candidateBase, 'index.ts'));
147
- candidates.push(path.join(candidateBase, 'index.tsx'));
148
- candidates.push(path.join(candidateBase, 'index.mts'));
149
- candidates.push(path.join(candidateBase, 'index.cts'));
150
- candidates.push(path.join(candidateBase, 'index.js'));
151
- candidates.push(path.join(candidateBase, 'index.mjs'));
152
- candidates.push(path.join(candidateBase, 'index.cjs'));
153
- }
154
-
155
- for (const candidate of candidates) {
156
- if (await exists(candidate)) {
157
- return pathToFileURL(candidate).href;
158
- }
159
- }
160
-
161
- throw new Error(
162
- '[zenith-preview] Cannot resolve server import "' + specifier + '" from "' + (basePath || '<inline>') + '"'
163
- );
164
- }
165
-
166
- function isRelativeSpecifier(specifier) {
167
- return (
168
- specifier.startsWith('./') ||
169
- specifier.startsWith('../') ||
170
- specifier.startsWith('/') ||
171
- specifier.startsWith('file:')
172
- );
173
- }
174
-
175
- const safeRequestHeaders =
176
- requestHeaders && typeof requestHeaders === 'object'
177
- ? { ...requestHeaders }
178
- : {};
179
- function parseCookies(rawCookieHeader) {
180
- const out = Object.create(null);
181
- const raw = String(rawCookieHeader || '');
182
- if (!raw) return out;
183
- const pairs = raw.split(';');
184
- for (let i = 0; i < pairs.length; i++) {
185
- const part = pairs[i];
186
- const eq = part.indexOf('=');
187
- if (eq <= 0) continue;
188
- const key = part.slice(0, eq).trim();
189
- if (!key) continue;
190
- const value = part.slice(eq + 1).trim();
191
- try {
192
- out[key] = decodeURIComponent(value);
193
- } catch {
194
- out[key] = value;
195
- }
196
- }
197
- return out;
198
- }
199
- const cookieHeader = typeof safeRequestHeaders.cookie === 'string'
200
- ? safeRequestHeaders.cookie
201
- : '';
202
- const requestCookies = parseCookies(cookieHeader);
203
-
204
- function ctxAllow() {
205
- return { kind: 'allow' };
206
- }
207
- function ctxRedirect(location, status = 302) {
208
- return {
209
- kind: 'redirect',
210
- location: String(location || ''),
211
- status: Number.isInteger(status) ? status : 302
212
- };
213
- }
214
- function ctxDeny(status = 403, message = undefined) {
215
- return {
216
- kind: 'deny',
217
- status: Number.isInteger(status) ? status : 403,
218
- message: typeof message === 'string' ? message : undefined
219
- };
220
- }
221
- function ctxInvalid(payload, status = 400) {
222
- return {
223
- kind: 'invalid',
224
- data: payload,
225
- status: Number.isInteger(status) ? status : 400
226
- };
227
- }
228
- function ctxData(payload) {
229
- return {
230
- kind: 'data',
231
- data: payload
232
- };
233
- }
234
- function ctxJson(payload, status = 200) {
235
- return {
236
- kind: 'json',
237
- data: payload,
238
- status: Number.isInteger(status) ? status : 200
239
- };
240
- }
241
- function ctxText(body, status = 200) {
242
- return {
243
- kind: 'text',
244
- body: typeof body === 'string' ? body : String(body ?? ''),
245
- status: Number.isInteger(status) ? status : 200
246
- };
247
- }
248
-
249
- async function readStdinBuffer() {
250
- const chunks = [];
251
- for await (const chunk of process.stdin) {
252
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
253
- }
254
- return Buffer.concat(chunks);
255
- }
256
-
257
- const requestInit = {
258
- method: requestMethod,
259
- headers: new Headers(safeRequestHeaders)
260
- };
261
- const requestBodyBuffer =
262
- requestMethod !== 'GET' && requestMethod !== 'HEAD'
263
- ? await readStdinBuffer()
264
- : Buffer.alloc(0);
265
- if (requestMethod !== 'GET' && requestMethod !== 'HEAD' && requestBodyBuffer.length > 0) {
266
- requestInit.body = requestBodyBuffer;
267
- requestInit.duplex = 'half';
268
- }
269
- const requestSnapshot = new Request(requestUrl, requestInit);
270
- const routeParams = { ...params };
271
- const routeMeta = {
272
- id: routeId,
273
- pattern: routePattern,
274
- file: routeFile ? path.relative(process.cwd(), routeFile) : ''
275
- };
276
- const routeContext = {
277
- params: routeParams,
278
- url: new URL(requestUrl),
279
- headers: { ...safeRequestHeaders },
280
- cookies: requestCookies,
281
- request: requestSnapshot,
282
- method: requestMethod,
283
- route: routeMeta,
284
- env: {},
285
- action: null,
286
- allow: ctxAllow,
287
- redirect: ctxRedirect,
288
- deny: ctxDeny,
289
- invalid: ctxInvalid,
290
- data: ctxData,
291
- json: ctxJson,
292
- text: ctxText
293
- };
294
-
295
- const context = vm.createContext({
296
- params: routeParams,
297
- ctx: routeContext,
298
- fetch: globalThis.fetch,
299
- Blob: globalThis.Blob,
300
- File: globalThis.File,
301
- FormData: globalThis.FormData,
302
- Headers: globalThis.Headers,
303
- Request: globalThis.Request,
304
- Response: globalThis.Response,
305
- TextEncoder: globalThis.TextEncoder,
306
- TextDecoder: globalThis.TextDecoder,
307
- URL,
308
- URLSearchParams,
309
- Buffer,
310
- console,
311
- process,
312
- setTimeout,
313
- clearTimeout,
314
- setInterval,
315
- clearInterval
316
- });
317
-
318
- const moduleCache = new Map();
319
- const syntheticModuleCache = new Map();
320
-
321
- async function createSyntheticModule(specifier) {
322
- if (syntheticModuleCache.has(specifier)) {
323
- return syntheticModuleCache.get(specifier);
324
- }
325
-
326
- const ns = await import(specifier);
327
- const exportNames = Object.keys(ns);
328
- const module = new vm.SyntheticModule(
329
- exportNames,
330
- function() {
331
- for (const key of exportNames) {
332
- this.setExport(key, ns[key]);
333
- }
334
- },
335
- { context }
336
- );
337
- await module.link(() => {
338
- throw new Error(
339
- '[zenith-preview] synthetic modules cannot contain nested imports: ' + specifier
340
- );
341
- });
342
- syntheticModuleCache.set(specifier, module);
343
- return module;
344
- }
345
-
346
- async function loadFileModule(moduleUrl) {
347
- if (moduleCache.has(moduleUrl)) {
348
- return moduleCache.get(moduleUrl);
349
- }
350
- const modulePromise = (async () => {
351
- const filename = fileURLToPath(moduleUrl);
352
- let code = await fs.readFile(filename, 'utf8');
353
- code = await transpileIfNeeded(filename, code);
354
- const module = new vm.SourceTextModule(code, {
355
- context,
356
- identifier: moduleUrl,
357
- initializeImportMeta(meta) {
358
- meta.url = moduleUrl;
359
- }
360
- });
361
-
362
- await module.link((specifier, referencingModule) => {
363
- return linkModule(specifier, referencingModule.identifier);
364
- });
365
- return module;
366
- })();
367
-
368
- moduleCache.set(moduleUrl, modulePromise);
369
- try {
370
- return await modulePromise;
371
- } catch (error) {
372
- moduleCache.delete(moduleUrl);
373
- throw error;
374
- }
375
- }
376
-
377
- async function linkModule(specifier, parentIdentifier) {
378
- if (!isRelativeSpecifier(specifier)) {
379
- return createSyntheticModule(specifier);
380
- }
381
- const resolvedUrl = await resolveRelativeSpecifier(specifier, parentIdentifier);
382
- return loadFileModule(resolvedUrl);
383
- }
384
-
385
- const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender', 'exportPaths']);
386
- const prelude = "const params = globalThis.params;\n" +
387
- "const ctx = globalThis.ctx;\n" +
388
- "import { download, resolveRouteResult } from 'zenith:server-contract';\n" +
389
- "import { attachRouteAuth } from 'zenith:route-auth';\n" +
390
- "ctx.download = download;\n" +
391
- "globalThis.resolveRouteResult = resolveRouteResult;\n" +
392
- "globalThis.attachRouteAuth = attachRouteAuth;\n";
393
- const entryIdentifier = sourcePath
394
- ? pathToFileURL(sourcePath).href
395
- : 'zenith:server-script';
396
- const entryTranspileFilename = sourcePath && sourcePath.toLowerCase().endsWith('.zen')
397
- ? sourcePath.replace(/\.zen$/i, '.ts')
398
- : (sourcePath || 'server-script.ts');
399
-
400
- const entryCode = await transpileIfNeeded(entryTranspileFilename, prelude + source);
401
- const entryModule = new vm.SourceTextModule(entryCode, {
402
- context,
403
- identifier: entryIdentifier,
404
- initializeImportMeta(meta) {
405
- meta.url = entryIdentifier;
406
- }
407
- });
408
-
409
- moduleCache.set(entryIdentifier, entryModule);
410
- await entryModule.link((specifier, referencingModule) => {
411
- if (specifier === 'zenith:server-contract') {
412
- const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
413
- const configuredPath = process.env.ZENITH_SERVER_CONTRACT_PATH || '';
414
- const contractUrl = pathToFileURL(configuredPath || defaultPath).href;
415
- if (configuredPath) {
416
- return loadFileModule(contractUrl);
417
- }
418
- return loadFileModule(contractUrl).catch(() =>
419
- loadFileModule(pathToFileURL(defaultPath).href)
420
- );
421
- }
422
- if (specifier === 'zenith:route-auth') {
423
- const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'auth', 'route-auth.js');
424
- const configuredPath = process.env.ZENITH_SERVER_ROUTE_AUTH_PATH || '';
425
- const authUrl = pathToFileURL(configuredPath || defaultPath).href;
426
- if (configuredPath) {
427
- return loadFileModule(authUrl);
428
- }
429
- return loadFileModule(authUrl).catch(() =>
430
- loadFileModule(pathToFileURL(defaultPath).href)
431
- );
432
- }
433
- return linkModule(specifier, referencingModule.identifier);
434
- });
435
- await entryModule.evaluate();
436
- context.attachRouteAuth(routeContext, {
437
- requestUrl: routeContext.url,
438
- guardOnly,
439
- redirect: ctxRedirect,
440
- deny: ctxDeny
441
- });
442
-
443
- const namespaceKeys = Object.keys(entryModule.namespace);
444
- for (const key of namespaceKeys) {
445
- if (!allowed.has(key)) {
446
- throw new Error('[zenith-preview] unsupported server export "' + key + '"');
447
- }
448
- }
449
-
450
- const exported = entryModule.namespace;
451
- try {
452
- const resolved = await context.resolveRouteResult({
453
- exports: exported,
454
- ctx: context.ctx,
455
- filePath: sourcePath || 'server_script',
456
- guardOnly: guardOnly,
457
- routeKind: routeKind
458
- });
459
-
460
- process.stdout.write(JSON.stringify(resolved || null));
461
- } catch (error) {
462
- const message = error instanceof Error
463
- ? (typeof error.stack === 'string' && error.stack.length > 0 ? error.stack : error.message)
464
- : String(error);
465
- process.stderr.write('[Zenith:Server] preview route execution failed\\n' + message + '\\n');
466
- process.stdout.write(
467
- JSON.stringify({
468
- __zenith_error: {
469
- status: 500,
470
- code: 'LOAD_FAILED'
471
- }
472
- })
473
- );
474
- }
475
- `;
476
- /**
477
- * Create and start a preview server.
478
- *
479
- * @param {{ distDir: string, port?: number, host?: string, logger?: object | null, config?: object, projectRoot?: string }} options
480
- * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
481
- */
482
- export async function createPreviewServer(options) {
483
- const resolvedProjectRoot = options?.projectRoot ? resolve(options.projectRoot) : resolve(options.distDir, '..');
484
- const loadedConfig = await loadConfig(resolvedProjectRoot);
485
- const resolvedConfig = options?.config && typeof options.config === 'object'
486
- ? (() => {
487
- const overrideConfig = isLoadedConfig(options.config)
488
- ? options.config
489
- : validateConfig(options.config);
490
- const mergedConfig = { ...loadedConfig };
491
- for (const key of Object.keys(overrideConfig)) {
492
- if (isConfigKeyExplicit(overrideConfig, key)) {
493
- mergedConfig[key] = overrideConfig[key];
494
- }
495
- }
496
- return mergedConfig;
497
- })()
498
- : loadedConfig;
499
- const { distDir, port = 4000, host = '127.0.0.1', logger: providedLogger = null } = options;
500
- const projectRoot = resolvedProjectRoot;
501
- const config = resolvedConfig;
502
- const logger = providedLogger || createSilentLogger();
503
- const verboseLogging = logger.mode?.logLevel === 'verbose';
504
- const configuredBasePath = normalizeBasePath(config.basePath || '/');
505
- const resolvedTarget = resolveBuildAdapter(config).target;
506
- const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
507
- const isStaticExportTarget = resolvedTarget === 'static-export';
508
- let actualPort = port;
509
- const resolveServerOrigin = createTrustedOriginResolver({
510
- host,
511
- getPort: () => actualPort,
512
- label: 'preview server'
513
- });
514
- async function loadImageManifest() {
515
- try {
516
- const manifestRaw = await readFile(join(distDir, '_zenith', 'image', 'manifest.json'), 'utf8');
517
- const parsed = JSON.parse(manifestRaw);
518
- return parsed && typeof parsed === 'object' ? parsed : {};
519
- }
520
- catch {
521
- return {};
522
- }
523
- }
524
- const server = createServer(async (req, res) => {
525
- const url = new URL(req.url, resolveServerOrigin());
526
- const { basePath, pageRoutes, resourceRoutes } = await loadRouteSurfaceState(distDir, configuredBasePath);
527
- const canonicalPath = stripBasePath(url.pathname, basePath);
528
- try {
529
- if (url.pathname === routeCheckPath(basePath)) {
530
- if (!routeCheckEnabled) {
531
- res.writeHead(501, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
532
- res.end(JSON.stringify({ error: 'route_check_unsupported' }));
533
- return;
534
- }
535
- // Security: Require explicitly designated header to prevent public oracle probing
536
- if (req.headers['x-zenith-route-check'] !== '1') {
537
- res.writeHead(403, { 'Content-Type': 'application/json' });
538
- res.end(JSON.stringify({ error: 'forbidden', message: 'invalid request context' }));
539
- return;
540
- }
541
- const targetPath = String(url.searchParams.get('path') || '/');
542
- // Security: Prevent protocol/domain injection in path
543
- if (targetPath.includes('://') || targetPath.startsWith('//') || /[\r\n]/.test(targetPath)) {
544
- res.writeHead(400, { 'Content-Type': 'application/json' });
545
- res.end(JSON.stringify({ error: 'invalid_path_format' }));
546
- return;
547
- }
548
- const targetUrl = new URL(targetPath, url.origin);
549
- if (targetUrl.origin !== url.origin) {
550
- res.writeHead(400, { 'Content-Type': 'application/json' });
551
- res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
552
- return;
553
- }
554
- const canonicalTargetPath = stripBasePath(targetUrl.pathname, basePath);
555
- if (canonicalTargetPath === null) {
556
- res.writeHead(404, { 'Content-Type': 'application/json' });
557
- res.end(JSON.stringify({ error: 'route_not_found' }));
558
- return;
559
- }
560
- const canonicalTargetUrl = new URL(targetUrl.toString());
561
- canonicalTargetUrl.pathname = canonicalTargetPath;
562
- const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, pageRoutes);
563
- if (!resolvedCheck.matched || !resolvedCheck.route) {
564
- res.writeHead(404, { 'Content-Type': 'application/json' });
565
- res.end(JSON.stringify({ error: 'route_not_found' }));
566
- return;
567
- }
568
- const checkResult = await executeServerRoute({
569
- source: resolvedCheck.route.server_script || '',
570
- sourcePath: resolvedCheck.route.server_script_path || '',
571
- params: resolvedCheck.params,
572
- requestUrl: targetUrl.toString(),
573
- requestMethod: req.method || 'GET',
574
- requestHeaders: req.headers,
575
- routePattern: resolvedCheck.route.path,
576
- routeFile: resolvedCheck.route.server_script_path || '',
577
- routeId: resolvedCheck.route.route_id || routeIdFromSourcePath(resolvedCheck.route.server_script_path || ''),
578
- guardOnly: true
579
- });
580
- // Security: Enforce relative or same-origin redirects
581
- if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
582
- const loc = appLocalRedirectLocation(checkResult.result.location || '/', basePath);
583
- checkResult.result.location = loc;
584
- if (loc.includes('://') || loc.startsWith('//')) {
585
- try {
586
- const parsedLoc = new URL(loc);
587
- if (parsedLoc.origin !== targetUrl.origin) {
588
- checkResult.result.location = appLocalRedirectLocation('/', basePath);
589
- }
590
- }
591
- catch {
592
- checkResult.result.location = appLocalRedirectLocation('/', basePath);
593
- }
594
- }
595
- }
596
- res.writeHead(200, {
597
- 'Content-Type': 'application/json',
598
- 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
599
- 'Pragma': 'no-cache',
600
- 'Expires': '0',
601
- 'Vary': 'Cookie'
602
- });
603
- res.end(JSON.stringify({
604
- result: sanitizeRouteResult(checkResult?.result || checkResult),
605
- routeId: resolvedCheck.route.route_id || '',
606
- to: targetUrl.toString()
607
- }));
608
- return;
609
- }
610
- if (url.pathname === imageEndpointPath(basePath)) {
611
- if (isStaticExportTarget) {
612
- throw new Error('not found');
613
- }
614
- await handleImageRequest(req, res, {
615
- requestUrl: url,
616
- projectRoot,
617
- config: config.images
618
- });
619
- return;
620
- }
621
- if (canonicalPath === null) {
622
- throw new Error('not found');
623
- }
624
- if (extname(canonicalPath) && extname(canonicalPath) !== '.html') {
625
- const staticPath = isStaticExportTarget
626
- ? resolveWithinDist(distDir, url.pathname)
627
- : resolveWithinDist(distDir, canonicalPath);
628
- if (!staticPath || !(await fileExists(staticPath))) {
629
- throw new Error('not found');
630
- }
631
- const content = await readFile(staticPath);
632
- const mime = MIME_TYPES[extname(staticPath)] || 'application/octet-stream';
633
- res.writeHead(200, { 'Content-Type': mime });
634
- res.end(content);
635
- return;
636
- }
637
- if (isStaticExportTarget) {
638
- const directHtmlPath = toStaticFilePath(distDir, url.pathname);
639
- if (!directHtmlPath || !(await fileExists(directHtmlPath))) {
640
- throw new Error('not found');
641
- }
642
- const html = await readFile(directHtmlPath, 'utf8');
643
- res.writeHead(200, { 'Content-Type': 'text/html' });
644
- res.end(html);
645
- return;
646
- }
647
- const canonicalUrl = new URL(url.toString());
648
- canonicalUrl.pathname = canonicalPath;
649
- const resolvedResource = resolveRequestRoute(canonicalUrl, resourceRoutes);
650
- if (resolvedResource.matched && resolvedResource.route) {
651
- const requestMethod = req.method || 'GET';
652
- const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
653
- ? null
654
- : await readRequestBodyBuffer(req);
655
- const execution = await executeServerRoute({
656
- source: resolvedResource.route.server_script || '',
657
- sourcePath: resolvedResource.route.server_script_path || '',
658
- params: resolvedResource.params,
659
- requestUrl: url.toString(),
660
- requestMethod,
661
- requestHeaders: req.headers,
662
- requestBodyBuffer,
663
- routePattern: resolvedResource.route.path,
664
- routeFile: resolvedResource.route.server_script_path || '',
665
- routeId: resolvedResource.route.route_id || routeIdFromSourcePath(resolvedResource.route.server_script_path || ''),
666
- routeKind: 'resource'
667
- });
668
- const descriptor = buildResourceResponseDescriptor(execution?.result, basePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
669
- res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
670
- if ((req.method || 'GET').toUpperCase() === 'HEAD') {
671
- res.end();
672
- return;
673
- }
674
- res.end(descriptor.body);
675
- return;
676
- }
677
- const resolved = resolveRequestRoute(canonicalUrl, pageRoutes);
678
- let htmlPath = null;
679
- if (resolved.matched && resolved.route) {
680
- if (verboseLogging) {
681
- logger.router(`${req.method || 'GET'} ${url.pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`);
682
- }
683
- const output = resolved.route.output.startsWith('/')
684
- ? resolved.route.output.slice(1)
685
- : resolved.route.output;
686
- htmlPath = resolveWithinDist(distDir, output);
687
- }
688
- else {
689
- htmlPath = toStaticFilePath(distDir, url.pathname);
690
- }
691
- if (!htmlPath || !(await fileExists(htmlPath))) {
692
- throw new Error('not found');
693
- }
694
- let ssrPayload = null;
695
- let routeExecution = null;
696
- if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
697
- try {
698
- const requestMethod = req.method || 'GET';
699
- const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
700
- ? null
701
- : await readRequestBodyBuffer(req);
702
- routeExecution = await executeServerRoute({
703
- source: resolved.route.server_script,
704
- sourcePath: resolved.route.server_script_path || '',
705
- params: resolved.params,
706
- requestUrl: url.toString(),
707
- requestMethod,
708
- requestHeaders: req.headers,
709
- requestBodyBuffer,
710
- routePattern: resolved.route.path,
711
- routeFile: resolved.route.server_script_path || '',
712
- routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
713
- });
714
- }
715
- catch (error) {
716
- logServerException('preview server route execution failed', error);
717
- ssrPayload = {
718
- __zenith_error: {
719
- status: 500,
720
- code: 'LOAD_FAILED',
721
- message: error instanceof Error ? error.message : String(error || '')
722
- }
723
- };
724
- }
725
- const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
726
- const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
727
- const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
728
- if (verboseLogging) {
729
- logger.router(`${routeId} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
730
- }
731
- const result = routeExecution?.result;
732
- if (result && result.kind === 'redirect') {
733
- const status = Number.isInteger(result.status) ? result.status : 302;
734
- res.writeHead(status, appendSetCookieHeaders({
735
- Location: appLocalRedirectLocation(result.location, basePath),
736
- 'Cache-Control': 'no-store'
737
- }, setCookies));
738
- res.end('');
739
- return;
740
- }
741
- if (result && result.kind === 'deny') {
742
- const status = Number.isInteger(result.status) ? result.status : 403;
743
- res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
744
- res.end(clientFacingRouteMessage(status, result.message));
745
- return;
746
- }
747
- if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
748
- ssrPayload = result.data;
749
- }
750
- }
751
- let html = await readFile(htmlPath, 'utf8');
752
- if (resolved.matched) {
753
- html = await materializeImageMarkup({
754
- html,
755
- payload: createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath),
756
- imageMaterialization: Array.isArray(resolved.route?.image_materialization)
757
- ? resolved.route.image_materialization
758
- : []
759
- });
760
- }
761
- if (ssrPayload) {
762
- html = injectSsrPayload(html, ssrPayload);
763
- }
764
- if (!IMAGE_RUNTIME_TAG_RE.test(html)) {
765
- html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath));
766
- }
767
- res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, appendSetCookieHeaders({
768
- 'Content-Type': 'text/html'
769
- }, Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : []));
770
- res.end(html);
771
- }
772
- catch {
773
- res.writeHead(404, { 'Content-Type': 'text/plain' });
774
- res.end('404 Not Found');
775
- }
776
- });
777
- return new Promise((resolveServer) => {
778
- server.listen(port, host, () => {
779
- actualPort = server.address().port;
780
- resolveServer({
781
- server,
782
- port: actualPort,
783
- close: () => {
784
- server.close();
785
- }
786
- });
787
- });
788
- });
789
- }
790
- /**
791
- * @typedef {{
792
- * path: string;
793
- * output: string;
794
- * server_script?: string | null;
795
- * server_script_path?: string | null;
796
- * prerender?: boolean;
797
- * route_id?: string;
798
- * pattern?: string;
799
- * params_shape?: Record<string, string>;
800
- * has_guard?: boolean;
801
- * has_load?: boolean;
802
- * guard_module_ref?: string | null;
803
- * load_module_ref?: string | null;
804
- * }} PreviewRoute
805
- */
806
- /**
807
- * @param {string} distDir
808
- * @returns {Promise<PreviewRoute[]>}
809
- */
810
- export async function loadRouteManifest(distDir) {
811
- const state = await loadRouteSurfaceState(distDir, '/');
812
- return state.pageRoutes;
813
- }
814
- export async function loadRouteSurfaceState(distDir, fallbackBasePath = '/') {
815
- const manifestPath = join(distDir, 'assets', 'router-manifest.json');
816
- const resourceState = await loadResourceRouteManifest(distDir, normalizeBasePath(fallbackBasePath || '/'));
817
- try {
818
- const source = await readFile(manifestPath, 'utf8');
819
- const parsed = JSON.parse(source);
820
- const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
821
- const basePath = normalizeBasePath(parsed?.base_path || resourceState.basePath || fallbackBasePath || '/');
822
- return {
823
- basePath,
824
- pageRoutes: routes
825
- .filter((entry) => entry &&
826
- typeof entry === 'object' &&
827
- typeof entry.path === 'string' &&
828
- typeof entry.output === 'string')
829
- .sort((a, b) => compareRouteSpecificity(a.path, b.path)),
830
- resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
831
- };
832
- }
833
- catch {
834
- return {
835
- basePath: normalizeBasePath(resourceState.basePath || fallbackBasePath || '/'),
836
- pageRoutes: [],
837
- resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
838
- };
839
- }
840
- }
841
- export const matchRoute = matchManifestRoute;
842
- /**
843
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource' }} input
844
- * @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
845
- */
846
- export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false }) {
847
- if (!source || !String(source).trim()) {
848
- return {
849
- result: { kind: 'data', data: {} },
850
- trace: { guard: 'none', action: 'none', load: 'none' }
851
- };
852
- }
853
- const payload = await spawnNodeServerRunner({
854
- source,
855
- sourcePath,
856
- params,
857
- requestUrl: requestUrl || 'http://localhost/',
858
- requestMethod: requestMethod || 'GET',
859
- requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
860
- requestBodyBuffer: Buffer.isBuffer(requestBodyBuffer) ? requestBodyBuffer : null,
861
- routePattern: routePattern || '',
862
- routeFile: routeFile || sourcePath || '',
863
- routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
864
- routeKind,
865
- guardOnly
866
- });
867
- if (payload === null || payload === undefined) {
868
- return {
869
- result: { kind: 'data', data: {} },
870
- trace: { guard: 'none', action: 'none', load: 'none' }
871
- };
872
- }
873
- if (typeof payload !== 'object' || Array.isArray(payload)) {
874
- throw new Error('[zenith-preview] server script payload must be an object');
875
- }
876
- const errorEnvelope = payload.__zenith_error;
877
- if (errorEnvelope && typeof errorEnvelope === 'object') {
878
- return {
879
- result: {
880
- kind: 'deny',
881
- status: 500,
882
- message: defaultRouteDenyMessage(500)
883
- },
884
- trace: { guard: 'none', action: 'none', load: 'deny' }
885
- };
886
- }
887
- const result = payload.result;
888
- const trace = payload.trace;
889
- if (result && typeof result === 'object' && !Array.isArray(result) && typeof result.kind === 'string') {
890
- return {
891
- result,
892
- trace: trace && typeof trace === 'object'
893
- ? {
894
- guard: String(trace.guard || 'none'),
895
- action: String(trace.action || 'none'),
896
- load: String(trace.load || 'none')
897
- }
898
- : { guard: 'none', action: 'none', load: 'none' },
899
- status: Number.isInteger(payload.status) ? payload.status : undefined,
900
- setCookies: Array.isArray(payload.setCookies)
901
- ? payload.setCookies.filter((value) => typeof value === 'string' && value.length > 0)
902
- : []
903
- };
904
- }
905
- return {
906
- result: {
907
- kind: 'data',
908
- data: payload
909
- },
910
- trace: { guard: 'none', action: 'none', load: 'data' }
911
- };
912
- }
913
- /**
914
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, routePattern?: string, routeFile?: string, routeId?: string }} input
915
- * @returns {Promise<Record<string, unknown> | null>}
916
- */
917
- export async function executeServerScript(input) {
918
- const execution = await executeServerRoute(input);
919
- const result = execution?.result;
920
- if (!result || typeof result !== 'object') {
921
- return null;
922
- }
923
- if (result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
924
- return result.data;
925
- }
926
- if (result.kind === 'redirect') {
927
- return {
928
- __zenith_error: {
929
- status: Number.isInteger(result.status) ? result.status : 302,
930
- code: 'REDIRECT',
931
- message: `Redirect to ${String(result.location || '')}`
932
- }
933
- };
934
- }
935
- if (result.kind === 'deny') {
936
- const status = Number.isInteger(result.status) ? result.status : 403;
937
- return {
938
- __zenith_error: {
939
- status,
940
- code: status >= 500 ? 'LOAD_FAILED' : (status === 404 ? 'NOT_FOUND' : 'ACCESS_DENIED'),
941
- message: clientFacingRouteMessage(status, result.message)
942
- }
943
- };
944
- }
945
- return {};
946
- }
947
- /**
948
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, requestBodyBuffer?: Buffer | null, routePattern: string, routeFile: string, routeId: string, routeKind?: 'page' | 'resource' }} input
949
- * @returns {Promise<unknown>}
950
- */
951
- function spawnNodeServerRunner(input) {
952
- return new Promise((resolvePromise, rejectPromise) => {
953
- const child = spawn(process.execPath, ['--experimental-vm-modules', '--input-type=module', '-e', SERVER_SCRIPT_RUNNER], {
954
- env: {
955
- ...process.env,
956
- ZENITH_SERVER_SOURCE: input.source,
957
- ZENITH_SERVER_SOURCE_PATH: input.sourcePath || '',
958
- ZENITH_SERVER_PARAMS: JSON.stringify(input.params || {}),
959
- ZENITH_SERVER_REQUEST_URL: input.requestUrl || 'http://localhost/',
960
- ZENITH_SERVER_REQUEST_METHOD: input.requestMethod || 'GET',
961
- ZENITH_SERVER_REQUEST_HEADERS: JSON.stringify(input.requestHeaders || {}),
962
- ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
963
- ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
964
- ZENITH_SERVER_ROUTE_ID: input.routeId || '',
965
- ZENITH_SERVER_ROUTE_KIND: input.routeKind || 'page',
966
- ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
967
- ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js'),
968
- ZENITH_SERVER_ROUTE_AUTH_PATH: join(__dirname, 'auth', 'route-auth.js')
969
- },
970
- stdio: ['pipe', 'pipe', 'pipe']
971
- });
972
- const runnerRequestBody = Buffer.isBuffer(input.requestBodyBuffer) ? input.requestBodyBuffer : null;
973
- child.stdin.on('error', () => {
974
- // ignore broken pipes when the runner exits before consuming stdin
975
- });
976
- child.stdin.end(runnerRequestBody && runnerRequestBody.length > 0 ? runnerRequestBody : undefined);
977
- let stdout = '';
978
- let stderr = '';
979
- child.stdout.on('data', (chunk) => {
980
- stdout += String(chunk);
981
- });
982
- child.stderr.on('data', (chunk) => {
983
- stderr += String(chunk);
984
- });
985
- child.on('error', (error) => {
986
- rejectPromise(error);
987
- });
988
- child.on('close', (code) => {
989
- if (code !== 0) {
990
- rejectPromise(new Error(`[zenith-preview] server script execution failed (${code}): ${stderr.trim() || stdout.trim()}`));
991
- return;
992
- }
993
- const stderrOutput = stderr.trim();
994
- const internalErrorIndex = stderrOutput.indexOf('[Zenith:Server]');
995
- if (internalErrorIndex >= 0) {
996
- console.error(stderrOutput.slice(internalErrorIndex).trim());
997
- }
998
- const raw = stdout.trim();
999
- if (!raw || raw === 'null') {
1000
- resolvePromise(null);
1001
- return;
1002
- }
1003
- try {
1004
- resolvePromise(JSON.parse(raw));
1005
- }
1006
- catch (error) {
1007
- rejectPromise(new Error(`[zenith-preview] invalid server payload JSON: ${error instanceof Error ? error.message : String(error)}`));
1008
- }
1009
- });
1010
- });
1011
- }
1012
- /**
1013
- * @param {string} html
1014
- * @param {Record<string, unknown>} payload
1015
- * @returns {string}
1016
- */
1017
- export function injectSsrPayload(html, payload) {
1018
- const serialized = serializeInlineScriptJson(payload);
1019
- const scriptTag = `<script id="zenith-ssr-data">window.__zenith_ssr_data = ${serialized};</script>`;
1020
- const existingTagRe = /<script\b[^>]*\bid=(["'])zenith-ssr-data\1[^>]*>[\s\S]*?<\/script>/i;
1021
- if (existingTagRe.test(html)) {
1022
- return html.replace(existingTagRe, scriptTag);
1023
- }
1024
- const headClose = html.match(/<\/head>/i);
1025
- if (headClose) {
1026
- return html.replace(/<\/head>/i, `${scriptTag}</head>`);
1027
- }
1028
- const bodyOpen = html.match(/<body\b[^>]*>/i);
1029
- if (bodyOpen) {
1030
- return html.replace(bodyOpen[0], `${bodyOpen[0]}${scriptTag}`);
1031
- }
1032
- return `${scriptTag}${html}`;
1033
- }
1034
- /**
1035
- * @param {Record<string, unknown>} payload
1036
- * @returns {string}
1037
- */
1038
- function serializeInlineScriptJson(payload) {
1039
- return JSON.stringify(payload)
1040
- .replace(/</g, '\\u003C')
1041
- .replace(/>/g, '\\u003E')
1042
- .replace(/\//g, '\\u002F')
1043
- .replace(/\u2028/g, '\\u2028')
1044
- .replace(/\u2029/g, '\\u2029');
1045
- }
1046
- export function toStaticFilePath(distDir, pathname) {
1047
- let resolved = pathname;
1048
- if (resolved === '/') {
1049
- resolved = '/index.html';
1050
- }
1051
- else if (!extname(resolved)) {
1052
- resolved += '/index.html';
1053
- }
1054
- return resolveWithinDist(distDir, resolved);
1055
- }
1056
- export function resolveWithinDist(distDir, requestPath) {
1057
- let decoded = requestPath;
1058
- try {
1059
- decoded = decodeURIComponent(requestPath);
1060
- }
1061
- catch {
1062
- return null;
1063
- }
1064
- const normalized = normalize(decoded).replace(/\\/g, '/');
1065
- const relative = normalized.replace(/^\/+/, '');
1066
- const root = resolve(distDir);
1067
- const candidate = resolve(root, relative);
1068
- if (candidate === root || candidate.startsWith(`${root}${sep}`)) {
1069
- return candidate;
1070
- }
1071
- return null;
1072
- }
1073
- /**
1074
- * @param {Record<string, string | string[] | undefined>} headers
1075
- * @returns {Record<string, string>}
1076
- */
1077
- function sanitizeRequestHeaders(headers) {
1078
- const out = Object.create(null);
1079
- const denyExact = new Set(['proxy-authorization', 'set-cookie']);
1080
- const denyPrefixes = ['x-forwarded-', 'cf-'];
1081
- for (const [rawKey, rawValue] of Object.entries(headers || {})) {
1082
- const key = String(rawKey || '').toLowerCase();
1083
- if (!key)
1084
- continue;
1085
- if (denyExact.has(key))
1086
- continue;
1087
- if (denyPrefixes.some((prefix) => key.startsWith(prefix)))
1088
- continue;
1089
- let value = '';
1090
- if (Array.isArray(rawValue)) {
1091
- value = rawValue.filter((entry) => entry !== undefined).map(String).join(', ');
1092
- }
1093
- else if (rawValue !== undefined) {
1094
- value = String(rawValue);
1095
- }
1096
- out[key] = value;
1097
- }
1098
- return out;
1099
- }
1100
- /**
1101
- * @param {string} sourcePath
1102
- * @returns {string}
1103
- */
1104
- function routeIdFromSourcePath(sourcePath) {
1105
- const normalized = String(sourcePath || '').replaceAll('\\', '/');
1106
- const marker = '/pages/';
1107
- const markerIndex = normalized.lastIndexOf(marker);
1108
- let routeId = markerIndex >= 0
1109
- ? normalized.slice(markerIndex + marker.length)
1110
- : normalized.split('/').pop() || normalized;
1111
- routeId = routeId.replace(/\.zen$/i, '');
1112
- if (routeId.endsWith('/index')) {
1113
- routeId = routeId.slice(0, -('/index'.length));
1114
- }
1115
- return routeId || 'index';
1116
- }
1117
- async function fileExists(fullPath) {
1118
- try {
1119
- await access(fullPath);
1120
- return true;
1121
- }
1122
- catch {
1123
- return false;
1124
- }
1125
- }
9
+ export { createPreviewServer } from './preview/create-preview-server.js';
10
+ export { loadRouteManifest, loadRouteSurfaceState, matchRoute } from './preview/manifest.js';
11
+ export { executeServerRoute, executeServerScript } from './preview/server-runner.js';
12
+ export { injectSsrPayload } from './preview/payload.js';
13
+ export { toStaticFilePath, resolveWithinDist } from './preview/paths.js';