@zenithbuild/cli 0.7.4 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/adapters/adapter-netlify.d.ts +1 -1
- package/dist/adapters/adapter-netlify.js +56 -14
- 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 -14
- 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 +1 -1
- package/dist/build/compiler-runtime.js +8 -2
- package/dist/build/page-loop-state.js +1 -1
- package/dist/build/server-script.d.ts +2 -1
- package/dist/build/server-script.js +7 -3
- package/dist/build-output-manifest.d.ts +3 -2
- package/dist/build-output-manifest.js +3 -0
- package/dist/build.js +29 -17
- package/dist/dev-server.js +79 -25
- package/dist/download-result.d.ts +14 -0
- package/dist/download-result.js +148 -0
- package/dist/images/service.d.ts +13 -1
- package/dist/images/service.js +45 -15
- package/dist/manifest.d.ts +15 -1
- package/dist/manifest.js +24 -5
- package/dist/preview.d.ts +11 -3
- package/dist/preview.js +188 -62
- package/dist/request-body.d.ts +0 -1
- package/dist/request-body.js +0 -6
- 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.js +1 -1
- package/dist/server-contract.d.ts +24 -16
- package/dist/server-contract.js +217 -25
- package/dist/server-error.d.ts +1 -1
- package/dist/server-error.js +2 -0
- package/dist/server-output.d.ts +2 -1
- package/dist/server-output.js +59 -11
- package/dist/server-runtime/node-server.js +34 -4
- package/dist/server-runtime/route-render.d.ts +25 -1
- package/dist/server-runtime/route-render.js +81 -29
- package/dist/server-script-composition.d.ts +4 -2
- package/dist/server-script-composition.js +6 -3
- package/dist/static-export-paths.d.ts +3 -0
- package/dist/static-export-paths.js +160 -0
- package/package.json +3 -3
package/dist/preview.js
CHANGED
|
@@ -19,8 +19,10 @@ import { isConfigKeyExplicit, isLoadedConfig, loadConfig, validateConfig } from
|
|
|
19
19
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
20
20
|
import { createImageRuntimePayload, injectImageRuntimePayload } from './images/payload.js';
|
|
21
21
|
import { handleImageRequest } from './images/service.js';
|
|
22
|
-
import {
|
|
22
|
+
import { readRequestBodyBuffer } from './request-body.js';
|
|
23
23
|
import { createTrustedOriginResolver } from './request-origin.js';
|
|
24
|
+
import { buildResourceResponseDescriptor } from './resource-response.js';
|
|
25
|
+
import { loadResourceRouteManifest } from './resource-manifest.js';
|
|
24
26
|
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
25
27
|
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
|
|
26
28
|
import { createSilentLogger } from './ui/logger.js';
|
|
@@ -40,6 +42,12 @@ const MIME_TYPES = {
|
|
|
40
42
|
'.gif': 'image/gif'
|
|
41
43
|
};
|
|
42
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
|
+
}
|
|
43
51
|
const SERVER_SCRIPT_RUNNER = String.raw `
|
|
44
52
|
import vm from 'node:vm';
|
|
45
53
|
import fs from 'node:fs/promises';
|
|
@@ -55,6 +63,7 @@ const requestHeaders = JSON.parse(process.env.ZENITH_SERVER_REQUEST_HEADERS || '
|
|
|
55
63
|
const routePattern = process.env.ZENITH_SERVER_ROUTE_PATTERN || '';
|
|
56
64
|
const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
|
|
57
65
|
const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
|
|
66
|
+
const routeKind = process.env.ZENITH_SERVER_ROUTE_KIND || 'page';
|
|
58
67
|
const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
|
|
59
68
|
|
|
60
69
|
if (!source.trim()) {
|
|
@@ -222,14 +231,39 @@ function ctxData(payload) {
|
|
|
222
231
|
data: payload
|
|
223
232
|
};
|
|
224
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
|
+
}
|
|
225
256
|
|
|
226
257
|
const requestInit = {
|
|
227
258
|
method: requestMethod,
|
|
228
259
|
headers: new Headers(safeRequestHeaders)
|
|
229
260
|
};
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
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;
|
|
233
267
|
requestInit.duplex = 'half';
|
|
234
268
|
}
|
|
235
269
|
const requestSnapshot = new Request(requestUrl, requestInit);
|
|
@@ -249,28 +283,27 @@ const routeContext = {
|
|
|
249
283
|
route: routeMeta,
|
|
250
284
|
env: {},
|
|
251
285
|
action: null,
|
|
252
|
-
auth: {
|
|
253
|
-
async getSession(_ctx) {
|
|
254
|
-
return null;
|
|
255
|
-
},
|
|
256
|
-
async requireSession(_ctx) {
|
|
257
|
-
throw ctxRedirect('/login', 302);
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
286
|
allow: ctxAllow,
|
|
261
287
|
redirect: ctxRedirect,
|
|
262
288
|
deny: ctxDeny,
|
|
263
289
|
invalid: ctxInvalid,
|
|
264
|
-
data: ctxData
|
|
290
|
+
data: ctxData,
|
|
291
|
+
json: ctxJson,
|
|
292
|
+
text: ctxText
|
|
265
293
|
};
|
|
266
294
|
|
|
267
295
|
const context = vm.createContext({
|
|
268
296
|
params: routeParams,
|
|
269
297
|
ctx: routeContext,
|
|
270
298
|
fetch: globalThis.fetch,
|
|
299
|
+
Blob: globalThis.Blob,
|
|
300
|
+
File: globalThis.File,
|
|
301
|
+
FormData: globalThis.FormData,
|
|
271
302
|
Headers: globalThis.Headers,
|
|
272
303
|
Request: globalThis.Request,
|
|
273
304
|
Response: globalThis.Response,
|
|
305
|
+
TextEncoder: globalThis.TextEncoder,
|
|
306
|
+
TextDecoder: globalThis.TextDecoder,
|
|
274
307
|
URL,
|
|
275
308
|
URLSearchParams,
|
|
276
309
|
Buffer,
|
|
@@ -314,23 +347,31 @@ async function loadFileModule(moduleUrl) {
|
|
|
314
347
|
if (moduleCache.has(moduleUrl)) {
|
|
315
348
|
return moduleCache.get(moduleUrl);
|
|
316
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
|
+
});
|
|
317
361
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
identifier: moduleUrl,
|
|
324
|
-
initializeImportMeta(meta) {
|
|
325
|
-
meta.url = moduleUrl;
|
|
326
|
-
}
|
|
327
|
-
});
|
|
362
|
+
await module.link((specifier, referencingModule) => {
|
|
363
|
+
return linkModule(specifier, referencingModule.identifier);
|
|
364
|
+
});
|
|
365
|
+
return module;
|
|
366
|
+
})();
|
|
328
367
|
|
|
329
|
-
moduleCache.set(moduleUrl,
|
|
330
|
-
|
|
331
|
-
return
|
|
332
|
-
})
|
|
333
|
-
|
|
368
|
+
moduleCache.set(moduleUrl, modulePromise);
|
|
369
|
+
try {
|
|
370
|
+
return await modulePromise;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
moduleCache.delete(moduleUrl);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
334
375
|
}
|
|
335
376
|
|
|
336
377
|
async function linkModule(specifier, parentIdentifier) {
|
|
@@ -341,11 +382,14 @@ async function linkModule(specifier, parentIdentifier) {
|
|
|
341
382
|
return loadFileModule(resolvedUrl);
|
|
342
383
|
}
|
|
343
384
|
|
|
344
|
-
const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender']);
|
|
385
|
+
const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender', 'exportPaths']);
|
|
345
386
|
const prelude = "const params = globalThis.params;\n" +
|
|
346
387
|
"const ctx = globalThis.ctx;\n" +
|
|
347
|
-
"import { resolveRouteResult } from 'zenith:server-contract';\n" +
|
|
348
|
-
"
|
|
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";
|
|
349
393
|
const entryIdentifier = sourcePath
|
|
350
394
|
? pathToFileURL(sourcePath).href
|
|
351
395
|
: 'zenith:server-script';
|
|
@@ -366,14 +410,35 @@ moduleCache.set(entryIdentifier, entryModule);
|
|
|
366
410
|
await entryModule.link((specifier, referencingModule) => {
|
|
367
411
|
if (specifier === 'zenith:server-contract') {
|
|
368
412
|
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
|
|
369
|
-
const
|
|
370
|
-
|
|
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(() =>
|
|
371
430
|
loadFileModule(pathToFileURL(defaultPath).href)
|
|
372
431
|
);
|
|
373
432
|
}
|
|
374
433
|
return linkModule(specifier, referencingModule.identifier);
|
|
375
434
|
});
|
|
376
435
|
await entryModule.evaluate();
|
|
436
|
+
context.attachRouteAuth(routeContext, {
|
|
437
|
+
requestUrl: routeContext.url,
|
|
438
|
+
guardOnly,
|
|
439
|
+
redirect: ctxRedirect,
|
|
440
|
+
deny: ctxDeny
|
|
441
|
+
});
|
|
377
442
|
|
|
378
443
|
const namespaceKeys = Object.keys(entryModule.namespace);
|
|
379
444
|
for (const key of namespaceKeys) {
|
|
@@ -388,7 +453,8 @@ try {
|
|
|
388
453
|
exports: exported,
|
|
389
454
|
ctx: context.ctx,
|
|
390
455
|
filePath: sourcePath || 'server_script',
|
|
391
|
-
guardOnly: guardOnly
|
|
456
|
+
guardOnly: guardOnly,
|
|
457
|
+
routeKind: routeKind
|
|
392
458
|
});
|
|
393
459
|
|
|
394
460
|
process.stdout.write(JSON.stringify(resolved || null));
|
|
@@ -436,7 +502,9 @@ export async function createPreviewServer(options) {
|
|
|
436
502
|
const logger = providedLogger || createSilentLogger();
|
|
437
503
|
const verboseLogging = logger.mode?.logLevel === 'verbose';
|
|
438
504
|
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
439
|
-
const
|
|
505
|
+
const resolvedTarget = resolveBuildAdapter(config).target;
|
|
506
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
|
|
507
|
+
const isStaticExportTarget = resolvedTarget === 'static-export';
|
|
440
508
|
let actualPort = port;
|
|
441
509
|
const resolveServerOrigin = createTrustedOriginResolver({
|
|
442
510
|
host,
|
|
@@ -455,7 +523,7 @@ export async function createPreviewServer(options) {
|
|
|
455
523
|
}
|
|
456
524
|
const server = createServer(async (req, res) => {
|
|
457
525
|
const url = new URL(req.url, resolveServerOrigin());
|
|
458
|
-
const { basePath,
|
|
526
|
+
const { basePath, pageRoutes, resourceRoutes } = await loadRouteSurfaceState(distDir, configuredBasePath);
|
|
459
527
|
const canonicalPath = stripBasePath(url.pathname, basePath);
|
|
460
528
|
try {
|
|
461
529
|
if (url.pathname === routeCheckPath(basePath)) {
|
|
@@ -491,7 +559,7 @@ export async function createPreviewServer(options) {
|
|
|
491
559
|
}
|
|
492
560
|
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
493
561
|
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
494
|
-
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl,
|
|
562
|
+
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, pageRoutes);
|
|
495
563
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
496
564
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
497
565
|
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
@@ -540,6 +608,9 @@ export async function createPreviewServer(options) {
|
|
|
540
608
|
return;
|
|
541
609
|
}
|
|
542
610
|
if (url.pathname === imageEndpointPath(basePath)) {
|
|
611
|
+
if (isStaticExportTarget) {
|
|
612
|
+
throw new Error('not found');
|
|
613
|
+
}
|
|
543
614
|
await handleImageRequest(req, res, {
|
|
544
615
|
requestUrl: url,
|
|
545
616
|
projectRoot,
|
|
@@ -551,7 +622,9 @@ export async function createPreviewServer(options) {
|
|
|
551
622
|
throw new Error('not found');
|
|
552
623
|
}
|
|
553
624
|
if (extname(canonicalPath) && extname(canonicalPath) !== '.html') {
|
|
554
|
-
const staticPath =
|
|
625
|
+
const staticPath = isStaticExportTarget
|
|
626
|
+
? resolveWithinDist(distDir, url.pathname)
|
|
627
|
+
: resolveWithinDist(distDir, canonicalPath);
|
|
555
628
|
if (!staticPath || !(await fileExists(staticPath))) {
|
|
556
629
|
throw new Error('not found');
|
|
557
630
|
}
|
|
@@ -561,9 +634,47 @@ export async function createPreviewServer(options) {
|
|
|
561
634
|
res.end(content);
|
|
562
635
|
return;
|
|
563
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
|
+
}
|
|
564
647
|
const canonicalUrl = new URL(url.toString());
|
|
565
648
|
canonicalUrl.pathname = canonicalPath;
|
|
566
|
-
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);
|
|
567
678
|
let htmlPath = null;
|
|
568
679
|
if (resolved.matched && resolved.route) {
|
|
569
680
|
if (verboseLogging) {
|
|
@@ -595,7 +706,7 @@ export async function createPreviewServer(options) {
|
|
|
595
706
|
requestUrl: url.toString(),
|
|
596
707
|
requestMethod,
|
|
597
708
|
requestHeaders: req.headers,
|
|
598
|
-
|
|
709
|
+
requestBodyBuffer,
|
|
599
710
|
routePattern: resolved.route.path,
|
|
600
711
|
routeFile: resolved.route.server_script_path || '',
|
|
601
712
|
routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
|
|
@@ -613,22 +724,23 @@ export async function createPreviewServer(options) {
|
|
|
613
724
|
}
|
|
614
725
|
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
615
726
|
const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
|
|
727
|
+
const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
|
|
616
728
|
if (verboseLogging) {
|
|
617
729
|
logger.router(`${routeId} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
618
730
|
}
|
|
619
731
|
const result = routeExecution?.result;
|
|
620
732
|
if (result && result.kind === 'redirect') {
|
|
621
733
|
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
622
|
-
res.writeHead(status, {
|
|
734
|
+
res.writeHead(status, appendSetCookieHeaders({
|
|
623
735
|
Location: appLocalRedirectLocation(result.location, basePath),
|
|
624
736
|
'Cache-Control': 'no-store'
|
|
625
|
-
});
|
|
737
|
+
}, setCookies));
|
|
626
738
|
res.end('');
|
|
627
739
|
return;
|
|
628
740
|
}
|
|
629
741
|
if (result && result.kind === 'deny') {
|
|
630
742
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
631
|
-
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
743
|
+
res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
|
|
632
744
|
res.end(clientFacingRouteMessage(status, result.message));
|
|
633
745
|
return;
|
|
634
746
|
}
|
|
@@ -652,9 +764,9 @@ export async function createPreviewServer(options) {
|
|
|
652
764
|
if (!IMAGE_RUNTIME_TAG_RE.test(html)) {
|
|
653
765
|
html = injectImageRuntimePayload(html, createImageRuntimePayload(config.images, await loadImageManifest(), 'endpoint', basePath));
|
|
654
766
|
}
|
|
655
|
-
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, {
|
|
767
|
+
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, appendSetCookieHeaders({
|
|
656
768
|
'Content-Type': 'text/html'
|
|
657
|
-
});
|
|
769
|
+
}, Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : []));
|
|
658
770
|
res.end(html);
|
|
659
771
|
}
|
|
660
772
|
catch {
|
|
@@ -696,38 +808,42 @@ export async function createPreviewServer(options) {
|
|
|
696
808
|
* @returns {Promise<PreviewRoute[]>}
|
|
697
809
|
*/
|
|
698
810
|
export async function loadRouteManifest(distDir) {
|
|
699
|
-
const state = await
|
|
700
|
-
return state.
|
|
811
|
+
const state = await loadRouteSurfaceState(distDir, '/');
|
|
812
|
+
return state.pageRoutes;
|
|
701
813
|
}
|
|
702
|
-
async function
|
|
814
|
+
export async function loadRouteSurfaceState(distDir, fallbackBasePath = '/') {
|
|
703
815
|
const manifestPath = join(distDir, 'assets', 'router-manifest.json');
|
|
816
|
+
const resourceState = await loadResourceRouteManifest(distDir, normalizeBasePath(fallbackBasePath || '/'));
|
|
704
817
|
try {
|
|
705
818
|
const source = await readFile(manifestPath, 'utf8');
|
|
706
819
|
const parsed = JSON.parse(source);
|
|
707
820
|
const routes = Array.isArray(parsed?.routes) ? parsed.routes : [];
|
|
821
|
+
const basePath = normalizeBasePath(parsed?.base_path || resourceState.basePath || fallbackBasePath || '/');
|
|
708
822
|
return {
|
|
709
|
-
basePath
|
|
710
|
-
|
|
823
|
+
basePath,
|
|
824
|
+
pageRoutes: routes
|
|
711
825
|
.filter((entry) => entry &&
|
|
712
826
|
typeof entry === 'object' &&
|
|
713
827
|
typeof entry.path === 'string' &&
|
|
714
828
|
typeof entry.output === 'string')
|
|
715
|
-
.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 : []
|
|
716
831
|
};
|
|
717
832
|
}
|
|
718
833
|
catch {
|
|
719
834
|
return {
|
|
720
|
-
basePath: normalizeBasePath(fallbackBasePath || '/'),
|
|
721
|
-
|
|
835
|
+
basePath: normalizeBasePath(resourceState.basePath || fallbackBasePath || '/'),
|
|
836
|
+
pageRoutes: [],
|
|
837
|
+
resourceRoutes: Array.isArray(resourceState.routes) ? resourceState.routes : []
|
|
722
838
|
};
|
|
723
839
|
}
|
|
724
840
|
}
|
|
725
841
|
export const matchRoute = matchManifestRoute;
|
|
726
842
|
/**
|
|
727
|
-
* @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
|
|
728
|
-
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
|
|
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[] }>}
|
|
729
845
|
*/
|
|
730
|
-
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders,
|
|
846
|
+
export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false }) {
|
|
731
847
|
if (!source || !String(source).trim()) {
|
|
732
848
|
return {
|
|
733
849
|
result: { kind: 'data', data: {} },
|
|
@@ -741,10 +857,11 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
741
857
|
requestUrl: requestUrl || 'http://localhost/',
|
|
742
858
|
requestMethod: requestMethod || 'GET',
|
|
743
859
|
requestHeaders: sanitizeRequestHeaders(requestHeaders || {}),
|
|
744
|
-
|
|
860
|
+
requestBodyBuffer: Buffer.isBuffer(requestBodyBuffer) ? requestBodyBuffer : null,
|
|
745
861
|
routePattern: routePattern || '',
|
|
746
862
|
routeFile: routeFile || sourcePath || '',
|
|
747
863
|
routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
|
|
864
|
+
routeKind,
|
|
748
865
|
guardOnly
|
|
749
866
|
});
|
|
750
867
|
if (payload === null || payload === undefined) {
|
|
@@ -779,7 +896,10 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
|
|
|
779
896
|
load: String(trace.load || 'none')
|
|
780
897
|
}
|
|
781
898
|
: { guard: 'none', action: 'none', load: 'none' },
|
|
782
|
-
status: Number.isInteger(payload.status) ? payload.status : undefined
|
|
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
|
+
: []
|
|
783
903
|
};
|
|
784
904
|
}
|
|
785
905
|
return {
|
|
@@ -825,7 +945,7 @@ export async function executeServerScript(input) {
|
|
|
825
945
|
return {};
|
|
826
946
|
}
|
|
827
947
|
/**
|
|
828
|
-
* @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
|
|
829
949
|
* @returns {Promise<unknown>}
|
|
830
950
|
*/
|
|
831
951
|
function spawnNodeServerRunner(input) {
|
|
@@ -839,15 +959,21 @@ function spawnNodeServerRunner(input) {
|
|
|
839
959
|
ZENITH_SERVER_REQUEST_URL: input.requestUrl || 'http://localhost/',
|
|
840
960
|
ZENITH_SERVER_REQUEST_METHOD: input.requestMethod || 'GET',
|
|
841
961
|
ZENITH_SERVER_REQUEST_HEADERS: JSON.stringify(input.requestHeaders || {}),
|
|
842
|
-
ZENITH_SERVER_REQUEST_BODY_BASE64: input.requestBodyBase64 || '',
|
|
843
962
|
ZENITH_SERVER_ROUTE_PATTERN: input.routePattern || '',
|
|
844
963
|
ZENITH_SERVER_ROUTE_FILE: input.routeFile || input.sourcePath || '',
|
|
845
964
|
ZENITH_SERVER_ROUTE_ID: input.routeId || '',
|
|
965
|
+
ZENITH_SERVER_ROUTE_KIND: input.routeKind || 'page',
|
|
846
966
|
ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
|
|
847
|
-
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')
|
|
848
969
|
},
|
|
849
|
-
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
|
|
850
975
|
});
|
|
976
|
+
child.stdin.end(runnerRequestBody && runnerRequestBody.length > 0 ? runnerRequestBody : undefined);
|
|
851
977
|
let stdout = '';
|
|
852
978
|
let stderr = '';
|
|
853
979
|
child.stdout.on('data', (chunk) => {
|
package/dist/request-body.d.ts
CHANGED
package/dist/request-body.js
CHANGED
|
@@ -5,9 +5,3 @@ export async function readRequestBodyBuffer(req) {
|
|
|
5
5
|
}
|
|
6
6
|
return Buffer.concat(chunks);
|
|
7
7
|
}
|
|
8
|
-
export function encodeRequestBodyBase64(bodyBuffer) {
|
|
9
|
-
if (!Buffer.isBuffer(bodyBuffer) || bodyBuffer.length === 0) {
|
|
10
|
-
return '';
|
|
11
|
-
}
|
|
12
|
-
return bodyBuffer.toString('base64');
|
|
13
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function writeResourceRouteManifest(staticDir: any, routeManifest: any, basePath?: string): Promise<({
|
|
2
|
+
path: any;
|
|
3
|
+
file: any;
|
|
4
|
+
route_kind: string;
|
|
5
|
+
server_script: any;
|
|
6
|
+
server_script_path: any;
|
|
7
|
+
has_guard: boolean;
|
|
8
|
+
has_load: boolean;
|
|
9
|
+
has_action: boolean;
|
|
10
|
+
params: any;
|
|
11
|
+
route_id: any;
|
|
12
|
+
} | null)[]>;
|
|
13
|
+
export function loadResourceRouteManifest(distDir: any, fallbackBasePath?: string): Promise<{
|
|
14
|
+
basePath: any;
|
|
15
|
+
routes: any;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { compareRouteSpecificity } from './server/resolve-request-route.js';
|
|
4
|
+
function sanitizeResourceRoute(entry) {
|
|
5
|
+
if (!entry || typeof entry !== 'object') {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
if (typeof entry.path !== 'string' || typeof entry.server_script !== 'string') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
path: entry.path,
|
|
13
|
+
file: typeof entry.file === 'string' ? entry.file : '',
|
|
14
|
+
route_kind: 'resource',
|
|
15
|
+
server_script: entry.server_script,
|
|
16
|
+
server_script_path: typeof entry.server_script_path === 'string' ? entry.server_script_path : '',
|
|
17
|
+
has_guard: entry.has_guard === true,
|
|
18
|
+
has_load: entry.has_load === true,
|
|
19
|
+
has_action: entry.has_action === true,
|
|
20
|
+
params: Array.isArray(entry.params) ? entry.params.filter((value) => typeof value === 'string') : [],
|
|
21
|
+
route_id: typeof entry.route_id === 'string' ? entry.route_id : null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function writeResourceRouteManifest(staticDir, routeManifest, basePath = '/') {
|
|
25
|
+
const routes = (Array.isArray(routeManifest) ? routeManifest : [])
|
|
26
|
+
.filter((entry) => entry?.route_kind === 'resource')
|
|
27
|
+
.map((entry) => sanitizeResourceRoute(entry))
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.sort((left, right) => compareRouteSpecificity(left.path, right.path));
|
|
30
|
+
const manifestPath = join(staticDir, 'assets', 'resource-manifest.json');
|
|
31
|
+
await mkdir(join(staticDir, 'assets'), { recursive: true });
|
|
32
|
+
await writeFile(manifestPath, `${JSON.stringify({ base_path: basePath, routes }, null, 2)}\n`, 'utf8');
|
|
33
|
+
return routes;
|
|
34
|
+
}
|
|
35
|
+
export async function loadResourceRouteManifest(distDir, fallbackBasePath = '/') {
|
|
36
|
+
const manifestPath = join(distDir, 'assets', 'resource-manifest.json');
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
39
|
+
return {
|
|
40
|
+
basePath: typeof parsed?.base_path === 'string' ? parsed.base_path : fallbackBasePath,
|
|
41
|
+
routes: (Array.isArray(parsed?.routes) ? parsed.routes : [])
|
|
42
|
+
.map((entry) => sanitizeResourceRoute(entry))
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.sort((left, right) => compareRouteSpecificity(left.path, right.path))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return {
|
|
49
|
+
basePath: fallbackBasePath,
|
|
50
|
+
routes: []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function buildResourceResponseDescriptor(result: any, basePath?: string, setCookies?: any[]): {
|
|
2
|
+
status: any;
|
|
3
|
+
headers: {
|
|
4
|
+
Location: string;
|
|
5
|
+
'Cache-Control': string;
|
|
6
|
+
'Content-Type'?: undefined;
|
|
7
|
+
'Content-Disposition'?: undefined;
|
|
8
|
+
'Content-Length'?: undefined;
|
|
9
|
+
};
|
|
10
|
+
body: string;
|
|
11
|
+
setCookies: any[];
|
|
12
|
+
} | {
|
|
13
|
+
status: any;
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': string;
|
|
16
|
+
Location?: undefined;
|
|
17
|
+
'Cache-Control'?: undefined;
|
|
18
|
+
'Content-Disposition'?: undefined;
|
|
19
|
+
'Content-Length'?: undefined;
|
|
20
|
+
};
|
|
21
|
+
body: any;
|
|
22
|
+
setCookies: any[];
|
|
23
|
+
} | {
|
|
24
|
+
status: number;
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': any;
|
|
27
|
+
'Content-Disposition': string;
|
|
28
|
+
'Content-Length': string;
|
|
29
|
+
Location?: undefined;
|
|
30
|
+
'Cache-Control'?: undefined;
|
|
31
|
+
};
|
|
32
|
+
body: Buffer<ArrayBuffer>;
|
|
33
|
+
setCookies: any[];
|
|
34
|
+
};
|