@zenithbuild/cli 0.7.3 → 0.7.4
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 +14 -11
- package/dist/adapters/adapter-netlify.js +1 -0
- package/dist/adapters/adapter-node.js +8 -0
- package/dist/adapters/adapter-vercel.js +1 -0
- package/dist/build/compiler-runtime.d.ts +10 -9
- package/dist/build/compiler-runtime.js +51 -1
- 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 +2 -1
- package/dist/build/server-script.js +29 -3
- package/dist/build.js +5 -3
- package/dist/component-instance-ir.js +158 -52
- package/dist/dev-build-session.js +20 -6
- package/dist/dev-server.js +82 -39
- 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/index.js +8 -2
- package/dist/manifest.js +3 -2
- package/dist/preview.d.ts +4 -3
- package/dist/preview.js +87 -53
- package/dist/request-body.d.ts +2 -0
- package/dist/request-body.js +13 -0
- package/dist/request-origin.d.ts +2 -0
- package/dist/request-origin.js +45 -0
- package/dist/route-check-support.d.ts +1 -0
- package/dist/route-check-support.js +4 -0
- package/dist/server-contract.d.ts +15 -0
- package/dist/server-contract.js +102 -32
- package/dist/server-error.d.ts +4 -0
- package/dist/server-error.js +34 -0
- package/dist/server-output.d.ts +2 -0
- package/dist/server-output.js +13 -0
- package/dist/server-runtime/node-server.js +33 -27
- package/dist/server-runtime/route-render.d.ts +3 -3
- package/dist/server-runtime/route-render.js +20 -31
- package/dist/server-script-composition.d.ts +11 -5
- package/dist/server-script-composition.js +25 -10
- package/package.json +6 -3
package/dist/dev-server.js
CHANGED
|
@@ -15,11 +15,17 @@ 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 { encodeRequestBodyBase64, readRequestBodyBuffer } from './request-body.js';
|
|
26
|
+
import { supportsTargetRouteCheck } from './route-check-support.js';
|
|
27
|
+
import { clientFacingRouteMessage, defaultRouteDenyMessage, logServerException, sanitizeRouteResult } from './server-error.js';
|
|
28
|
+
import { executeServerRoute, injectSsrPayload, loadRouteManifest, resolveWithinDist, toStaticFilePath } from './preview.js';
|
|
23
29
|
import { materializeImageMarkup } from './images/materialize.js';
|
|
24
30
|
import { injectImageRuntimePayload } from './images/payload.js';
|
|
25
31
|
import { handleImageRequest } from './images/service.js';
|
|
@@ -37,6 +43,9 @@ const MIME_TYPES = {
|
|
|
37
43
|
'.avif': 'image/avif',
|
|
38
44
|
'.gif': 'image/gif'
|
|
39
45
|
};
|
|
46
|
+
const IMAGE_RUNTIME_TAG_RE = new RegExp('<' + 'script\\b[^>]*\\bid=(["\'])zenith-image-runtime\\1[^>]*>[\\s\\S]*?<\\/' + 'script>', 'i');
|
|
47
|
+
const EVENT_STREAM_MIME = ['text', 'event-stream'].join('/');
|
|
48
|
+
const LEGACY_DEV_STREAM_PATH = ['/__zenith', '_hmr'].join('');
|
|
40
49
|
// Note: V0 HMR script injection has been moved to the runtime client.
|
|
41
50
|
// This server purely hosts the V1 HMR contract endpoints.
|
|
42
51
|
/**
|
|
@@ -50,6 +59,8 @@ export async function createDevServer(options) {
|
|
|
50
59
|
const { pagesDir, outDir, port = 3000, host = '127.0.0.1', config = {}, logger: providedLogger = null } = options;
|
|
51
60
|
const logger = providedLogger || createSilentLogger();
|
|
52
61
|
const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
|
|
62
|
+
const configuredBasePath = normalizeBasePath(config.basePath || '/');
|
|
63
|
+
const routeCheckEnabled = supportsTargetRouteCheck(resolveBuildAdapter(config).target);
|
|
53
64
|
const resolvedPagesDir = resolve(pagesDir);
|
|
54
65
|
const resolvedOutDir = resolve(outDir);
|
|
55
66
|
const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
|
|
@@ -86,17 +97,19 @@ export async function createDevServer(options) {
|
|
|
86
97
|
let currentCssHref = '';
|
|
87
98
|
let currentCssContent = '';
|
|
88
99
|
let actualPort = port;
|
|
100
|
+
const resolveServerOrigin = createTrustedOriginResolver({
|
|
101
|
+
host,
|
|
102
|
+
getPort: () => actualPort,
|
|
103
|
+
label: 'dev server'
|
|
104
|
+
});
|
|
89
105
|
let currentRoutes = [];
|
|
90
106
|
const rebuildDebounceMs = 5;
|
|
91
107
|
const queuedRebuildDebounceMs = 5;
|
|
92
108
|
function _publicHost() {
|
|
93
|
-
|
|
94
|
-
return '127.0.0.1';
|
|
95
|
-
}
|
|
96
|
-
return host;
|
|
109
|
+
return publicHost(host);
|
|
97
110
|
}
|
|
98
111
|
function _serverOrigin() {
|
|
99
|
-
return
|
|
112
|
+
return resolveServerOrigin();
|
|
100
113
|
}
|
|
101
114
|
function _trace(event, payload = {}) {
|
|
102
115
|
if (!traceEnabled)
|
|
@@ -208,6 +221,9 @@ export async function createDevServer(options) {
|
|
|
208
221
|
throw lastError;
|
|
209
222
|
}
|
|
210
223
|
function _buildNotFoundPayload(pathname, category, cause) {
|
|
224
|
+
const hintedPath = category === 'page'
|
|
225
|
+
? (stripBasePath(pathname, configuredBasePath) || pathname)
|
|
226
|
+
: pathname;
|
|
211
227
|
const payload = {
|
|
212
228
|
kind: 'zenith_dev_not_found',
|
|
213
229
|
category,
|
|
@@ -235,7 +251,7 @@ export async function createDevServer(options) {
|
|
|
235
251
|
payload.docsLink = '/docs/documentation/contracts/hmr-v1-contract.md';
|
|
236
252
|
return payload;
|
|
237
253
|
}
|
|
238
|
-
const routeFile = _routeFileHint(
|
|
254
|
+
const routeFile = _routeFileHint(hintedPath);
|
|
239
255
|
payload.routeFile = routeFile;
|
|
240
256
|
payload.cause = `no route file found at ${routeFile}`;
|
|
241
257
|
payload.hint = `Create ${routeFile} or verify router manifest output.`;
|
|
@@ -452,15 +468,12 @@ export async function createDevServer(options) {
|
|
|
452
468
|
}
|
|
453
469
|
}
|
|
454
470
|
const server = createServer(async (req, res) => {
|
|
455
|
-
const
|
|
456
|
-
? `http://${req.headers.host}`
|
|
457
|
-
: _serverOrigin();
|
|
458
|
-
const url = new URL(req.url, requestBase);
|
|
471
|
+
const url = new URL(req.url, _serverOrigin());
|
|
459
472
|
let pathname = url.pathname;
|
|
460
473
|
// Legacy HMR endpoint (deprecated but kept alive to avoid breaking old caches instantly)
|
|
461
|
-
if (pathname ===
|
|
474
|
+
if (pathname === LEGACY_DEV_STREAM_PATH) {
|
|
462
475
|
res.writeHead(200, {
|
|
463
|
-
'Content-Type':
|
|
476
|
+
'Content-Type': EVENT_STREAM_MIME,
|
|
464
477
|
'Cache-Control': 'no-store',
|
|
465
478
|
'Connection': 'keep-alive',
|
|
466
479
|
'X-Zenith-Deprecated': 'true'
|
|
@@ -498,7 +511,7 @@ export async function createDevServer(options) {
|
|
|
498
511
|
// V1 Dev Events Endpoint (SSE)
|
|
499
512
|
if (pathname === '/__zenith_dev/events') {
|
|
500
513
|
res.writeHead(200, {
|
|
501
|
-
'Content-Type':
|
|
514
|
+
'Content-Type': EVENT_STREAM_MIME,
|
|
502
515
|
'Cache-Control': 'no-store',
|
|
503
516
|
'Connection': 'keep-alive',
|
|
504
517
|
'X-Accel-Buffering': 'no'
|
|
@@ -565,7 +578,7 @@ export async function createDevServer(options) {
|
|
|
565
578
|
res.end(currentCssContent);
|
|
566
579
|
return;
|
|
567
580
|
}
|
|
568
|
-
if (pathname ===
|
|
581
|
+
if (pathname === imageEndpointPath(configuredBasePath)) {
|
|
569
582
|
await handleImageRequest(req, res, {
|
|
570
583
|
requestUrl: url,
|
|
571
584
|
projectRoot,
|
|
@@ -573,8 +586,13 @@ export async function createDevServer(options) {
|
|
|
573
586
|
});
|
|
574
587
|
return;
|
|
575
588
|
}
|
|
576
|
-
if (pathname ===
|
|
589
|
+
if (pathname === routeCheckPath(configuredBasePath)) {
|
|
577
590
|
try {
|
|
591
|
+
if (!routeCheckEnabled) {
|
|
592
|
+
res.writeHead(501, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
593
|
+
res.end(JSON.stringify({ error: 'route_check_unsupported' }));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
578
596
|
if (!initialBuildSettled && buildStatus === 'building') {
|
|
579
597
|
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
580
598
|
res.end(JSON.stringify({
|
|
@@ -602,8 +620,16 @@ export async function createDevServer(options) {
|
|
|
602
620
|
res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
|
|
603
621
|
return;
|
|
604
622
|
}
|
|
623
|
+
const canonicalTargetPath = stripBasePath(targetUrl.pathname, configuredBasePath);
|
|
624
|
+
if (canonicalTargetPath === null) {
|
|
625
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
626
|
+
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const canonicalTargetUrl = new URL(targetUrl.toString());
|
|
630
|
+
canonicalTargetUrl.pathname = canonicalTargetPath;
|
|
605
631
|
const routes = await _loadRoutesForRequests();
|
|
606
|
-
const resolvedCheck = resolveRequestRoute(
|
|
632
|
+
const resolvedCheck = resolveRequestRoute(canonicalTargetUrl, routes);
|
|
607
633
|
if (!resolvedCheck.matched || !resolvedCheck.route) {
|
|
608
634
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
609
635
|
res.end(JSON.stringify({ error: 'route_not_found' }));
|
|
@@ -623,16 +649,17 @@ export async function createDevServer(options) {
|
|
|
623
649
|
});
|
|
624
650
|
// Security: Enforce relative or same-origin redirects
|
|
625
651
|
if (checkResult && checkResult.result && checkResult.result.kind === 'redirect') {
|
|
626
|
-
const loc =
|
|
652
|
+
const loc = appLocalRedirectLocation(checkResult.result.location || '/', configuredBasePath);
|
|
653
|
+
checkResult.result.location = loc;
|
|
627
654
|
if (loc.includes('://') || loc.startsWith('//')) {
|
|
628
655
|
try {
|
|
629
656
|
const parsedLoc = new URL(loc);
|
|
630
657
|
if (parsedLoc.origin !== targetUrl.origin) {
|
|
631
|
-
checkResult.result.location = '/';
|
|
658
|
+
checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
|
|
632
659
|
}
|
|
633
660
|
}
|
|
634
661
|
catch {
|
|
635
|
-
checkResult.result.location = '/';
|
|
662
|
+
checkResult.result.location = appLocalRedirectLocation('/', configuredBasePath);
|
|
636
663
|
}
|
|
637
664
|
}
|
|
638
665
|
}
|
|
@@ -644,7 +671,7 @@ export async function createDevServer(options) {
|
|
|
644
671
|
'Vary': 'Cookie'
|
|
645
672
|
});
|
|
646
673
|
res.end(JSON.stringify({
|
|
647
|
-
result: checkResult?.result || checkResult,
|
|
674
|
+
result: sanitizeRouteResult(checkResult?.result || checkResult),
|
|
648
675
|
routeId: resolvedCheck.route.route_id || '',
|
|
649
676
|
to: targetUrl.toString()
|
|
650
677
|
}));
|
|
@@ -659,6 +686,7 @@ export async function createDevServer(options) {
|
|
|
659
686
|
let resolvedPathFor404 = null;
|
|
660
687
|
let staticRootFor404 = null;
|
|
661
688
|
try {
|
|
689
|
+
const canonicalPath = stripBasePath(pathname, configuredBasePath);
|
|
662
690
|
if (!initialBuildSettled && buildStatus === 'building') {
|
|
663
691
|
const pendingPayload = {
|
|
664
692
|
kind: 'zenith_dev_build_pending',
|
|
@@ -689,9 +717,12 @@ export async function createDevServer(options) {
|
|
|
689
717
|
].join(''));
|
|
690
718
|
return;
|
|
691
719
|
}
|
|
692
|
-
|
|
720
|
+
if (canonicalPath === null) {
|
|
721
|
+
throw new Error('not found');
|
|
722
|
+
}
|
|
723
|
+
const requestExt = extname(canonicalPath);
|
|
693
724
|
if (requestExt && requestExt !== '.html') {
|
|
694
|
-
const assetPath = join(outDir,
|
|
725
|
+
const assetPath = join(outDir, canonicalPath);
|
|
695
726
|
resolvedPathFor404 = assetPath;
|
|
696
727
|
staticRootFor404 = outDir;
|
|
697
728
|
const asset = await _readFileForRequest(assetPath);
|
|
@@ -701,7 +732,9 @@ export async function createDevServer(options) {
|
|
|
701
732
|
return;
|
|
702
733
|
}
|
|
703
734
|
const routes = await _loadRoutesForRequests();
|
|
704
|
-
const
|
|
735
|
+
const canonicalUrl = new URL(url.toString());
|
|
736
|
+
canonicalUrl.pathname = canonicalPath;
|
|
737
|
+
const resolved = resolveRequestRoute(canonicalUrl, routes);
|
|
705
738
|
let filePath = null;
|
|
706
739
|
if (resolved.matched && resolved.route) {
|
|
707
740
|
if (verboseLogging) {
|
|
@@ -713,7 +746,7 @@ export async function createDevServer(options) {
|
|
|
713
746
|
filePath = resolveWithinDist(outDir, output);
|
|
714
747
|
}
|
|
715
748
|
else {
|
|
716
|
-
filePath = toStaticFilePath(outDir,
|
|
749
|
+
filePath = toStaticFilePath(outDir, canonicalPath);
|
|
717
750
|
}
|
|
718
751
|
resolvedPathFor404 = filePath;
|
|
719
752
|
staticRootFor404 = outDir;
|
|
@@ -721,39 +754,46 @@ export async function createDevServer(options) {
|
|
|
721
754
|
throw new Error('not found');
|
|
722
755
|
}
|
|
723
756
|
let ssrPayload = null;
|
|
757
|
+
let routeExecution = null;
|
|
724
758
|
if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
|
|
725
|
-
let routeExecution = null;
|
|
726
759
|
try {
|
|
760
|
+
const requestMethod = req.method || 'GET';
|
|
761
|
+
const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
|
|
762
|
+
? null
|
|
763
|
+
: await readRequestBodyBuffer(req);
|
|
727
764
|
routeExecution = await executeServerRoute({
|
|
728
765
|
source: resolved.route.server_script,
|
|
729
766
|
sourcePath: resolved.route.server_script_path || '',
|
|
730
767
|
params: resolved.params,
|
|
731
768
|
requestUrl: url.toString(),
|
|
732
|
-
requestMethod
|
|
769
|
+
requestMethod,
|
|
733
770
|
requestHeaders: req.headers,
|
|
771
|
+
requestBodyBase64: encodeRequestBodyBase64(requestBodyBuffer),
|
|
734
772
|
routePattern: resolved.route.path,
|
|
735
773
|
routeFile: resolved.route.server_script_path || '',
|
|
736
774
|
routeId: resolved.route.route_id || ''
|
|
737
775
|
});
|
|
738
776
|
}
|
|
739
777
|
catch (error) {
|
|
778
|
+
logServerException('dev server route execution failed', error);
|
|
740
779
|
ssrPayload = {
|
|
741
780
|
__zenith_error: {
|
|
781
|
+
status: 500,
|
|
742
782
|
code: 'LOAD_FAILED',
|
|
743
|
-
message: error instanceof Error ? error.message : String(error)
|
|
783
|
+
message: error instanceof Error ? error.message : String(error || '')
|
|
744
784
|
}
|
|
745
785
|
};
|
|
746
786
|
}
|
|
747
|
-
const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
|
|
787
|
+
const trace = routeExecution?.trace || { guard: 'none', action: 'none', load: 'none' };
|
|
748
788
|
const routeId = resolved.route.route_id || '';
|
|
749
789
|
if (verboseLogging) {
|
|
750
|
-
logger.router(`${routeId || resolved.route.path} guard=${trace.guard} load=${trace.load}`);
|
|
790
|
+
logger.router(`${routeId || resolved.route.path} guard=${trace.guard} action=${trace.action} load=${trace.load}`);
|
|
751
791
|
}
|
|
752
792
|
const result = routeExecution?.result;
|
|
753
793
|
if (result && result.kind === 'redirect') {
|
|
754
794
|
const status = Number.isInteger(result.status) ? result.status : 302;
|
|
755
795
|
res.writeHead(status, {
|
|
756
|
-
Location: result.location,
|
|
796
|
+
Location: appLocalRedirectLocation(result.location, configuredBasePath),
|
|
757
797
|
'Cache-Control': 'no-store'
|
|
758
798
|
});
|
|
759
799
|
res.end('');
|
|
@@ -762,7 +802,7 @@ export async function createDevServer(options) {
|
|
|
762
802
|
if (result && result.kind === 'deny') {
|
|
763
803
|
const status = Number.isInteger(result.status) ? result.status : 403;
|
|
764
804
|
res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
765
|
-
res.end(result.message
|
|
805
|
+
res.end(clientFacingRouteMessage(status, result.message));
|
|
766
806
|
return;
|
|
767
807
|
}
|
|
768
808
|
if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
@@ -770,21 +810,24 @@ export async function createDevServer(options) {
|
|
|
770
810
|
}
|
|
771
811
|
}
|
|
772
812
|
let content = await _readFileForRequest(filePath, 'utf8');
|
|
773
|
-
if (resolved.matched
|
|
774
|
-
const pageAssetPath = resolveWithinDist(outDir, resolved.route.page_asset);
|
|
813
|
+
if (resolved.matched) {
|
|
775
814
|
content = await materializeImageMarkup({
|
|
776
815
|
html: content,
|
|
777
|
-
pageAssetPath,
|
|
778
816
|
payload: buildSession.getImageRuntimePayload(),
|
|
779
|
-
|
|
780
|
-
|
|
817
|
+
imageMaterialization: Array.isArray(resolved.route?.image_materialization)
|
|
818
|
+
? resolved.route.image_materialization
|
|
819
|
+
: []
|
|
781
820
|
});
|
|
782
821
|
}
|
|
783
822
|
if (ssrPayload) {
|
|
784
823
|
content = injectSsrPayload(content, ssrPayload);
|
|
785
824
|
}
|
|
786
|
-
|
|
787
|
-
|
|
825
|
+
if (!IMAGE_RUNTIME_TAG_RE.test(content)) {
|
|
826
|
+
content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
|
|
827
|
+
}
|
|
828
|
+
res.writeHead(Number.isInteger(routeExecution?.status) ? routeExecution.status : 200, {
|
|
829
|
+
'Content-Type': 'text/html'
|
|
830
|
+
});
|
|
788
831
|
res.end(content);
|
|
789
832
|
}
|
|
790
833
|
catch (error) {
|
|
@@ -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;
|
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
hydrate: () => () => { },
|
|
5
|
-
signal: (value) => value,
|
|
6
|
-
state: (value) => value,
|
|
7
|
-
ref: () => ({ current: null }),
|
|
8
|
-
zeneffect: () => () => { },
|
|
9
|
-
zenEffect: () => () => { },
|
|
10
|
-
zenMount: () => { },
|
|
11
|
-
zenWindow: () => undefined,
|
|
12
|
-
zenDocument: () => undefined,
|
|
13
|
-
zenOn: () => () => { },
|
|
14
|
-
zenResize: () => () => { },
|
|
15
|
-
collectRefs: (...refs) => refs.filter(Boolean)
|
|
16
|
-
};
|
|
3
|
+
import { renderImageHtmlWithPayload, replaceImageMarkers, serializeImageProps } from './runtime.js';
|
|
17
4
|
function escapeRegex(value) {
|
|
18
5
|
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
19
6
|
}
|
|
20
|
-
function escapeHtml(value) {
|
|
21
|
-
return String(value ?? '')
|
|
22
|
-
.replace(/&/g, '&')
|
|
23
|
-
.replace(/"/g, '"')
|
|
24
|
-
.replace(/</g, '<')
|
|
25
|
-
.replace(/>/g, '>');
|
|
26
|
-
}
|
|
27
7
|
function parseMarkerSelector(selector) {
|
|
28
8
|
const match = selector.match(/^\[([^\]=]+)=["']([^"']+)["']\]$/);
|
|
29
9
|
if (!match)
|
|
@@ -41,7 +21,7 @@ function upsertAttributeMarkup(attributes, attrName, value) {
|
|
|
41
21
|
if (value === null || value === undefined || value === false || value === '') {
|
|
42
22
|
return attributes.replace(attrPattern, '');
|
|
43
23
|
}
|
|
44
|
-
const serialized = ` ${trimmedName}="${
|
|
24
|
+
const serialized = ` ${trimmedName}="${String(value)}"`;
|
|
45
25
|
if (attrPattern.test(attributes)) {
|
|
46
26
|
return attributes.replace(attrPattern, serialized);
|
|
47
27
|
}
|
|
@@ -52,7 +32,7 @@ function applyAttributeMarker(html, selector, attrName, value) {
|
|
|
52
32
|
if (!parsed)
|
|
53
33
|
return html;
|
|
54
34
|
const markerRe = new RegExp(`<([A-Za-z][\\w:-]*)([^>]*\\s${escapeRegex(parsed.attrName)}=(["'])${escapeRegex(parsed.attrValue)}\\3[^>]*)>`, 'g');
|
|
55
|
-
return html.replace(markerRe, (
|
|
35
|
+
return html.replace(markerRe, (_match, tagName, attrs) => {
|
|
56
36
|
const nextAttrs = upsertAttributeMarkup(String(attrs || ''), attrName, value);
|
|
57
37
|
return `<${tagName}${nextAttrs}>`;
|
|
58
38
|
});
|
|
@@ -65,95 +45,33 @@ function applyInnerHtmlMarker(html, selector, value) {
|
|
|
65
45
|
const replacement = value === null || value === undefined || value === false ? '' : String(value);
|
|
66
46
|
return html.replace(markerRe, (_match, tagName, attrs) => `<${tagName}${attrs}>${replacement}</${tagName}>`);
|
|
67
47
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
if (/(^|\n)\s*import\s+/m.test(next)) {
|
|
71
|
-
throw new Error('[Zenith:Image] Cannot materialize page asset with unresolved imports');
|
|
72
|
-
}
|
|
73
|
-
next = next.replace(/^export\s+default\s+function\s+/gm, 'function ');
|
|
74
|
-
next = next.replace(/^export\s+function\s+/gm, 'function ');
|
|
75
|
-
next = next.replace(/^export\s+const\s+/gm, 'const ');
|
|
76
|
-
next = next.replace(/^export\s+let\s+/gm, 'let ');
|
|
77
|
-
next = next.replace(/^export\s+var\s+/gm, 'var ');
|
|
78
|
-
next = next.replace(/\bexport\s*\{[^}]*\};?/g, '');
|
|
79
|
-
return next;
|
|
80
|
-
}
|
|
81
|
-
async function evaluatePageModule(assetPath, payload, ssrData, routePathname) {
|
|
82
|
-
const source = stripModuleSyntax(await readFile(assetPath, 'utf8'));
|
|
83
|
-
const runtimeNames = Object.keys(RUNTIME_EXPORTS);
|
|
84
|
-
const evaluator = new Function('runtime', 'payload', 'ssrData', 'routePathname', [
|
|
85
|
-
'"use strict";',
|
|
86
|
-
`const { ${runtimeNames.join(', ')} } = runtime;`,
|
|
87
|
-
'const document = {};',
|
|
88
|
-
'const location = { pathname: routePathname || "/" };',
|
|
89
|
-
'const Document = class ZenithServerDocument {};',
|
|
90
|
-
'const globalThis = {',
|
|
91
|
-
' __zenith_image_runtime: payload,',
|
|
92
|
-
' document,',
|
|
93
|
-
' location,',
|
|
94
|
-
' Document',
|
|
95
|
-
'};',
|
|
96
|
-
'if (ssrData && typeof ssrData === "object" && !Array.isArray(ssrData)) {',
|
|
97
|
-
' globalThis.__zenith_ssr_data = ssrData;',
|
|
98
|
-
'}',
|
|
99
|
-
'globalThis.globalThis = globalThis;',
|
|
100
|
-
'globalThis.window = globalThis;',
|
|
101
|
-
'globalThis.self = globalThis;',
|
|
102
|
-
source,
|
|
103
|
-
'return {',
|
|
104
|
-
' __zenith_markers: typeof __zenith_markers !== "undefined" ? __zenith_markers : [],',
|
|
105
|
-
' __zenith_expression_bindings: typeof __zenith_expression_bindings !== "undefined" ? __zenith_expression_bindings : [],',
|
|
106
|
-
' __zenith_expr_fns: typeof __zenith_expr_fns !== "undefined" ? __zenith_expr_fns : []',
|
|
107
|
-
'};'
|
|
108
|
-
].join('\n'));
|
|
109
|
-
return evaluator(RUNTIME_EXPORTS, payload, ssrData, routePathname);
|
|
48
|
+
function isPlainObject(value) {
|
|
49
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
110
50
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
params: {},
|
|
115
|
-
props: {},
|
|
116
|
-
ssrData: ssrData || {},
|
|
117
|
-
componentBindings: {},
|
|
118
|
-
zenhtml: null,
|
|
119
|
-
fragment: null
|
|
120
|
-
};
|
|
51
|
+
function hasUnmaterializedImageMarkers(html) {
|
|
52
|
+
const matches = html.match(/<span\b[^>]*\bdata-zx-(?:data-zenith-image|unsafeHTML)=(["'])[^"']+\1[^>]*>/gi) || [];
|
|
53
|
+
return matches.some((tag) => /\sdata-zenith-image=/.test(tag) === false);
|
|
121
54
|
}
|
|
122
55
|
export async function materializeImageMarkup(options) {
|
|
123
|
-
const { html,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
const namespace = await evaluatePageModule(pageAssetPath, payload, ssrData, routePathname);
|
|
128
|
-
if (!namespace) {
|
|
129
|
-
return html;
|
|
130
|
-
}
|
|
131
|
-
const markers = Array.isArray(namespace.__zenith_markers) ? namespace.__zenith_markers : [];
|
|
132
|
-
const bindings = Array.isArray(namespace.__zenith_expression_bindings) ? namespace.__zenith_expression_bindings : [];
|
|
133
|
-
const exprFns = Array.isArray(namespace.__zenith_expr_fns) ? namespace.__zenith_expr_fns : [];
|
|
134
|
-
if (markers.length === 0 || bindings.length === 0 || exprFns.length === 0) {
|
|
56
|
+
const { html, payload, imageMaterialization = [] } = options;
|
|
57
|
+
const entries = Array.isArray(imageMaterialization) ? imageMaterialization : [];
|
|
58
|
+
if (typeof html !== 'string' || html.length === 0) {
|
|
135
59
|
return html;
|
|
136
60
|
}
|
|
137
|
-
const markerByIndex = new Map(markers.map((marker) => [marker.index, marker]));
|
|
138
61
|
let nextHtml = html;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
const marker = markerByIndex.get(Number(binding.marker_index));
|
|
142
|
-
const exprFn = Number.isInteger(binding.fn_index) ? exprFns[binding.fn_index] : null;
|
|
143
|
-
if (!marker ||
|
|
144
|
-
typeof exprFn !== 'function' ||
|
|
145
|
-
marker.kind !== 'attr' ||
|
|
146
|
-
typeof marker.selector !== 'string' ||
|
|
147
|
-
marker.selector.includes('data-zx-data-zenith-image') === false &&
|
|
148
|
-
marker.selector.includes('data-zx-innerHTML') === false) {
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry || typeof entry.selector !== 'string' || !isPlainObject(entry.props)) {
|
|
149
64
|
continue;
|
|
150
65
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
66
|
+
const encodedProps = serializeImageProps(entry.props);
|
|
67
|
+
const renderedHtml = renderImageHtmlWithPayload(entry.props, payload);
|
|
68
|
+
nextHtml = applyAttributeMarker(nextHtml, entry.selector, 'data-zenith-image', encodedProps);
|
|
69
|
+
nextHtml = applyInnerHtmlMarker(nextHtml, entry.selector, renderedHtml);
|
|
70
|
+
}
|
|
71
|
+
nextHtml = replaceImageMarkers(nextHtml, payload);
|
|
72
|
+
if (hasUnmaterializedImageMarkers(nextHtml)) {
|
|
73
|
+
throw new Error('[Zenith:Image] Unresolved Image markers require a compiler-owned image materialization artifact. ' +
|
|
74
|
+
'Dynamic image props are currently unsupported.');
|
|
157
75
|
}
|
|
158
76
|
return nextHtml;
|
|
159
77
|
}
|
|
@@ -175,11 +93,9 @@ export async function materializeImageMarkupInHtmlFiles(options) {
|
|
|
175
93
|
continue;
|
|
176
94
|
}
|
|
177
95
|
const outputPath = typeof route.output === 'string' ? route.output.replace(/^\//, '') : '';
|
|
178
|
-
|
|
179
|
-
if (!outputPath || !assetPath)
|
|
96
|
+
if (!outputPath)
|
|
180
97
|
continue;
|
|
181
98
|
const fullHtmlPath = join(distDir, outputPath);
|
|
182
|
-
const fullAssetPath = join(distDir, assetPath);
|
|
183
99
|
let html = '';
|
|
184
100
|
try {
|
|
185
101
|
html = await readFile(fullHtmlPath, 'utf8');
|
|
@@ -189,9 +105,8 @@ export async function materializeImageMarkupInHtmlFiles(options) {
|
|
|
189
105
|
}
|
|
190
106
|
const nextHtml = await materializeImageMarkup({
|
|
191
107
|
html,
|
|
192
|
-
pageAssetPath: fullAssetPath,
|
|
193
108
|
payload,
|
|
194
|
-
|
|
109
|
+
imageMaterialization: Array.isArray(route.image_materialization) ? route.image_materialization : []
|
|
195
110
|
});
|
|
196
111
|
if (nextHtml !== html) {
|
|
197
112
|
await writeFile(fullHtmlPath, nextHtml, 'utf8');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function injectImageMaterializationIntoRouterManifest(distDir: any, envelopes: any): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export async function injectImageMaterializationIntoRouterManifest(distDir, envelopes) {
|
|
4
|
+
const manifestPath = join(distDir, 'assets', 'router-manifest.json');
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const routes = Array.isArray(parsed?.routes) ? parsed.routes : null;
|
|
13
|
+
if (!routes) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const serverMetadataByRoute = new Map();
|
|
17
|
+
for (const envelope of Array.isArray(envelopes) ? envelopes : []) {
|
|
18
|
+
const route = typeof envelope?.route === 'string' ? envelope.route : '';
|
|
19
|
+
if (!route) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const routeIr = envelope?.ir && typeof envelope.ir === 'object' ? envelope.ir : {};
|
|
23
|
+
serverMetadataByRoute.set(route, {
|
|
24
|
+
guard_module_ref: routeIr.guard_module_ref || null,
|
|
25
|
+
load_module_ref: routeIr.load_module_ref || null,
|
|
26
|
+
action_module_ref: routeIr.action_module_ref || null,
|
|
27
|
+
has_guard: routeIr.has_guard === true,
|
|
28
|
+
has_load: routeIr.has_load === true,
|
|
29
|
+
has_action: routeIr.has_action === true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const route of routes) {
|
|
33
|
+
const routePath = typeof route?.path === 'string' ? route.path : '';
|
|
34
|
+
if (!routePath) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const serverMetadata = serverMetadataByRoute.get(routePath);
|
|
38
|
+
if (!serverMetadata) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
route.guard_module_ref = serverMetadata.guard_module_ref;
|
|
42
|
+
route.load_module_ref = serverMetadata.load_module_ref;
|
|
43
|
+
route.action_module_ref = serverMetadata.action_module_ref;
|
|
44
|
+
route.has_guard = serverMetadata.has_guard;
|
|
45
|
+
route.has_load = serverMetadata.has_load;
|
|
46
|
+
route.has_action = serverMetadata.has_action;
|
|
47
|
+
}
|
|
48
|
+
await writeFile(manifestPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
49
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -88,9 +88,15 @@ export async function cli(args, cwd) {
|
|
|
88
88
|
printUsage(logger);
|
|
89
89
|
process.exit(0);
|
|
90
90
|
}
|
|
91
|
-
if (!command
|
|
91
|
+
if (!command) {
|
|
92
92
|
printUsage(logger);
|
|
93
|
-
process.exit(
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
if (!COMMANDS.includes(command)) {
|
|
96
|
+
logger.print(`Unknown command: ${command}`);
|
|
97
|
+
logger.print('');
|
|
98
|
+
printUsage(logger);
|
|
99
|
+
process.exit(1);
|
|
94
100
|
}
|
|
95
101
|
const projectRoot = resolve(cwd || process.cwd());
|
|
96
102
|
const config = await loadConfig(projectRoot);
|
package/dist/manifest.js
CHANGED
|
@@ -88,12 +88,13 @@ async function _scanDir(dir, root, ext, compilerOpts) {
|
|
|
88
88
|
function buildManifestEntry({ fullPath, root, routePath, compilerOpts }) {
|
|
89
89
|
const rawSource = readFileSync(fullPath, 'utf8');
|
|
90
90
|
const inlineServerScript = extractServerScript(rawSource, fullPath, compilerOpts).serverScript;
|
|
91
|
-
const { guardPath, loadPath } = resolveAdjacentServerModules(fullPath);
|
|
91
|
+
const { guardPath, loadPath, actionPath } = resolveAdjacentServerModules(fullPath);
|
|
92
92
|
const composed = composeServerScriptEnvelope({
|
|
93
93
|
sourceFile: fullPath,
|
|
94
94
|
inlineServerScript,
|
|
95
95
|
adjacentGuardPath: guardPath,
|
|
96
|
-
adjacentLoadPath: loadPath
|
|
96
|
+
adjacentLoadPath: loadPath,
|
|
97
|
+
adjacentActionPath: actionPath
|
|
97
98
|
});
|
|
98
99
|
return {
|
|
99
100
|
path: routePath,
|
package/dist/preview.d.ts
CHANGED
|
@@ -39,9 +39,9 @@ export function createPreviewServer(options: {
|
|
|
39
39
|
export function loadRouteManifest(distDir: string): Promise<PreviewRoute[]>;
|
|
40
40
|
/**
|
|
41
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, load: string } }>}
|
|
42
|
+
* @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number }>}
|
|
43
43
|
*/
|
|
44
|
-
export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, routePattern, routeFile, routeId, guardOnly }: {
|
|
44
|
+
export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBase64, routePattern, routeFile, routeId, guardOnly }: {
|
|
45
45
|
source: string;
|
|
46
46
|
sourcePath: string;
|
|
47
47
|
params: Record<string, string>;
|
|
@@ -58,10 +58,11 @@ export function executeServerRoute({ source, sourcePath, params, requestUrl, req
|
|
|
58
58
|
};
|
|
59
59
|
trace: {
|
|
60
60
|
guard: string;
|
|
61
|
+
action: string;
|
|
61
62
|
load: string;
|
|
62
63
|
};
|
|
64
|
+
status?: number;
|
|
63
65
|
}>;
|
|
64
|
-
export function defaultRouteDenyMessage(status: any): "Unauthorized" | "Forbidden" | "Not Found" | "Internal Server Error";
|
|
65
66
|
/**
|
|
66
67
|
* @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
|
|
67
68
|
* @returns {Promise<Record<string, unknown> | null>}
|