@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/dev-server.js
CHANGED
|
@@ -15,11 +15,18 @@ import { existsSync, watch } from 'node:fs';
|
|
|
15
15
|
import { readFile, stat } from 'node:fs/promises';
|
|
16
16
|
import { performance } from 'node:perf_hooks';
|
|
17
17
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
18
|
+
import { appLocalRedirectLocation, imageEndpointPath, normalizeBasePath, routeCheckPath, stripBasePath } from './base-path.js';
|
|
19
|
+
import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
|
|
18
20
|
import { createDevBuildSession } from './dev-build-session.js';
|
|
19
21
|
import { createStartupProfiler } from './startup-profile.js';
|
|
20
22
|
import { createSilentLogger } from './ui/logger.js';
|
|
21
23
|
import { readChangeFingerprint } from './dev-watch.js';
|
|
22
|
-
import {
|
|
24
|
+
import { createTrustedOriginResolver, publicHost } from './request-origin.js';
|
|
25
|
+
import { readRequestBodyBuffer } from './request-body.js';
|
|
26
|
+
import { buildResourceResponseDescriptor } from './resource-response.js';
|
|
27
|
+
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
28
|
+
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
|
|
29
|
+
import { executeServerRoute, injectSsrPayload, loadRouteSurfaceState, resolveWithinDist, toStaticFilePath } from './preview.js';
|
|
23
30
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
24
31
|
import { injectImageRuntimePayload } from './images/payload.js';
|
|
25
32
|
import { handleImageRequest } from './images/service.js';
|
|
@@ -37,6 +44,15 @@ const MIME_TYPES = {
|
|
|
37
44
|
'.avif': 'image/avif',
|
|
38
45
|
'.gif': 'image/gif'
|
|
39
46
|
};
|
|
47
|
+
const IMAGE_RUNTIME_TAG_RE = new RegExp('<' + 'script\\b[^>]*\\bid=(["\'])zenith-image-runtime\\1[^>]*>[\\s\\S]*?<\\/' + 'script>', 'i');
|
|
48
|
+
const EVENT_STREAM_MIME = ['text', 'event-stream'].join('/');
|
|
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
|
+
}
|
|
40
56
|
// Note: V0 HMR script injection has been moved to the runtime client.
|
|
41
57
|
// This server purely hosts the V1 HMR contract endpoints.
|
|
42
58
|
/**
|
|
@@ -50,6 +66,10 @@ export async function createDevServer(options) {
|
|
|
50
66
|
const { pagesDir, outDir, port = 3000, host = '127.0.0.1', config = {}, logger: providedLogger = null } = options;
|
|
51
67
|
const logger = providedLogger || createSilentLogger();
|
|
52
68
|
const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
|
|
69
|
+
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
70
|
+
const resolvedTarget = resolveBuildAdapter(config).target;
|
|
71
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolvedTarget);
|
|
72
|
+
const isStaticExportTarget = resolvedTarget === 'static-export';
|
|
53
73
|
const resolvedPagesDir = resolve(pagesDir);
|
|
54
74
|
const resolvedOutDir = resolve(outDir);
|
|
55
75
|
const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
|
|
@@ -86,17 +106,19 @@ export async function createDevServer(options) {
|
|
|
86
106
|
let currentCssHref = '';
|
|
87
107
|
let currentCssContent = '';
|
|
88
108
|
let actualPort = port;
|
|
89
|
-
|
|
109
|
+
const resolveServerOrigin = createTrustedOriginResolver({
|
|
110
|
+
host,
|
|
111
|
+
getPort: () => actualPort,
|
|
112
|
+
label: 'dev server'
|
|
113
|
+
});
|
|
114
|
+
let currentRouteState = { pageRoutes: [], resourceRoutes: [] };
|
|
90
115
|
const rebuildDebounceMs = 5;
|
|
91
116
|
const queuedRebuildDebounceMs = 5;
|
|
92
117
|
function _publicHost() {
|
|
93
|
-
|
|
94
|
-
return '127.0.0.1';
|
|
95
|
-
}
|
|
96
|
-
return host;
|
|
118
|
+
return publicHost(host);
|
|
97
119
|
}
|
|
98
120
|
function _serverOrigin() {
|
|
99
|
-
return
|
|
121
|
+
return resolveServerOrigin();
|
|
100
122
|
}
|
|
101
123
|
function _trace(event, payload = {}) {
|
|
102
124
|
if (!traceEnabled)
|
|
@@ -208,6 +230,9 @@ export async function createDevServer(options) {
|
|
|
208
230
|
throw lastError;
|
|
209
231
|
}
|
|
210
232
|
function _buildNotFoundPayload(pathname, category, cause) {
|
|
233
|
+
const hintedPath = category === 'page'
|
|
234
|
+
? (stripBasePath(pathname, configuredBasePath) || pathname)
|
|
235
|
+
: pathname;
|
|
211
236
|
const payload = {
|
|
212
237
|
kind: 'zenith_dev_not_found',
|
|
213
238
|
category,
|
|
@@ -235,7 +260,7 @@ export async function createDevServer(options) {
|
|
|
235
260
|
payload.docsLink = '/docs/documentation/contracts/hmr-v1-contract.md';
|
|
236
261
|
return payload;
|
|
237
262
|
}
|
|
238
|
-
const routeFile = _routeFileHint(
|
|
263
|
+
const routeFile = _routeFileHint(hintedPath);
|
|
239
264
|
payload.routeFile = routeFile;
|
|
240
265
|
payload.cause = `no route file found at ${routeFile}`;
|
|
241
266
|
payload.hint = `Create ${routeFile} or verify router manifest output.`;
|
|
@@ -355,22 +380,26 @@ export async function createDevServer(options) {
|
|
|
355
380
|
return true;
|
|
356
381
|
}
|
|
357
382
|
async function _loadRoutesForRequests() {
|
|
358
|
-
if (buildStatus === 'building' &&
|
|
359
|
-
|
|
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;
|
|
360
387
|
}
|
|
361
388
|
try {
|
|
362
|
-
const
|
|
363
|
-
if (Array.isArray(
|
|
364
|
-
|
|
365
|
-
|
|
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;
|
|
366
394
|
}
|
|
367
395
|
}
|
|
368
396
|
catch (error) {
|
|
369
|
-
if (!(Array.isArray(
|
|
397
|
+
if (!(Array.isArray(currentRouteState.pageRoutes) && currentRouteState.pageRoutes.length > 0) &&
|
|
398
|
+
!(Array.isArray(currentRouteState.resourceRoutes) && currentRouteState.resourceRoutes.length > 0)) {
|
|
370
399
|
throw error;
|
|
371
400
|
}
|
|
372
401
|
}
|
|
373
|
-
return
|
|
402
|
+
return currentRouteState;
|
|
374
403
|
}
|
|
375
404
|
function _broadcastEvent(type, payload = {}) {
|
|
376
405
|
const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : buildId;
|
|
@@ -403,7 +432,7 @@ export async function createDevServer(options) {
|
|
|
403
432
|
logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
|
|
404
433
|
const initialBuild = await buildSession.build();
|
|
405
434
|
const cssReady = await _syncCssStateFromBuild(initialBuild, buildId);
|
|
406
|
-
|
|
435
|
+
currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
407
436
|
buildStatus = 'ok';
|
|
408
437
|
buildError = null;
|
|
409
438
|
lastBuildMs = Date.now();
|
|
@@ -422,7 +451,8 @@ export async function createDevServer(options) {
|
|
|
422
451
|
status: buildStatus,
|
|
423
452
|
durationMs,
|
|
424
453
|
cssReady,
|
|
425
|
-
routes: Array.isArray(
|
|
454
|
+
routes: (Array.isArray(currentRouteState.pageRoutes) ? currentRouteState.pageRoutes.length : 0) +
|
|
455
|
+
(Array.isArray(currentRouteState.resourceRoutes) ? currentRouteState.resourceRoutes.length : 0)
|
|
426
456
|
});
|
|
427
457
|
}
|
|
428
458
|
catch (err) {
|
|
@@ -452,15 +482,12 @@ export async function createDevServer(options) {
|
|
|
452
482
|
}
|
|
453
483
|
}
|
|
454
484
|
const server = createServer(async (req, res) => {
|
|
455
|
-
const
|
|
456
|
-
? `http://${req.headers.host}`
|
|
457
|
-
: _serverOrigin();
|
|
458
|
-
const url = new URL(req.url, requestBase);
|
|
485
|
+
const url = new URL(req.url, _serverOrigin());
|
|
459
486
|
let pathname = url.pathname;
|
|
460
487
|
// Legacy HMR endpoint (deprecated but kept alive to avoid breaking old caches instantly)
|
|
461
|
-
if (pathname ===
|
|
488
|
+
if (pathname === LEGACY_DEV_STREAM_PATH) {
|
|
462
489
|
res.writeHead(200, {
|
|
463
|
-
'Content-Type':
|
|
490
|
+
'Content-Type': EVENT_STREAM_MIME,
|
|
464
491
|
'Cache-Control': 'no-store',
|
|
465
492
|
'Connection': 'keep-alive',
|
|
466
493
|
'X-Zenith-Deprecated': 'true'
|
|
@@ -498,7 +525,7 @@ export async function createDevServer(options) {
|
|
|
498
525
|
// V1 Dev Events Endpoint (SSE)
|
|
499
526
|
if (pathname === '/__zenith_dev/events') {
|
|
500
527
|
res.writeHead(200, {
|
|
501
|
-
'Content-Type':
|
|
528
|
+
'Content-Type': EVENT_STREAM_MIME,
|
|
502
529
|
'Cache-Control': 'no-store',
|
|
503
530
|
'Connection': 'keep-alive',
|
|
504
531
|
'X-Accel-Buffering': 'no'
|
|
@@ -565,7 +592,10 @@ export async function createDevServer(options) {
|
|
|
565
592
|
res.end(currentCssContent);
|
|
566
593
|
return;
|
|
567
594
|
}
|
|
568
|
-
if (pathname ===
|
|
595
|
+
if (pathname === imageEndpointPath(configuredBasePath)) {
|
|
596
|
+
if (isStaticExportTarget) {
|
|
597
|
+
throw new Error('not found');
|
|
598
|
+
}
|
|
569
599
|
await handleImageRequest(req, res, {
|
|
570
600
|
requestUrl: url,
|
|
571
601
|
projectRoot,
|
|
@@ -573,8 +603,13 @@ export async function createDevServer(options) {
|
|
|
573
603
|
});
|
|
574
604
|
return;
|
|
575
605
|
}
|
|
576
|
-
if (pathname ===
|
|
606
|
+
if (pathname === routeCheckPath(configuredBasePath)) {
|
|
577
607
|
try {
|
|
608
|
+
if (!routeCheckEnabled) {
|
|
609
|
+
res.writeHead(501, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
610
|
+
res.end(JSON.stringify({ error: 'route_check_unsupported' }));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
578
613
|
if (!initialBuildSettled && buildStatus === 'building') {
|
|
579
614
|
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
580
615
|
res.end(JSON.stringify({
|
|
@@ -602,8 +637,16 @@ export async function createDevServer(options) {
|
|
|
602
637
|
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
603
638
|
return;
|
|
604
639
|
}
|
|
640
|
+
const canonicalTargetPath = stripBasePath(targetUrl.pathname, configuredBasePath);
|
|
641
|
+
if (canonicalTargetPath === null) {
|
|
642
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
643
|
+
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
647
|
+
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
605
648
|
const routes = await _loadRoutesForRequests();
|
|
606
|
-
const resolvedCheck = resolveRequestRoute(
|
|
649
|
+
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes.pageRoutes || []);
|
|
607
650
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
608
651
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
609
652
|
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
@@ -623,16 +666,17 @@ export async function createDevServer(options) {
|
|
|
623
666
|
});
|
|
624
667
|
// Security: Enforce relative or same-origin redirects
|
|
625
668
|
if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
|
|
626
|
-
const loc =
|
|
669
|
+
const loc = appLocalRedirectLocation(checkResult.result.location || '/', configuredBasePath);
|
|
670
|
+
checkResult.result.location = loc;
|
|
627
671
|
if (loc.includes('://') || loc.startsWith('//')) {
|
|
628
672
|
try {
|
|
629
673
|
const parsedLoc = new URL(loc);
|
|
630
674
|
if (parsedLoc.origin !== targetUrl.origin) {
|
|
631
|
-
checkResult.result.location = '/';
|
|
675
|
+
checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
|
|
632
676
|
}
|
|
633
677
|
}
|
|
634
678
|
catch {
|
|
635
|
-
checkResult.result.location = '/';
|
|
679
|
+
checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
|
|
636
680
|
}
|
|
637
681
|
}
|
|
638
682
|
}
|
|
@@ -644,7 +688,7 @@ export async function createDevServer(options) {
|
|
|
644
688
|
'Vary': 'Cookie'
|
|
645
689
|
});
|
|
646
690
|
res.end(JSON.stringify({
|
|
647
|
-
result: checkResult?.result || checkResult,
|
|
691
|
+
result: sanitizeRouteResult(checkResult?.result || checkResult),
|
|
648
692
|
routeId: resolvedCheck.route.route_id || '',
|
|
649
693
|
to: targetUrl.toString()
|
|
650
694
|
}));
|
|
@@ -659,6 +703,7 @@ export async function createDevServer(options) {
|
|
|
659
703
|
let resolvedPathFor404 = null;
|
|
660
704
|
let staticRootFor404 = null;
|
|
661
705
|
try {
|
|
706
|
+
const canonicalPath = stripBasePath(pathname, configuredBasePath);
|
|
662
707
|
if (!initialBuildSettled && buildStatus === 'building') {
|
|
663
708
|
const pendingPayload = {
|
|
664
709
|
kind: 'zenith_dev_build_pending',
|
|
@@ -689,11 +734,19 @@ export async function createDevServer(options) {
|
|
|
689
734
|
].join(''));
|
|
690
735
|
return;
|
|
691
736
|
}
|
|
692
|
-
|
|
737
|
+
if (canonicalPath === null) {
|
|
738
|
+
throw new Error('not found');
|
|
739
|
+
}
|
|
740
|
+
const requestExt = extname(canonicalPath);
|
|
693
741
|
if (requestExt && requestExt !== '.html') {
|
|
694
|
-
const assetPath =
|
|
742
|
+
const assetPath = isStaticExportTarget
|
|
743
|
+
? resolveWithinDist(outDir, pathname)
|
|
744
|
+
: join(outDir, canonicalPath);
|
|
695
745
|
resolvedPathFor404 = assetPath;
|
|
696
746
|
staticRootFor404 = outDir;
|
|
747
|
+
if (!assetPath) {
|
|
748
|
+
throw new Error('not found');
|
|
749
|
+
}
|
|
697
750
|
const asset = await _readFileForRequest(assetPath);
|
|
698
751
|
const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
|
|
699
752
|
res.writeHead(200, { 'Content-Type': mime });
|
|
@@ -701,9 +754,42 @@ export async function createDevServer(options) {
|
|
|
701
754
|
return;
|
|
702
755
|
}
|
|
703
756
|
const routes = await _loadRoutesForRequests();
|
|
704
|
-
const
|
|
757
|
+
const canonicalUrl = new URL(url.toString());
|
|
758
|
+
canonicalUrl.pathname = canonicalPath;
|
|
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 || []);
|
|
705
788
|
let filePath = null;
|
|
706
|
-
if (
|
|
789
|
+
if (isStaticExportTarget) {
|
|
790
|
+
filePath = toStaticFilePath(outDir, pathname);
|
|
791
|
+
}
|
|
792
|
+
else if (resolved.matched && resolved.route) {
|
|
707
793
|
if (verboseLogging) {
|
|
708
794
|
logger.router(`${req.method || 'GET'} ${pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`);
|
|
709
795
|
}
|
|
@@ -713,7 +799,7 @@ export async function createDevServer(options) {
|
|
|
713
799
|
filePath = resolveWithinDist(outDir, output);
|
|
714
800
|
}
|
|
715
801
|
else {
|
|
716
|
-
filePath = toStaticFilePath(outDir,
|
|
802
|
+
filePath = toStaticFilePath(outDir, canonicalPath);
|
|
717
803
|
}
|
|
718
804
|
resolvedPathFor404 = filePath;
|
|
719
805
|
staticRootFor404 = outDir;
|
|
@@ -721,48 +807,56 @@ export async function createDevServer(options) {
|
|
|
721
807
|
throw new Error('not found');
|
|
722
808
|
}
|
|
723
809
|
let ssrPayload = null;
|
|
810
|
+
let routeExecution = null;
|
|
724
811
|
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
725
|
-
let routeExecution = null;
|
|
726
812
|
try {
|
|
813
|
+
const requestMethod = req.method || 'GET';
|
|
814
|
+
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
815
|
+
? null
|
|
816
|
+
: await readRequestBodyBuffer(req);
|
|
727
817
|
routeExecution = await executeServerRoute({
|
|
728
818
|
source: resolved.route.server_script,
|
|
729
819
|
sourcePath: resolved.route.server_script_path || '',
|
|
730
820
|
params: resolved.params,
|
|
731
821
|
requestUrl: url.toString(),
|
|
732
|
-
requestMethod
|
|
822
|
+
requestMethod,
|
|
733
823
|
requestHeaders: req.headers,
|
|
824
|
+
requestBodyBuffer,
|
|
734
825
|
routePattern: resolved.route.path,
|
|
735
826
|
routeFile: resolved.route.server_script_path || '',
|
|
736
827
|
routeId: resolved.route.route_id || ''
|
|
737
828
|
});
|
|
738
829
|
}
|
|
739
830
|
catch (error) {
|
|
831
|
+
logServerException('dev server route execution failed', error);
|
|
740
832
|
ssrPayload = {
|
|
741
833
|
__zenith_error: {
|
|
834
|
+
status: 500,
|
|
742
835
|
code: 'LOAD_FAILED',
|
|
743
|
-
message: error instanceof Error ? error.message : String(error)
|
|
836
|
+
message: error instanceof Error ? error.message : String(error || '')
|
|
744
837
|
}
|
|
745
838
|
};
|
|
746
839
|
}
|
|
747
|
-
const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
|
|
840
|
+
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
748
841
|
const routeId = resolved.route.route_id || '';
|
|
842
|
+
const setCookies = Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : [];
|
|
749
843
|
if (verboseLogging) {
|
|
750
|
-
logger.router(`${routeId || resolved.route.path} guard=${trace.guard} load=${trace.load}`);
|
|
844
|
+
logger.router(`${routeId || resolved.route.path} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
751
845
|
}
|
|
752
846
|
const result = routeExecution?.result;
|
|
753
847
|
if (result && result.kind === 'redirect') {
|
|
754
848
|
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
755
|
-
res.writeHead(status, {
|
|
756
|
-
Location: result.location,
|
|
849
|
+
res.writeHead(status, appendSetCookieHeaders({
|
|
850
|
+
Location: appLocalRedirectLocation(result.location, configuredBasePath),
|
|
757
851
|
'Cache-Control': 'no-store'
|
|
758
|
-
});
|
|
852
|
+
}, setCookies));
|
|
759
853
|
res.end('');
|
|
760
854
|
return;
|
|
761
855
|
}
|
|
762
856
|
if (result && result.kind === 'deny') {
|
|
763
857
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
764
|
-
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
765
|
-
res.end(result.message
|
|
858
|
+
res.writeHead(status, appendSetCookieHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }, setCookies));
|
|
859
|
+
res.end(clientFacingRouteMessage(status, result.message));
|
|
766
860
|
return;
|
|
767
861
|
}
|
|
768
862
|
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
@@ -770,21 +864,24 @@ export async function createDevServer(options) {
|
|
|
770
864
|
}
|
|
771
865
|
}
|
|
772
866
|
let content = await _readFileForRequest(filePath, 'utf8');
|
|
773
|
-
if (resolved.matched
|
|
774
|
-
const pageAssetPath = resolveWithinDist(outDir, resolved.route.page_asset);
|
|
867
|
+
if (resolved.matched) {
|
|
775
868
|
content = await materializeImageMarkup({
|
|
776
869
|
html: content,
|
|
777
|
-
pageAssetPath,
|
|
778
870
|
payload: buildSession.getImageRuntimePayload(),
|
|
779
|
-
|
|
780
|
-
|
|
871
|
+
imageMaterialization: Array.isArray(resolved.route?.image_materialization)
|
|
872
|
+
? resolved.route.image_materialization
|
|
873
|
+
: []
|
|
781
874
|
});
|
|
782
875
|
}
|
|
783
876
|
if (ssrPayload) {
|
|
784
877
|
content = injectSsrPayload(content, ssrPayload);
|
|
785
878
|
}
|
|
786
|
-
|
|
787
|
-
|
|
879
|
+
if (!IMAGE_RUNTIME_TAG_RE.test(content)) {
|
|
880
|
+
content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
|
|
881
|
+
}
|
|
882
|
+
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, appendSetCookieHeaders({
|
|
883
|
+
'Content-Type': 'text/html'
|
|
884
|
+
}, Array.isArray(routeExecution?.setCookies) ? routeExecution.setCookies : []));
|
|
788
885
|
res.end(content);
|
|
789
886
|
}
|
|
790
887
|
catch (error) {
|
|
@@ -907,7 +1004,7 @@ export async function createDevServer(options) {
|
|
|
907
1004
|
const buildResult = await buildSession.build({ changedFiles, logger });
|
|
908
1005
|
const cssReady = await _syncCssStateFromBuild(buildResult, cycleBuildId);
|
|
909
1006
|
if (!onlyCss) {
|
|
910
|
-
|
|
1007
|
+
currentRouteState = await loadRouteSurfaceState(outDir, configuredBasePath);
|
|
911
1008
|
}
|
|
912
1009
|
const cssChanged = cssReady && (currentCssAssetPath !== previousCssAssetPath ||
|
|
913
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
|
+
}
|
|
@@ -335,4 +335,4 @@ const imageHtml = renderImage();
|
|
|
335
335
|
const imagePayload = serializeImageProps();
|
|
336
336
|
</script>
|
|
337
337
|
|
|
338
|
-
<span class="contents" data-zenith-image={imagePayload}
|
|
338
|
+
<span class="contents" data-zenith-image={imagePayload} unsafeHTML={imageHtml}></span>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Image props are evaluated in the Rust compiler (`zenith-compiler --merge-image-materialization`).
|
|
3
|
+
* See `mergePageImageMaterialization` in `build/compiler-runtime.js` and `packages/compiler/zenith_compiler/src/image_materialization.rs`.
|
|
4
|
+
* Legacy CLI-side TypeScript reconstruction was removed in Phase 2 Track B Sub-step 2.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
@@ -3,12 +3,14 @@ type ImagePayload = {
|
|
|
3
3
|
config: Record<string, unknown>;
|
|
4
4
|
localImages: Record<string, unknown>;
|
|
5
5
|
};
|
|
6
|
+
type ImageMaterializationEntry = {
|
|
7
|
+
selector?: string;
|
|
8
|
+
props?: Record<string, unknown> | null;
|
|
9
|
+
};
|
|
6
10
|
export declare function materializeImageMarkup(options: {
|
|
7
11
|
html: string;
|
|
8
|
-
pageAssetPath?: string | null;
|
|
9
12
|
payload: ImagePayload;
|
|
10
|
-
|
|
11
|
-
routePathname?: string;
|
|
13
|
+
imageMaterialization?: ImageMaterializationEntry[] | null;
|
|
12
14
|
}): Promise<string>;
|
|
13
15
|
export declare function materializeImageMarkupInHtmlFiles(options: {
|
|
14
16
|
distDir: string;
|