@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/dev-server.js
CHANGED
|
@@ -22,10 +22,11 @@ import { createStartupProfiler } from './startup-profile.js';
|
|
|
22
22
|
import { createSilentLogger } from './ui/logger.js';
|
|
23
23
|
import { readChangeFingerprint } from './dev-watch.js';
|
|
24
24
|
import { createTrustedOriginResolver, publicHost } from './request-origin.js';
|
|
25
|
-
import {
|
|
25
|
+
import { readRequestBodyBuffer } from './request-body.js';
|
|
26
|
+
import { buildResourceResponseDescriptor } from './resource-response.js';
|
|
26
27
|
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
27
28
|
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
|
|
28
|
-
import { executeServerRoute, injectSsrPayload,
|
|
29
|
+
import { executeServerRoute, injectSsrPayload, loadRouteSurfaceState, resolveWithinDist, toStaticFilePath } from './preview.js';
|
|
29
30
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
30
31
|
import { injectImageRuntimePayload } from './images/payload.js';
|
|
31
32
|
import { handleImageRequest } from './images/service.js';
|
|
@@ -46,6 +47,12 @@ const MIME_TYPES = {
|
|
|
46
47
|
const IMAGE_RUNTIME_TAG_RE = new RegExp('<' + 'script\\b[^>]*\\bid=(["\'])zenith-image-runtime\\1[^>]*>[\\s\\S]*?<\\/' + 'script>', 'i');
|
|
47
48
|
const EVENT_STREAM_MIME = ['text', 'event-stream'].join('/');
|
|
48
49
|
const LEGACY_DEV_STREAM_PATH = ['/__zenith', '_hmr'].join('');
|
|
50
|
+
function appendSetCookieHeaders(headers, setCookies = []) {
|
|
51
|
+
if (Array.isArray(setCookies) && setCookies.length > 0) {
|
|
52
|
+
headers['Set-Cookie'] = setCookies.slice();
|
|
53
|
+
}
|
|
54
|
+
return headers;
|
|
55
|
+
}
|
|
49
56
|
// Note: V0 HMR script injection has been moved to the runtime client.
|
|
50
57
|
// This server purely hosts the V1 HMR contract endpoints.
|
|
51
58
|
/**
|
|
@@ -60,7 +67,9 @@ export async function createDevServer(options) {
|
|
|
60
67
|
const logger = providedLogger || createSilentLogger();
|
|
61
68
|
const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
|
|
62
69
|
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
63
|
-
const
|
|
70
|
+
const resolvedTarget = resolveBuildAdapter(config).target;
|
|
71
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
|
|
72
|
+
const isStaticExportTarget = resolvedTarget === 'static-export';
|
|
64
73
|
const resolvedPagesDir = resolve(pagesDir);
|
|
65
74
|
const resolvedOutDir = resolve(outDir);
|
|
66
75
|
const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
|
|
@@ -102,7 +111,7 @@ export async function createDevServer(options) {
|
|
|
102
111
|
getPort: () => actualPort,
|
|
103
112
|
label: 'dev server'
|
|
104
113
|
});
|
|
105
|
-
let
|
|
114
|
+
let currentRouteState = { pageRoutes: [], resourceRoutes: [] };
|
|
106
115
|
const rebuildDebounceMs = 5;
|
|
107
116
|
const queuedRebuildDebounceMs = 5;
|
|
108
117
|
function _publicHost() {
|
|
@@ -371,22 +380,26 @@ export async function createDevServer(options) {
|
|
|
371
380
|
return true;
|
|
372
381
|
}
|
|
373
382
|
async function _loadRoutesForRequests() {
|
|
374
|
-
if (buildStatus === 'building' &&
|
|
375
|
-
|
|
383
|
+
if (buildStatus === 'building' &&
|
|
384
|
+
((Array.isArray(currentRouteState.pageRoutes) && currentRouteState.pageRoutes.length > 0) ||
|
|
385
|
+
(Array.isArray(currentRouteState.resourceRoutes) && currentRouteState.resourceRoutes.length > 0))) {
|
|
386
|
+
return currentRouteState;
|
|
376
387
|
}
|
|
377
388
|
try {
|
|
378
|
-
const
|
|
379
|
-
if (Array.isArray(
|
|
380
|
-
|
|
381
|
-
|
|
389
|
+
const routeState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
390
|
+
if ((Array.isArray(routeState.pageRoutes) && routeState.pageRoutes.length > 0) ||
|
|
391
|
+
(Array.isArray(routeState.resourceRoutes) && routeState.resourceRoutes.length > 0)) {
|
|
392
|
+
currentRouteState = routeState;
|
|
393
|
+
return routeState;
|
|
382
394
|
}
|
|
383
395
|
}
|
|
384
396
|
catch (error) {
|
|
385
|
-
if (!(Array.isArray(
|
|
397
|
+
if (!(Array.isArray(currentRouteState.pageRoutes) && currentRouteState.pageRoutes.length > 0) &&
|
|
398
|
+
!(Array.isArray(currentRouteState.resourceRoutes) && currentRouteState.resourceRoutes.length > 0)) {
|
|
386
399
|
throw error;
|
|
387
400
|
}
|
|
388
401
|
}
|
|
389
|
-
return
|
|
402
|
+
return currentRouteState;
|
|
390
403
|
}
|
|
391
404
|
function _broadcastEvent(type, payload = {}) {
|
|
392
405
|
const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : buildId;
|
|
@@ -419,7 +432,7 @@ export async function createDevServer(options) {
|
|
|
419
432
|
logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
|
|
420
433
|
const initialBuild = await buildSession.build();
|
|
421
434
|
const cssReady = await _syncCssStateFromBuild(initialBuild, buildId);
|
|
422
|
-
|
|
435
|
+
currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
423
436
|
buildStatus = 'ok';
|
|
424
437
|
buildError = null;
|
|
425
438
|
lastBuildMs = Date.now();
|
|
@@ -438,7 +451,8 @@ export async function createDevServer(options) {
|
|
|
438
451
|
status: buildStatus,
|
|
439
452
|
durationMs,
|
|
440
453
|
cssReady,
|
|
441
|
-
routes: Array.isArray(
|
|
454
|
+
routes: (Array.isArray(currentRouteState.pageRoutes) ? currentRouteState.pageRoutes.length : 0) +
|
|
455
|
+
(Array.isArray(currentRouteState.resourceRoutes) ? currentRouteState.resourceRoutes.length : 0)
|
|
442
456
|
});
|
|
443
457
|
}
|
|
444
458
|
catch (err) {
|
|
@@ -579,6 +593,9 @@ export async function createDevServer(options) {
|
|
|
579
593
|
return;
|
|
580
594
|
}
|
|
581
595
|
if (pathname === imageEndpointPath(configuredBasePath)) {
|
|
596
|
+
if (isStaticExportTarget) {
|
|
597
|
+
throw new Error('not found');
|
|
598
|
+
}
|
|
582
599
|
await handleImageRequest(req, res, {
|
|
583
600
|
requestUrl: url,
|
|
584
601
|
projectRoot,
|
|
@@ -629,7 +646,7 @@ export async function createDevServer(options) {
|
|
|
629
646
|
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
630
647
|
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
631
648
|
const routes = await _loadRoutesForRequests();
|
|
632
|
-
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes);
|
|
649
|
+
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes.pageRoutes || []);
|
|
633
650
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
634
651
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
635
652
|
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
@@ -722,9 +739,14 @@ export async function createDevServer(options) {
|
|
|
722
739
|
}
|
|
723
740
|
const requestExt = extname(canonicalPath);
|
|
724
741
|
if (requestExt && requestExt !== '.html') {
|
|
725
|
-
const assetPath =
|
|
742
|
+
const assetPath = isStaticExportTarget
|
|
743
|
+
? resolveWithinDist(outDir, pathname)
|
|
744
|
+
: join(outDir, canonicalPath);
|
|
726
745
|
resolvedPathFor404 = assetPath;
|
|
727
746
|
staticRootFor404 = outDir;
|
|
747
|
+
if (!assetPath) {
|
|
748
|
+
throw new Error('not found');
|
|
749
|
+
}
|
|
728
750
|
const asset = await _readFileForRequest(assetPath);
|
|
729
751
|
const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
|
|
730
752
|
res.writeHead(200, { 'Content-Type': mime });
|
|
@@ -734,9 +756,40 @@ export async function createDevServer(options) {
|
|
|
734
756
|
const routes = await _loadRoutesForRequests();
|
|
735
757
|
const canonicalUrl = new URL(url.toString());
|
|
736
758
|
canonicalUrl.pathname = canonicalPath;
|
|
737
|
-
const
|
|
759
|
+
const resolvedResource = resolveRequestRoute(canonicalUrl, routes.resourceRoutes || []);
|
|
760
|
+
if (resolvedResource.matched && resolvedResource.route) {
|
|
761
|
+
const requestMethod = req.method || 'GET';
|
|
762
|
+
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
763
|
+
? null
|
|
764
|
+
: await readRequestBodyBuffer(req);
|
|
765
|
+
const execution = await executeServerRoute({
|
|
766
|
+
source: resolvedResource.route.server_script || '',
|
|
767
|
+
sourcePath: resolvedResource.route.server_script_path || '',
|
|
768
|
+
params: resolvedResource.params,
|
|
769
|
+
requestUrl: url.toString(),
|
|
770
|
+
requestMethod,
|
|
771
|
+
requestHeaders: req.headers,
|
|
772
|
+
requestBodyBuffer,
|
|
773
|
+
routePattern: resolvedResource.route.path,
|
|
774
|
+
routeFile: resolvedResource.route.server_script_path || '',
|
|
775
|
+
routeId: resolvedResource.route.route_id || '',
|
|
776
|
+
routeKind: 'resource'
|
|
777
|
+
});
|
|
778
|
+
const descriptor = buildResourceResponseDescriptor(execution?.result, configuredBasePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
|
|
779
|
+
res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
|
|
780
|
+
if ((req.method || 'GET').toUpperCase() === 'HEAD') {
|
|
781
|
+
res.end();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
res.end(descriptor.body);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const resolved = resolveRequestRoute(canonicalUrl, routes.pageRoutes || []);
|
|
738
788
|
let filePath = null;
|
|
739
|
-
if (
|
|
789
|
+
if (isStaticExportTarget) {
|
|
790
|
+
filePath = toStaticFilePath(outDir, pathname);
|
|
791
|
+
}
|
|
792
|
+
else if (resolved.matched && resolved.route) {
|
|
740
793
|
if (verboseLogging) {
|
|
741
794
|
logger.router(`${req.method || 'GET'} ${pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`);
|
|
742
795
|
}
|
|
@@ -768,7 +821,7 @@ export async function createDevServer(options) {
|
|
|
768
821
|
requestUrl: url.toString(),
|
|
769
822
|
requestMethod,
|
|
770
823
|
requestHeaders: req.headers,
|
|
771
|
-
|
|
824
|
+
requestBodyBuffer,
|
|
772
825
|
routePattern: resolved.route.path,
|
|
773
826
|
routeFile: resolved.route.server_script_path || '',
|
|
774
827
|
routeId: resolved.route.route_id || ''
|
|
@@ -786,22 +839,23 @@ export async function createDevServer(options) {
|
|
|
786
839
|
}
|
|
787
840
|
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
788
841
|
const routeId = resolved.route.route_id || '';
|
|
842
|
+
const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
|
|
789
843
|
if (verboseLogging) {
|
|
790
844
|
logger.router(`${routeId || resolved.route.path} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
791
845
|
}
|
|
792
846
|
const result = routeExecution?.result;
|
|
793
847
|
if (result && result.kind === 'redirect') {
|
|
794
848
|
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
795
|
-
res.writeHead(status, {
|
|
849
|
+
res.writeHead(status, appendSetCookieHeaders({
|
|
796
850
|
Location: appLocalRedirectLocation(result.location, configuredBasePath),
|
|
797
851
|
'Cache-Control': 'no-store'
|
|
798
|
-
});
|
|
852
|
+
}, setCookies));
|
|
799
853
|
res.end('');
|
|
800
854
|
return;
|
|
801
855
|
}
|
|
802
856
|
if (result && result.kind === 'deny') {
|
|
803
857
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
804
|
-
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
858
|
+
res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
|
|
805
859
|
res.end(clientFacingRouteMessage(status, result.message));
|
|
806
860
|
return;
|
|
807
861
|
}
|
|
@@ -825,9 +879,9 @@ export async function createDevServer(options) {
|
|
|
825
879
|
if (!IMAGE_RUNTIME_TAG_RE.test(content)) {
|
|
826
880
|
content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
|
|
827
881
|
}
|
|
828
|
-
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, {
|
|
882
|
+
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, appendSetCookieHeaders({
|
|
829
883
|
'Content-Type': 'text/html'
|
|
830
|
-
});
|
|
884
|
+
}, Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : []));
|
|
831
885
|
res.end(content);
|
|
832
886
|
}
|
|
833
887
|
catch (error) {
|
|
@@ -950,7 +1004,7 @@ export async function createDevServer(options) {
|
|
|
950
1004
|
const buildResult = await buildSession.build({ changedFiles, logger });
|
|
951
1005
|
const cssReady = await _syncCssStateFromBuild(buildResult, cycleBuildId);
|
|
952
1006
|
if (!onlyCss) {
|
|
953
|
-
|
|
1007
|
+
currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
954
1008
|
}
|
|
955
1009
|
const cssChanged = cssReady && (currentCssAssetPath !== previousCssAssetPath ||
|
|
956
1010
|
currentCssContent !== previousCssContent);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function buildAttachmentContentDisposition(filename: any): string;
|
|
2
|
+
export function createDownloadResult(body: any, options?: {}): {
|
|
3
|
+
kind: string;
|
|
4
|
+
body: any;
|
|
5
|
+
bodyEncoding: string;
|
|
6
|
+
bodySize: number;
|
|
7
|
+
filename: string;
|
|
8
|
+
contentType: string;
|
|
9
|
+
status: number;
|
|
10
|
+
};
|
|
11
|
+
export function assertValidDownloadResult(value: any, where?: string): void;
|
|
12
|
+
export function decodeDownloadResultBody(result: any, where?: string): Buffer<ArrayBuffer>;
|
|
13
|
+
export const DOWNLOAD_PAYLOAD_LIMIT_BYTES: number;
|
|
14
|
+
export const DOWNLOAD_DEFAULT_CONTENT_TYPE: "application/octet-stream";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const DOWNLOAD_PAYLOAD_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
2
|
+
export const DOWNLOAD_DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
3
|
+
const CONTROL_CHAR_RE = /[\0-\x1F\x7F]/;
|
|
4
|
+
const PATH_SEPARATOR_RE = /[\\/]/;
|
|
5
|
+
function formatWhere(where = 'download(...)') {
|
|
6
|
+
return String(where || 'download(...)');
|
|
7
|
+
}
|
|
8
|
+
function normalizeFilename(filename, where = 'download(...)') {
|
|
9
|
+
const label = formatWhere(where);
|
|
10
|
+
const value = String(filename ?? '').trim();
|
|
11
|
+
if (!value) {
|
|
12
|
+
throw new Error(`[Zenith] ${label}: download filename is required.`);
|
|
13
|
+
}
|
|
14
|
+
if (CONTROL_CHAR_RE.test(value) || PATH_SEPARATOR_RE.test(value)) {
|
|
15
|
+
throw new Error(`[Zenith] ${label}: download filename must not contain path separators or control characters.`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function normalizeContentType(contentType, where = 'download(...)') {
|
|
20
|
+
const label = formatWhere(where);
|
|
21
|
+
if (contentType === undefined || contentType === null) {
|
|
22
|
+
return DOWNLOAD_DEFAULT_CONTENT_TYPE;
|
|
23
|
+
}
|
|
24
|
+
const value = String(contentType).trim();
|
|
25
|
+
if (!value) {
|
|
26
|
+
throw new Error(`[Zenith] ${label}: download contentType must be a non-empty string when provided.`);
|
|
27
|
+
}
|
|
28
|
+
if (CONTROL_CHAR_RE.test(value)) {
|
|
29
|
+
throw new Error(`[Zenith] ${label}: download contentType must not contain control characters.`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function isBlobLike(value) {
|
|
34
|
+
return (typeof Blob !== 'undefined' && value instanceof Blob)
|
|
35
|
+
|| (typeof File !== 'undefined' && value instanceof File);
|
|
36
|
+
}
|
|
37
|
+
function encodeBuffer(buffer, encoding) {
|
|
38
|
+
return encoding === 'utf8'
|
|
39
|
+
? buffer.toString('utf8')
|
|
40
|
+
: buffer.toString('base64');
|
|
41
|
+
}
|
|
42
|
+
function decodeBody(body, bodyEncoding, where = 'download(...)') {
|
|
43
|
+
const label = formatWhere(where);
|
|
44
|
+
if (typeof body !== 'string') {
|
|
45
|
+
throw new Error(`[Zenith] ${label}: download body must be a string after normalization.`);
|
|
46
|
+
}
|
|
47
|
+
if (bodyEncoding === 'utf8') {
|
|
48
|
+
return Buffer.from(body, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
if (bodyEncoding === 'base64') {
|
|
51
|
+
return Buffer.from(body, 'base64');
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`[Zenith] ${label}: download bodyEncoding must be "utf8" or "base64".`);
|
|
54
|
+
}
|
|
55
|
+
function normalizeBody(body, where = 'download(...)') {
|
|
56
|
+
const label = formatWhere(where);
|
|
57
|
+
if (isBlobLike(body)) {
|
|
58
|
+
throw new Error(`[Zenith] ${label}: download body must be string, Uint8Array, ArrayBuffer, or Buffer-compatible bytes.`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof body === 'string') {
|
|
61
|
+
const size = Buffer.byteLength(body, 'utf8');
|
|
62
|
+
if (size > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
|
|
63
|
+
throw new Error(`[Zenith] ${label}: download payload exceeds ${DOWNLOAD_PAYLOAD_LIMIT_BYTES} bytes.`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
body: body,
|
|
67
|
+
bodyEncoding: 'utf8',
|
|
68
|
+
bodySize: size
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (body instanceof ArrayBuffer) {
|
|
72
|
+
const buffer = Buffer.from(body);
|
|
73
|
+
if (buffer.byteLength > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
|
|
74
|
+
throw new Error(`[Zenith] ${label}: download payload exceeds ${DOWNLOAD_PAYLOAD_LIMIT_BYTES} bytes.`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
body: encodeBuffer(buffer, 'base64'),
|
|
78
|
+
bodyEncoding: 'base64',
|
|
79
|
+
bodySize: buffer.byteLength
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (ArrayBuffer.isView(body)) {
|
|
83
|
+
const buffer = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
84
|
+
if (buffer.byteLength > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
|
|
85
|
+
throw new Error(`[Zenith] ${label}: download payload exceeds ${DOWNLOAD_PAYLOAD_LIMIT_BYTES} bytes.`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
body: encodeBuffer(buffer, 'base64'),
|
|
89
|
+
bodyEncoding: 'base64',
|
|
90
|
+
bodySize: buffer.byteLength
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`[Zenith] ${label}: download body must be string, Uint8Array, ArrayBuffer, or Buffer-compatible bytes.`);
|
|
94
|
+
}
|
|
95
|
+
function buildAsciiFilename(filename) {
|
|
96
|
+
const replaced = Array.from(String(filename || '')).map((char) => {
|
|
97
|
+
const code = char.charCodeAt(0);
|
|
98
|
+
if (code < 0x20 || code > 0x7E || char === '"' || char === '\\') {
|
|
99
|
+
return '_';
|
|
100
|
+
}
|
|
101
|
+
return char;
|
|
102
|
+
}).join('');
|
|
103
|
+
return replaced || 'download';
|
|
104
|
+
}
|
|
105
|
+
export function buildAttachmentContentDisposition(filename) {
|
|
106
|
+
const safeFilename = normalizeFilename(filename, 'download result');
|
|
107
|
+
const asciiFilename = buildAsciiFilename(safeFilename);
|
|
108
|
+
const encodedFilename = encodeURIComponent(safeFilename)
|
|
109
|
+
.replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
110
|
+
.replace(/\*/g, '%2A');
|
|
111
|
+
return `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`;
|
|
112
|
+
}
|
|
113
|
+
export function createDownloadResult(body, options = {}) {
|
|
114
|
+
const filename = normalizeFilename(options?.filename, 'download(...)');
|
|
115
|
+
const contentType = normalizeContentType(options?.contentType, 'download(...)');
|
|
116
|
+
const normalized = normalizeBody(body, 'download(...)');
|
|
117
|
+
return {
|
|
118
|
+
kind: 'download',
|
|
119
|
+
body: normalized.body,
|
|
120
|
+
bodyEncoding: normalized.bodyEncoding,
|
|
121
|
+
bodySize: normalized.bodySize,
|
|
122
|
+
filename,
|
|
123
|
+
contentType,
|
|
124
|
+
status: 200
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function assertValidDownloadResult(value, where = 'download result') {
|
|
128
|
+
const label = formatWhere(where);
|
|
129
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
130
|
+
throw new Error(`[Zenith] ${label}: download result must be an object.`);
|
|
131
|
+
}
|
|
132
|
+
normalizeFilename(value.filename, label);
|
|
133
|
+
normalizeContentType(value.contentType, label);
|
|
134
|
+
if (value.status !== 200) {
|
|
135
|
+
throw new Error(`[Zenith] ${label}: download status is fixed to 200 in this milestone.`);
|
|
136
|
+
}
|
|
137
|
+
if (!Number.isInteger(value.bodySize) || value.bodySize < 0 || value.bodySize > DOWNLOAD_PAYLOAD_LIMIT_BYTES) {
|
|
138
|
+
throw new Error(`[Zenith] ${label}: download bodySize must be an integer between 0 and ${DOWNLOAD_PAYLOAD_LIMIT_BYTES}.`);
|
|
139
|
+
}
|
|
140
|
+
const buffer = decodeBody(value.body, value.bodyEncoding, label);
|
|
141
|
+
if (buffer.byteLength !== value.bodySize) {
|
|
142
|
+
throw new Error(`[Zenith] ${label}: download bodySize does not match the normalized body.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function decodeDownloadResultBody(result, where = 'download result') {
|
|
146
|
+
assertValidDownloadResult(result, where);
|
|
147
|
+
return decodeBody(result.body, result.bodyEncoding, where);
|
|
148
|
+
}
|
package/dist/images/service.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
export function buildImageArtifacts(options: any): Promise<{
|
|
2
2
|
manifest: {};
|
|
3
3
|
}>;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @param {Request | { url?: string } | null | undefined} request
|
|
6
|
+
* @param {{ requestUrl?: URL | string, projectRoot: string, config?: Record<string, unknown> }} options
|
|
7
|
+
* @returns {Promise<Response>}
|
|
8
|
+
*/
|
|
9
|
+
export function handleImageFetchRequest(request: Request | {
|
|
10
|
+
url?: string;
|
|
11
|
+
} | null | undefined, options: {
|
|
12
|
+
requestUrl?: URL | string;
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
config?: Record<string, unknown>;
|
|
15
|
+
}): Promise<Response>;
|
|
16
|
+
export function handleImageRequest(_req: any, res: any, options: any): Promise<boolean>;
|
package/dist/images/service.js
CHANGED
|
@@ -220,7 +220,29 @@ function remoteCachePaths(cacheDir, cacheKey) {
|
|
|
220
220
|
metaPath: join(cacheDir, `${cacheKey}.json`)
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
|
-
|
|
223
|
+
function createJsonResponse(status, payload) {
|
|
224
|
+
return new Response(JSON.stringify(payload), {
|
|
225
|
+
status,
|
|
226
|
+
headers: {
|
|
227
|
+
'Content-Type': 'application/json'
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function createBufferResponse(status, contentType, buffer, cacheSeconds) {
|
|
232
|
+
return new Response(buffer, {
|
|
233
|
+
status,
|
|
234
|
+
headers: {
|
|
235
|
+
'Content-Type': contentType,
|
|
236
|
+
'Cache-Control': `public, max-age=${cacheSeconds}`
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function sendResponse(res, response) {
|
|
241
|
+
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
242
|
+
const body = await response.arrayBuffer();
|
|
243
|
+
res.end(Buffer.from(body));
|
|
244
|
+
}
|
|
245
|
+
async function createImageResponse(options) {
|
|
224
246
|
const { requestUrl, projectRoot, config: rawConfig } = options;
|
|
225
247
|
const config = normalizeImageConfig(rawConfig);
|
|
226
248
|
const url = requestUrl instanceof URL ? requestUrl : new URL(String(requestUrl));
|
|
@@ -230,14 +252,10 @@ export async function handleImageRequest(req, res, options) {
|
|
|
230
252
|
const format = normalizeImageFormat(url.searchParams.get('f') || '');
|
|
231
253
|
const quality = Number.isInteger(requestedQuality) && requestedQuality > 0 ? requestedQuality : config.quality;
|
|
232
254
|
if (!remoteUrl) {
|
|
233
|
-
|
|
234
|
-
res.end(JSON.stringify({ error: 'missing_url' }));
|
|
235
|
-
return true;
|
|
255
|
+
return createJsonResponse(400, { error: 'missing_url' });
|
|
236
256
|
}
|
|
237
257
|
if (!Number.isInteger(width) || width <= 0) {
|
|
238
|
-
|
|
239
|
-
res.end(JSON.stringify({ error: 'invalid_width' }));
|
|
240
|
-
return true;
|
|
258
|
+
return createJsonResponse(400, { error: 'invalid_width' });
|
|
241
259
|
}
|
|
242
260
|
try {
|
|
243
261
|
const remote = await validateRemoteTarget(remoteUrl, config);
|
|
@@ -253,8 +271,7 @@ export async function handleImageRequest(req, res, options) {
|
|
|
253
271
|
const contentType = typeof parsedMeta?.contentType === 'string'
|
|
254
272
|
? parsedMeta.contentType
|
|
255
273
|
: mimeTypeForFormat(format || 'jpg');
|
|
256
|
-
|
|
257
|
-
return true;
|
|
274
|
+
return createBufferResponse(200, contentType, cached, config.minimumCacheTTL);
|
|
258
275
|
}
|
|
259
276
|
const response = await fetch(remote, {
|
|
260
277
|
headers: {
|
|
@@ -288,15 +305,28 @@ export async function handleImageRequest(req, res, options) {
|
|
|
288
305
|
format: targetFormat
|
|
289
306
|
}, null, 2)}\n`, 'utf8')
|
|
290
307
|
]);
|
|
291
|
-
|
|
292
|
-
return true;
|
|
308
|
+
return createBufferResponse(200, mimeTypeForFormat(targetFormat), output, config.minimumCacheTTL);
|
|
293
309
|
}
|
|
294
310
|
catch (error) {
|
|
295
|
-
|
|
296
|
-
res.end(JSON.stringify({
|
|
311
|
+
return createJsonResponse(400, {
|
|
297
312
|
error: 'image_request_failed',
|
|
298
313
|
message: error instanceof Error ? error.message : String(error)
|
|
299
|
-
})
|
|
300
|
-
return true;
|
|
314
|
+
});
|
|
301
315
|
}
|
|
302
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* @param {Request | { url?: string } | null | undefined} request
|
|
319
|
+
* @param {{ requestUrl?: URL | string, projectRoot: string, config?: Record<string, unknown> }} options
|
|
320
|
+
* @returns {Promise<Response>}
|
|
321
|
+
*/
|
|
322
|
+
export async function handleImageFetchRequest(request, options) {
|
|
323
|
+
return createImageResponse({
|
|
324
|
+
...options,
|
|
325
|
+
requestUrl: request?.url || options?.requestUrl
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
export async function handleImageRequest(_req, res, options) {
|
|
329
|
+
const response = await createImageResponse(options);
|
|
330
|
+
await sendResponse(res, response);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* @typedef {{
|
|
3
3
|
* path: string,
|
|
4
4
|
* file: string,
|
|
5
|
+
* route_kind?: 'page' | 'resource',
|
|
5
6
|
* path_kind: 'static' | 'dynamic',
|
|
6
7
|
* render_mode: 'prerender' | 'server',
|
|
7
|
-
* params: string[]
|
|
8
|
+
* params: string[],
|
|
9
|
+
* server_script?: string,
|
|
10
|
+
* server_script_path?: string,
|
|
11
|
+
* has_guard?: boolean,
|
|
12
|
+
* has_load?: boolean,
|
|
13
|
+
* has_action?: boolean,
|
|
14
|
+
* export_paths?: string[]
|
|
8
15
|
* }} ManifestEntry
|
|
9
16
|
*/
|
|
10
17
|
/**
|
|
@@ -29,7 +36,14 @@ export function serializeManifest(entries: ManifestEntry[]): string;
|
|
|
29
36
|
export type ManifestEntry = {
|
|
30
37
|
path: string;
|
|
31
38
|
file: string;
|
|
39
|
+
route_kind?: "page" | "resource";
|
|
32
40
|
path_kind: "static" | "dynamic";
|
|
33
41
|
render_mode: "prerender" | "server";
|
|
34
42
|
params: string[];
|
|
43
|
+
server_script?: string;
|
|
44
|
+
server_script_path?: string;
|
|
45
|
+
has_guard?: boolean;
|
|
46
|
+
has_load?: boolean;
|
|
47
|
+
has_action?: boolean;
|
|
48
|
+
export_paths?: string[];
|
|
35
49
|
};
|
package/dist/manifest.js
CHANGED
|
@@ -18,14 +18,23 @@ import { readFileSync } from 'node:fs';
|
|
|
18
18
|
import { readdir, stat } from 'node:fs/promises';
|
|
19
19
|
import { join, relative, sep, basename, extname, dirname } from 'node:path';
|
|
20
20
|
import { extractServerScript } from './build/server-script.js';
|
|
21
|
+
import { analyzeResourceRouteModule, isResourceRouteFile } from './resource-route-module.js';
|
|
21
22
|
import { composeServerScriptEnvelope, resolveAdjacentServerModules } from './server-script-composition.js';
|
|
23
|
+
import { validateStaticExportPaths } from './static-export-paths.js';
|
|
22
24
|
/**
|
|
23
25
|
* @typedef {{
|
|
24
26
|
* path: string,
|
|
25
27
|
* file: string,
|
|
28
|
+
* route_kind?: 'page' | 'resource',
|
|
26
29
|
* path_kind: 'static' | 'dynamic',
|
|
27
30
|
* render_mode: 'prerender' | 'server',
|
|
28
|
-
* params: string[]
|
|
31
|
+
* params: string[],
|
|
32
|
+
* server_script?: string,
|
|
33
|
+
* server_script_path?: string,
|
|
34
|
+
* has_guard?: boolean,
|
|
35
|
+
* has_load?: boolean,
|
|
36
|
+
* has_action?: boolean,
|
|
37
|
+
* export_paths?: string[]
|
|
29
38
|
* }} ManifestEntry
|
|
30
39
|
*/
|
|
31
40
|
/**
|
|
@@ -75,17 +84,20 @@ async function _scanDir(dir, root, ext, compilerOpts) {
|
|
|
75
84
|
}
|
|
76
85
|
else if (item.endsWith(ext)) {
|
|
77
86
|
const routePath = _fileToRoute(fullPath, root, ext);
|
|
78
|
-
entries.push(
|
|
87
|
+
entries.push(buildPageManifestEntry({
|
|
79
88
|
fullPath,
|
|
80
89
|
root,
|
|
81
90
|
routePath,
|
|
82
91
|
compilerOpts
|
|
83
92
|
}));
|
|
84
93
|
}
|
|
94
|
+
else if (isResourceRouteFile(item)) {
|
|
95
|
+
entries.push(analyzeResourceRouteModule(fullPath, root));
|
|
96
|
+
}
|
|
85
97
|
}
|
|
86
98
|
return entries;
|
|
87
99
|
}
|
|
88
|
-
function
|
|
100
|
+
function buildPageManifestEntry({ fullPath, root, routePath, compilerOpts }) {
|
|
89
101
|
const rawSource = readFileSync(fullPath, 'utf8');
|
|
90
102
|
const inlineServerScript = extractServerScript(rawSource, fullPath, compilerOpts).serverScript;
|
|
91
103
|
const { guardPath, loadPath, actionPath } = resolveAdjacentServerModules(fullPath);
|
|
@@ -96,12 +108,17 @@ function buildManifestEntry({ fullPath, root, routePath, compilerOpts }) {
|
|
|
96
108
|
adjacentLoadPath: loadPath,
|
|
97
109
|
adjacentActionPath: actionPath
|
|
98
110
|
});
|
|
111
|
+
const exportPaths = Array.isArray(composed.serverScript?.export_paths)
|
|
112
|
+
? validateStaticExportPaths(routePath, composed.serverScript.export_paths, fullPath)
|
|
113
|
+
: [];
|
|
99
114
|
return {
|
|
100
115
|
path: routePath,
|
|
101
116
|
file: relative(root, fullPath),
|
|
117
|
+
route_kind: 'page',
|
|
102
118
|
path_kind: _isDynamic(routePath) ? 'dynamic' : 'static',
|
|
103
119
|
render_mode: composed.serverScript && composed.serverScript.prerender !== true ? 'server' : 'prerender',
|
|
104
|
-
params: extractRouteParams(routePath)
|
|
120
|
+
params: extractRouteParams(routePath),
|
|
121
|
+
...(exportPaths.length > 0 ? { export_paths: exportPaths } : {})
|
|
105
122
|
};
|
|
106
123
|
}
|
|
107
124
|
function extractRouteParams(routePath) {
|
|
@@ -325,7 +342,9 @@ function segmentWeight(segment) {
|
|
|
325
342
|
* @returns {string}
|
|
326
343
|
*/
|
|
327
344
|
export function serializeManifest(entries) {
|
|
328
|
-
const lines = entries
|
|
345
|
+
const lines = entries
|
|
346
|
+
.filter((entry) => entry?.route_kind !== 'resource')
|
|
347
|
+
.map((e) => {
|
|
329
348
|
const hasParams = _isDynamic(e.path);
|
|
330
349
|
const loader = hasParams
|
|
331
350
|
? `(params) => import('./pages/${e.file}')`
|
package/dist/preview.d.ts
CHANGED
|
@@ -37,20 +37,27 @@ export function createPreviewServer(options: {
|
|
|
37
37
|
* @returns {Promise<PreviewRoute[]>}
|
|
38
38
|
*/
|
|
39
39
|
export function loadRouteManifest(distDir: string): Promise<PreviewRoute[]>;
|
|
40
|
+
export function loadRouteSurfaceState(distDir: any, fallbackBasePath?: string): Promise<{
|
|
41
|
+
basePath: string;
|
|
42
|
+
pageRoutes: any;
|
|
43
|
+
resourceRoutes: any[];
|
|
44
|
+
}>;
|
|
40
45
|
/**
|
|
41
|
-
* @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
|
|
42
|
-
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
|
|
46
|
+
* @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
|
|
47
|
+
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
|
|
43
48
|
*/
|
|
44
|
-
export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders,
|
|
49
|
+
export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind, guardOnly }: {
|
|
45
50
|
source: string;
|
|
46
51
|
sourcePath: string;
|
|
47
52
|
params: Record<string, string>;
|
|
48
53
|
requestUrl?: string;
|
|
49
54
|
requestMethod?: string;
|
|
50
55
|
requestHeaders?: Record<string, string | string[] | undefined>;
|
|
56
|
+
requestBodyBuffer?: Buffer | null;
|
|
51
57
|
routePattern?: string;
|
|
52
58
|
routeFile?: string;
|
|
53
59
|
routeId?: string;
|
|
60
|
+
routeKind?: "page" | "resource";
|
|
54
61
|
}): Promise<{
|
|
55
62
|
result: {
|
|
56
63
|
kind: string;
|
|
@@ -62,6 +69,7 @@ export function executeServerRoute({ source, sourcePath, params, requestUrl, req
|
|
|
62
69
|
load: string;
|
|
63
70
|
};
|
|
64
71
|
status?: number;
|
|
72
|
+
setCookies?: string[];
|
|
65
73
|
}>;
|
|
66
74
|
/**
|
|
67
75
|
* @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
|