@zenithbuild/cli 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -13
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +56 -13
- package/dist/adapters/adapter-node.js +8 -0
- package/dist/adapters/adapter-static-export.d.ts +5 -0
- package/dist/adapters/adapter-static-export.js +115 -0
- package/dist/adapters/adapter-types.d.ts +3 -1
- package/dist/adapters/adapter-types.js +5 -2
- package/dist/adapters/adapter-vercel.d.ts +1 -1
- package/dist/adapters/adapter-vercel.js +70 -13
- package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
- package/dist/adapters/copy-hosted-page-runtime.js +49 -0
- package/dist/adapters/resolve-adapter.js +4 -0
- package/dist/adapters/route-rules.d.ts +5 -0
- package/dist/adapters/route-rules.js +9 -0
- package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
- package/dist/adapters/validate-hosted-resource-routes.js +13 -0
- package/dist/auth/route-auth.d.ts +6 -0
- package/dist/auth/route-auth.js +236 -0
- package/dist/build/compiler-runtime.d.ts +10 -9
- package/dist/build/compiler-runtime.js +58 -2
- package/dist/build/compiler-signal-expression.d.ts +1 -0
- package/dist/build/compiler-signal-expression.js +155 -0
- package/dist/build/expression-rewrites.d.ts +1 -6
- package/dist/build/expression-rewrites.js +61 -65
- package/dist/build/page-component-loop.d.ts +3 -13
- package/dist/build/page-component-loop.js +21 -46
- package/dist/build/page-ir-normalization.d.ts +0 -8
- package/dist/build/page-ir-normalization.js +13 -234
- package/dist/build/page-loop-state.d.ts +6 -9
- package/dist/build/page-loop-state.js +9 -8
- package/dist/build/page-loop.js +27 -22
- package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
- package/dist/build/scoped-identifier-rewrite.js +28 -128
- package/dist/build/server-script.d.ts +3 -1
- package/dist/build/server-script.js +35 -5
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +32 -18
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +152 -55
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/framework-components/Image.zen +1 -1
- package/dist/images/materialization-plan.d.ts +1 -0
- package/dist/images/materialization-plan.js +6 -0
- package/dist/images/materialize.d.ts +5 -3
- package/dist/images/materialize.js +24 -109
- package/dist/images/router-manifest.d.ts +1 -0
- package/dist/images/router-manifest.js +49 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/index.js +8 -2
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +27 -7
- package/dist/preview.d.ts +13 -4
- package/dist/preview.js +261 -101
- package/dist/request-body.d.ts +1 -0
- package/dist/request-body.js +7 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/resource-manifest.d.ts +16 -0
- package/dist/resource-manifest.js +53 -0
- package/dist/resource-response.d.ts +34 -0
- package/dist/resource-response.js +71 -0
- package/dist/resource-route-module.d.ts +15 -0
- package/dist/resource-route-module.js +129 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +29 -6
- package/dist/server-contract.js +304 -42
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +36 -0
- package/dist/server-output.d.ts +4 -1
- package/dist/server-output.js +71 -10
- package/dist/server-runtime/node-server.js +67 -31
- package/dist/server-runtime/route-render.d.ts +27 -3
- package/dist/server-runtime/route-render.js +94 -53
- package/dist/server-script-composition.d.ts +13 -5
- package/dist/server-script-composition.js +29 -11
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +6 -3
package/dist/preview.js
CHANGED
|
@@ -14,10 +14,17 @@ import { access, readFile } from 'node:fs/promises';
|
|
|
14
14
|
import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from './base-path.js';
|
|
17
|
-
import {
|
|
17
|
+
import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
|
|
18
|
+
import { isConfigKeyExplicit, isLoadedConfig, loadConfig, validateConfig } from './config.js';
|
|
18
19
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
19
20
|
import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
|
|
20
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';
|
|
21
28
|
import { createSilentLogger } from './ui/logger.js';
|
|
22
29
|
import { compareRouteSpecificity, matchRoute as matchManifestRoute, resolveRequestRoute } from './server/resolve-request-route.js';
|
|
23
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -34,6 +41,13 @@ const MIME_TYPES = {
|
|
|
34
41
|
'.avif': 'image/avif',
|
|
35
42
|
'.gif': 'image/gif'
|
|
36
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
|
+
}
|
|
37
51
|
const SERVER_SCRIPT_RUNNER = String.raw `
|
|
38
52
|
import vm from 'node:vm';
|
|
39
53
|
import fs from 'node:fs/promises';
|
|
@@ -49,6 +63,7 @@ const requestHeaders = JSON.parse(process.env.ZENITH_SERVER_REQUEST_HEADERS || '
|
|
|
49
63
|
const routePattern = process.env.ZENITH_SERVER_ROUTE_PATTERN || '';
|
|
50
64
|
const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
|
|
51
65
|
const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
|
|
66
|
+
const routeKind = process.env.ZENITH_SERVER_ROUTE_KIND || 'page';
|
|
52
67
|
const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
|
|
53
68
|
|
|
54
69
|
if (!source.trim()) {
|
|
@@ -203,17 +218,55 @@ function ctxDeny(status = 403, message = undefined) {
|
|
|
203
218
|
message: typeof message === 'string' ? message : undefined
|
|
204
219
|
};
|
|
205
220
|
}
|
|
221
|
+
function ctxInvalid(payload, status = 400) {
|
|
222
|
+
return {
|
|
223
|
+
kind: 'invalid',
|
|
224
|
+
data: payload,
|
|
225
|
+
status: Number.isInteger(status) ? status : 400
|
|
226
|
+
};
|
|
227
|
+
}
|
|
206
228
|
function ctxData(payload) {
|
|
207
229
|
return {
|
|
208
230
|
kind: 'data',
|
|
209
231
|
data: payload
|
|
210
232
|
};
|
|
211
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
|
+
}
|
|
212
248
|
|
|
213
|
-
|
|
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 = {
|
|
214
258
|
method: requestMethod,
|
|
215
259
|
headers: new Headers(safeRequestHeaders)
|
|
216
|
-
}
|
|
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);
|
|
217
270
|
const routeParams = { ...params };
|
|
218
271
|
const routeMeta = {
|
|
219
272
|
id: routeId,
|
|
@@ -229,27 +282,28 @@ const routeContext = {
|
|
|
229
282
|
method: requestMethod,
|
|
230
283
|
route: routeMeta,
|
|
231
284
|
env: {},
|
|
232
|
-
|
|
233
|
-
async getSession(_ctx) {
|
|
234
|
-
return null;
|
|
235
|
-
},
|
|
236
|
-
async requireSession(_ctx) {
|
|
237
|
-
throw ctxRedirect('/login', 302);
|
|
238
|
-
}
|
|
239
|
-
},
|
|
285
|
+
action: null,
|
|
240
286
|
allow: ctxAllow,
|
|
241
287
|
redirect: ctxRedirect,
|
|
242
288
|
deny: ctxDeny,
|
|
243
|
-
|
|
289
|
+
invalid: ctxInvalid,
|
|
290
|
+
data: ctxData,
|
|
291
|
+
json: ctxJson,
|
|
292
|
+
text: ctxText
|
|
244
293
|
};
|
|
245
294
|
|
|
246
295
|
const context = vm.createContext({
|
|
247
296
|
params: routeParams,
|
|
248
297
|
ctx: routeContext,
|
|
249
298
|
fetch: globalThis.fetch,
|
|
299
|
+
Blob: globalThis.Blob,
|
|
300
|
+
File: globalThis.File,
|
|
301
|
+
FormData: globalThis.FormData,
|
|
250
302
|
Headers: globalThis.Headers,
|
|
251
303
|
Request: globalThis.Request,
|
|
252
304
|
Response: globalThis.Response,
|
|
305
|
+
TextEncoder: globalThis.TextEncoder,
|
|
306
|
+
TextDecoder: globalThis.TextDecoder,
|
|
253
307
|
URL,
|
|
254
308
|
URLSearchParams,
|
|
255
309
|
Buffer,
|
|
@@ -293,23 +347,31 @@ async function loadFileModule(moduleUrl) {
|
|
|
293
347
|
if (moduleCache.has(moduleUrl)) {
|
|
294
348
|
return moduleCache.get(moduleUrl);
|
|
295
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
|
+
});
|
|
296
361
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
identifier: moduleUrl,
|
|
303
|
-
initializeImportMeta(meta) {
|
|
304
|
-
meta.url = moduleUrl;
|
|
305
|
-
}
|
|
306
|
-
});
|
|
362
|
+
await module.link((specifier, referencingModule) => {
|
|
363
|
+
return linkModule(specifier, referencingModule.identifier);
|
|
364
|
+
});
|
|
365
|
+
return module;
|
|
366
|
+
})();
|
|
307
367
|
|
|
308
|
-
moduleCache.set(moduleUrl,
|
|
309
|
-
|
|
310
|
-
return
|
|
311
|
-
})
|
|
312
|
-
|
|
368
|
+
moduleCache.set(moduleUrl, modulePromise);
|
|
369
|
+
try {
|
|
370
|
+
return await modulePromise;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
moduleCache.delete(moduleUrl);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
313
375
|
}
|
|
314
376
|
|
|
315
377
|
async function linkModule(specifier, parentIdentifier) {
|
|
@@ -320,11 +382,14 @@ async function linkModule(specifier, parentIdentifier) {
|
|
|
320
382
|
return loadFileModule(resolvedUrl);
|
|
321
383
|
}
|
|
322
384
|
|
|
323
|
-
const allowed = new Set(['data', 'load', 'guard', 'ssr_data', 'props', 'ssr', 'prerender']);
|
|
385
|
+
const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender', 'exportPaths']);
|
|
324
386
|
const prelude = "const params = globalThis.params;\n" +
|
|
325
387
|
"const ctx = globalThis.ctx;\n" +
|
|
326
|
-
"import { resolveRouteResult } from 'zenith:server-contract';\n" +
|
|
327
|
-
"
|
|
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";
|
|
328
393
|
const entryIdentifier = sourcePath
|
|
329
394
|
? pathToFileURL(sourcePath).href
|
|
330
395
|
: 'zenith:server-script';
|
|
@@ -345,14 +410,35 @@ moduleCache.set(entryIdentifier, entryModule);
|
|
|
345
410
|
await entryModule.link((specifier, referencingModule) => {
|
|
346
411
|
if (specifier === 'zenith:server-contract') {
|
|
347
412
|
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
|
|
348
|
-
const
|
|
349
|
-
|
|
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(() =>
|
|
350
430
|
loadFileModule(pathToFileURL(defaultPath).href)
|
|
351
431
|
);
|
|
352
432
|
}
|
|
353
433
|
return linkModule(specifier, referencingModule.identifier);
|
|
354
434
|
});
|
|
355
435
|
await entryModule.evaluate();
|
|
436
|
+
context.attachRouteAuth(routeContext, {
|
|
437
|
+
requestUrl: routeContext.url,
|
|
438
|
+
guardOnly,
|
|
439
|
+
redirect: ctxRedirect,
|
|
440
|
+
deny: ctxDeny
|
|
441
|
+
});
|
|
356
442
|
|
|
357
443
|
const namespaceKeys = Object.keys(entryModule.namespace);
|
|
358
444
|
for (const key of namespaceKeys) {
|
|
@@ -367,18 +453,21 @@ try {
|
|
|
367
453
|
exports: exported,
|
|
368
454
|
ctx: context.ctx,
|
|
369
455
|
filePath: sourcePath || 'server_script',
|
|
370
|
-
guardOnly: guardOnly
|
|
456
|
+
guardOnly: guardOnly,
|
|
457
|
+
routeKind: routeKind
|
|
371
458
|
});
|
|
372
459
|
|
|
373
460
|
process.stdout.write(JSON.stringify(resolved || null));
|
|
374
461
|
} catch (error) {
|
|
375
|
-
const message = error instanceof 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');
|
|
376
466
|
process.stdout.write(
|
|
377
467
|
JSON.stringify({
|
|
378
468
|
__zenith_error: {
|
|
379
469
|
status: 500,
|
|
380
|
-
code: 'LOAD_FAILED'
|
|
381
|
-
message
|
|
470
|
+
code: 'LOAD_FAILED'
|
|
382
471
|
}
|
|
383
472
|
})
|
|
384
473
|
);
|
|
@@ -395,7 +484,9 @@ export async function createPreviewServer(options) {
|
|
|
395
484
|
const loadedConfig = await loadConfig(resolvedProjectRoot);
|
|
396
485
|
const resolvedConfig = options?.config && typeof options.config === 'object'
|
|
397
486
|
? (() => {
|
|
398
|
-
const overrideConfig =
|
|
487
|
+
const overrideConfig = isLoadedConfig(options.config)
|
|
488
|
+
? options.config
|
|
489
|
+
: validateConfig(options.config);
|
|
399
490
|
const mergedConfig = { ...loadedConfig };
|
|
400
491
|
for (const key of Object.keys(overrideConfig)) {
|
|
401
492
|
if (isConfigKeyExplicit(overrideConfig, key)) {
|
|
@@ -411,7 +502,15 @@ export async function createPreviewServer(options) {
|
|
|
411
502
|
const logger = providedLogger || createSilentLogger();
|
|
412
503
|
const verboseLogging = logger.mode?.logLevel === 'verbose';
|
|
413
504
|
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
505
|
+
const resolvedTarget = resolveBuildAdapter(config).target;
|
|
506
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
|
|
507
|
+
const isStaticExportTarget = resolvedTarget === 'static-export';
|
|
414
508
|
let actualPort = port;
|
|
509
|
+
const resolveServerOrigin = createTrustedOriginResolver({
|
|
510
|
+
host,
|
|
511
|
+
getPort: () => actualPort,
|
|
512
|
+
label: 'preview server'
|
|
513
|
+
});
|
|
415
514
|
async function loadImageManifest() {
|
|
416
515
|
try {
|
|
417
516
|
const manifestRaw = await readFile(join(distDir, '_zenith', 'image', 'manifest.json'), 'utf8');
|
|
@@ -422,24 +521,17 @@ export async function createPreviewServer(options) {
|
|
|
422
521
|
return {};
|
|
423
522
|
}
|
|
424
523
|
}
|
|
425
|
-
function publicHost() {
|
|
426
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
427
|
-
return '127.0.0.1';
|
|
428
|
-
}
|
|
429
|
-
return host;
|
|
430
|
-
}
|
|
431
|
-
function serverOrigin() {
|
|
432
|
-
return `http://${publicHost()}:${actualPort}`;
|
|
433
|
-
}
|
|
434
524
|
const server = createServer(async (req, res) => {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
: serverOrigin();
|
|
438
|
-
const url = new URL(req.url, requestBase);
|
|
439
|
-
const { basePath, routes } = await loadRouteManifestState(distDir, configuredBasePath);
|
|
525
|
+
const url = new URL(req.url, resolveServerOrigin());
|
|
526
|
+
const { basePath, pageRoutes, resourceRoutes } = await loadRouteSurfaceState(distDir, configuredBasePath);
|
|
440
527
|
const canonicalPath = stripBasePath(url.pathname, basePath);
|
|
441
528
|
try {
|
|
442
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
|
+
}
|
|
443
535
|
// Security: Require explicitly designated header to prevent public oracle probing
|
|
444
536
|
if (req.headers['x-zenith-route-check'] !== '1') {
|
|
445
537
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
@@ -467,7 +559,7 @@ export async function createPreviewServer(options) {
|
|
|
467
559
|
}
|
|
468
560
|
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
469
561
|
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
470
|
-
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl,
|
|
562
|
+
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, pageRoutes);
|
|
471
563
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
472
564
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
473
565
|
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
@@ -509,13 +601,16 @@ export async function createPreviewServer(options) {
|
|
|
509
601
|
'Vary': 'Cookie'
|
|
510
602
|
});
|
|
511
603
|
res.end(JSON.stringify({
|
|
512
|
-
result: checkResult?.result || checkResult,
|
|
604
|
+
result: sanitizeRouteResult(checkResult?.result || checkResult),
|
|
513
605
|
routeId: resolvedCheck.route.route_id || '',
|
|
514
606
|
to: targetUrl.toString()
|
|
515
607
|
}));
|
|
516
608
|
return;
|
|
517
609
|
}
|
|
518
610
|
if (url.pathname === imageEndpointPath(basePath)) {
|
|
611
|
+
if (isStaticExportTarget) {
|
|
612
|
+
throw new Error('not found');
|
|
613
|
+
}
|
|
519
614
|
await handleImageRequest(req, res, {
|
|
520
615
|
requestUrl: url,
|
|
521
616
|
projectRoot,
|
|
@@ -527,7 +622,9 @@ export async function createPreviewServer(options) {
|
|
|
527
622
|
throw new Error('not found');
|
|
528
623
|
}
|
|
529
624
|
if (extname(canonicalPath) && extname(canonicalPath) !== '.html') {
|
|
530
|
-
const staticPath =
|
|
625
|
+
const staticPath = isStaticExportTarget
|
|
626
|
+
? resolveWithinDist(distDir, url.pathname)
|
|
627
|
+
: resolveWithinDist(distDir, canonicalPath);
|
|
531
628
|
if (!staticPath || !(await fileExists(staticPath))) {
|
|
532
629
|
throw new Error('not found');
|
|
533
630
|
}
|
|
@@ -537,9 +634,47 @@ export async function createPreviewServer(options) {
|
|
|
537
634
|
res.end(content);
|
|
538
635
|
return;
|
|
539
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
|
+
}
|
|
540
647
|
const canonicalUrl = new URL(url.toString());
|
|
541
648
|
canonicalUrl.pathname = canonicalPath;
|
|
542
|
-
const
|
|
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);
|
|
543
678
|
let htmlPath = null;
|
|
544
679
|
if (resolved.matched && resolved.route) {
|
|
545
680
|
if (verboseLogging) {
|
|
@@ -557,48 +692,56 @@ export async function createPreviewServer(options) {
|
|
|
557
692
|
throw new Error('not found');
|
|
558
693
|
}
|
|
559
694
|
let ssrPayload = null;
|
|
695
|
+
let routeExecution = null;
|
|
560
696
|
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
561
|
-
let routeExecution = null;
|
|
562
697
|
try {
|
|
698
|
+
const requestMethod = req.method || 'GET';
|
|
699
|
+
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
700
|
+
? null
|
|
701
|
+
: await readRequestBodyBuffer(req);
|
|
563
702
|
routeExecution = await executeServerRoute({
|
|
564
703
|
source: resolved.route.server_script,
|
|
565
704
|
sourcePath: resolved.route.server_script_path || '',
|
|
566
705
|
params: resolved.params,
|
|
567
706
|
requestUrl: url.toString(),
|
|
568
|
-
requestMethod
|
|
707
|
+
requestMethod,
|
|
569
708
|
requestHeaders: req.headers,
|
|
709
|
+
requestBodyBuffer,
|
|
570
710
|
routePattern: resolved.route.path,
|
|
571
711
|
routeFile: resolved.route.server_script_path || '',
|
|
572
712
|
routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
|
|
573
713
|
});
|
|
574
714
|
}
|
|
575
715
|
catch (error) {
|
|
716
|
+
logServerException('preview server route execution failed', error);
|
|
576
717
|
ssrPayload = {
|
|
577
718
|
__zenith_error: {
|
|
719
|
+
status: 500,
|
|
578
720
|
code: 'LOAD_FAILED',
|
|
579
|
-
message: error instanceof Error ? error.message : String(error)
|
|
721
|
+
message: error instanceof Error ? error.message : String(error || '')
|
|
580
722
|
}
|
|
581
723
|
};
|
|
582
724
|
}
|
|
583
|
-
const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
|
|
725
|
+
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
584
726
|
const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
|
|
727
|
+
const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
|
|
585
728
|
if (verboseLogging) {
|
|
586
|
-
logger.router(`${routeId} guard=${trace.guard} load=${trace.load}`);
|
|
729
|
+
logger.router(`${routeId} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
587
730
|
}
|
|
588
731
|
const result = routeExecution?.result;
|
|
589
732
|
if (result && result.kind === 'redirect') {
|
|
590
733
|
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
591
|
-
res.writeHead(status, {
|
|
734
|
+
res.writeHead(status, appendSetCookieHeaders({
|
|
592
735
|
Location: appLocalRedirectLocation(result.location, basePath),
|
|
593
736
|
'Cache-Control': 'no-store'
|
|
594
|
-
});
|
|
737
|
+
}, setCookies));
|
|
595
738
|
res.end('');
|
|
596
739
|
return;
|
|
597
740
|
}
|
|
598
741
|
if (result && result.kind === 'deny') {
|
|
599
742
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
600
|
-
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
601
|
-
res.end(result.message
|
|
743
|
+
res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
|
|
744
|
+
res.end(clientFacingRouteMessage(status, result.message));
|
|
602
745
|
return;
|
|
603
746
|
}
|
|
604
747
|
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
@@ -606,21 +749,24 @@ export async function createPreviewServer(options) {
|
|
|
606
749
|
}
|
|
607
750
|
}
|
|
608
751
|
let html = await readFile(htmlPath, 'utf8');
|
|
609
|
-
if (resolved.matched
|
|
610
|
-
const pageAssetPath = resolveWithinDist(distDir, resolved.route.page_asset);
|
|
752
|
+
if (resolved.matched) {
|
|
611
753
|
html = await materializeImageMarkup({
|
|
612
754
|
html,
|
|
613
|
-
pageAssetPath,
|
|
614
755
|
payload: createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath),
|
|
615
|
-
|
|
616
|
-
|
|
756
|
+
imageMaterialization: Array.isArray(resolved.route?.image_materialization)
|
|
757
|
+
? resolved.route.image_materialization
|
|
758
|
+
: []
|
|
617
759
|
});
|
|
618
760
|
}
|
|
619
761
|
if (ssrPayload) {
|
|
620
762
|
html = injectSsrPayload(html, ssrPayload);
|
|
621
763
|
}
|
|
622
|
-
|
|
623
|
-
|
|
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 : []));
|
|
624
770
|
res.end(html);
|
|
625
771
|
}
|
|
626
772
|
catch {
|
|
@@ -662,42 +808,46 @@ export async function createPreviewServer(options) {
|
|
|
662
808
|
* @returns {Promise<PreviewRoute[]>}
|
|
663
809
|
*/
|
|
664
810
|
export async function loadRouteManifest(distDir) {
|
|
665
|
-
const state = await
|
|
666
|
-
return state.
|
|
811
|
+
const state = await loadRouteSurfaceState(distDir, '/');
|
|
812
|
+
return state.pageRoutes;
|
|
667
813
|
}
|
|
668
|
-
async function
|
|
814
|
+
export async function loadRouteSurfaceState(distDir, fallbackBasePath = '/') {
|
|
669
815
|
const manifestPath = join(distDir, 'assets', 'router-manifest.json');
|
|
816
|
+
const resourceState = await loadResourceRouteManifest(distDir, normalizeBasePath(fallbackBasePath || '/'));
|
|
670
817
|
try {
|
|
671
818
|
const source = await readFile(manifestPath, 'utf8');
|
|
672
819
|
const parsed = JSON.parse(source);
|
|
673
820
|
const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
|
|
821
|
+
const basePath = normalizeBasePath(parsed?.base_path || resourceState.basePath || fallbackBasePath || '/');
|
|
674
822
|
return {
|
|
675
|
-
basePath
|
|
676
|
-
|
|
823
|
+
basePath,
|
|
824
|
+
pageRoutes: routes
|
|
677
825
|
.filter((entry) => entry &&
|
|
678
826
|
typeof entry === 'object' &&
|
|
679
827
|
typeof entry.path === 'string' &&
|
|
680
828
|
typeof entry.output === 'string')
|
|
681
|
-
.sort((a, b) => compareRouteSpecificity(a.path, b.path))
|
|
829
|
+
.sort((a, b) => compareRouteSpecificity(a.path, b.path)),
|
|
830
|
+
resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
|
|
682
831
|
};
|
|
683
832
|
}
|
|
684
833
|
catch {
|
|
685
834
|
return {
|
|
686
|
-
basePath: normalizeBasePath(fallbackBasePath || '/'),
|
|
687
|
-
|
|
835
|
+
basePath: normalizeBasePath(resourceState.basePath || fallbackBasePath || '/'),
|
|
836
|
+
pageRoutes: [],
|
|
837
|
+
resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
|
|
688
838
|
};
|
|
689
839
|
}
|
|
690
840
|
}
|
|
691
841
|
export const matchRoute = matchManifestRoute;
|
|
692
842
|
/**
|
|
693
|
-
* @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
|
|
694
|
-
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, load: string } }>}
|
|
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[] }>}
|
|
695
845
|
*/
|
|
696
|
-
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, routePattern, routeFile, routeId, guardOnly = false }) {
|
|
846
|
+
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false }) {
|
|
697
847
|
if (!source || !String(source).trim()) {
|
|
698
848
|
return {
|
|
699
849
|
result: { kind: 'data', data: {} },
|
|
700
|
-
trace: { guard: 'none', load: 'none' }
|
|
850
|
+
trace: { guard: 'none', action: 'none', load: 'none' }
|
|
701
851
|
};
|
|
702
852
|
}
|
|
703
853
|
const payload = await spawnNodeServerRunner({
|
|
@@ -707,15 +857,17 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
707
857
|
requestUrl: requestUrl || 'http://localhost/',
|
|
708
858
|
requestMethod: requestMethod || 'GET',
|
|
709
859
|
requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
|
|
860
|
+
requestBodyBuffer: Buffer.isBuffer(requestBodyBuffer) ? requestBodyBuffer : null,
|
|
710
861
|
routePattern: routePattern || '',
|
|
711
862
|
routeFile: routeFile || sourcePath || '',
|
|
712
863
|
routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
|
|
864
|
+
routeKind,
|
|
713
865
|
guardOnly
|
|
714
866
|
});
|
|
715
867
|
if (payload === null || payload === undefined) {
|
|
716
868
|
return {
|
|
717
869
|
result: { kind: 'data', data: {} },
|
|
718
|
-
trace: { guard: 'none', load: 'none' }
|
|
870
|
+
trace: { guard: 'none', action: 'none', load: 'none' }
|
|
719
871
|
};
|
|
720
872
|
}
|
|
721
873
|
if (typeof payload !== 'object' || Array.isArray(payload)) {
|
|
@@ -727,9 +879,9 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
727
879
|
result: {
|
|
728
880
|
kind: 'deny',
|
|
729
881
|
status: 500,
|
|
730
|
-
message:
|
|
882
|
+
message: defaultRouteDenyMessage(500)
|
|
731
883
|
},
|
|
732
|
-
trace: { guard: 'none', load: 'deny' }
|
|
884
|
+
trace: { guard: 'none', action: 'none', load: 'deny' }
|
|
733
885
|
};
|
|
734
886
|
}
|
|
735
887
|
const result = payload.result;
|
|
@@ -740,9 +892,14 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
740
892
|
trace: trace && typeof trace === 'object'
|
|
741
893
|
? {
|
|
742
894
|
guard: String(trace.guard || 'none'),
|
|
895
|
+
action: String(trace.action || 'none'),
|
|
743
896
|
load: String(trace.load || 'none')
|
|
744
897
|
}
|
|
745
|
-
: { guard: 'none', load: 'none' }
|
|
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
|
+
: []
|
|
746
903
|
};
|
|
747
904
|
}
|
|
748
905
|
return {
|
|
@@ -750,18 +907,9 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
750
907
|
kind: 'data',
|
|
751
908
|
data: payload
|
|
752
909
|
},
|
|
753
|
-
trace: { guard: 'none', load: 'data' }
|
|
910
|
+
trace: { guard: 'none', action: 'none', load: 'data' }
|
|
754
911
|
};
|
|
755
912
|
}
|
|
756
|
-
export function defaultRouteDenyMessage(status) {
|
|
757
|
-
if (status === 401)
|
|
758
|
-
return 'Unauthorized';
|
|
759
|
-
if (status === 403)
|
|
760
|
-
return 'Forbidden';
|
|
761
|
-
if (status === 404)
|
|
762
|
-
return 'Not Found';
|
|
763
|
-
return 'Internal Server Error';
|
|
764
|
-
}
|
|
765
913
|
/**
|
|
766
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
|
|
767
915
|
* @returns {Promise<Record<string, unknown> | null>}
|
|
@@ -790,14 +938,14 @@ export async function executeServerScript(input) {
|
|
|
790
938
|
__zenith_error: {
|
|
791
939
|
status,
|
|
792
940
|
code: status >= 500 ? 'LOAD_FAILED' : (status === 404 ? 'NOT_FOUND' : 'ACCESS_DENIED'),
|
|
793
|
-
message:
|
|
941
|
+
message: clientFacingRouteMessage(status, result.message)
|
|
794
942
|
}
|
|
795
943
|
};
|
|
796
944
|
}
|
|
797
945
|
return {};
|
|
798
946
|
}
|
|
799
947
|
/**
|
|
800
|
-
* @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, routePattern: string, routeFile: string, routeId: string }} input
|
|
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
|
|
801
949
|
* @returns {Promise<unknown>}
|
|
802
950
|
*/
|
|
803
951
|
function spawnNodeServerRunner(input) {
|
|
@@ -814,11 +962,18 @@ function spawnNodeServerRunner(input) {
|
|
|
814
962
|
ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
|
|
815
963
|
ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
|
|
816
964
|
ZENITH_SERVER_ROUTE_ID: input.routeId || '',
|
|
965
|
+
ZENITH_SERVER_ROUTE_KIND: input.routeKind || 'page',
|
|
817
966
|
ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
|
|
818
|
-
ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js')
|
|
967
|
+
ZENITH_SERVER_CONTRACT_PATH: join(__dirname, 'server-contract.js'),
|
|
968
|
+
ZENITH_SERVER_ROUTE_AUTH_PATH: join(__dirname, 'auth', 'route-auth.js')
|
|
819
969
|
},
|
|
820
|
-
stdio: ['
|
|
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
|
|
821
975
|
});
|
|
976
|
+
child.stdin.end(runnerRequestBody && runnerRequestBody.length > 0 ? runnerRequestBody : undefined);
|
|
822
977
|
let stdout = '';
|
|
823
978
|
let stderr = '';
|
|
824
979
|
child.stdout.on('data', (chunk) => {
|
|
@@ -835,6 +990,11 @@ function spawnNodeServerRunner(input) {
|
|
|
835
990
|
rejectPromise(new Error(`[zenith-preview] server script execution failed (${code}): ${stderr.trim() || stdout.trim()}`));
|
|
836
991
|
return;
|
|
837
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
|
+
}
|
|
838
998
|
const raw = stdout.trim();
|
|
839
999
|
if (!raw || raw === 'null') {
|
|
840
1000
|
resolvePromise(null);
|