@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.
Files changed (54) hide show
  1. package/README.md +14 -11
  2. package/dist/adapters/adapter-netlify.js +1 -0
  3. package/dist/adapters/adapter-node.js +8 -0
  4. package/dist/adapters/adapter-vercel.js +1 -0
  5. package/dist/build/compiler-runtime.d.ts +10 -9
  6. package/dist/build/compiler-runtime.js +51 -1
  7. package/dist/build/compiler-signal-expression.d.ts +1 -0
  8. package/dist/build/compiler-signal-expression.js +155 -0
  9. package/dist/build/expression-rewrites.d.ts +1 -6
  10. package/dist/build/expression-rewrites.js +61 -65
  11. package/dist/build/page-component-loop.d.ts +3 -13
  12. package/dist/build/page-component-loop.js +21 -46
  13. package/dist/build/page-ir-normalization.d.ts +0 -8
  14. package/dist/build/page-ir-normalization.js +13 -234
  15. package/dist/build/page-loop-state.d.ts +6 -9
  16. package/dist/build/page-loop-state.js +9 -8
  17. package/dist/build/page-loop.js +27 -22
  18. package/dist/build/scoped-identifier-rewrite.d.ts +37 -44
  19. package/dist/build/scoped-identifier-rewrite.js +28 -128
  20. package/dist/build/server-script.d.ts +2 -1
  21. package/dist/build/server-script.js +29 -3
  22. package/dist/build.js +5 -3
  23. package/dist/component-instance-ir.js +158 -52
  24. package/dist/dev-build-session.js +20 -6
  25. package/dist/dev-server.js +82 -39
  26. package/dist/framework-components/Image.zen +1 -1
  27. package/dist/images/materialization-plan.d.ts +1 -0
  28. package/dist/images/materialization-plan.js +6 -0
  29. package/dist/images/materialize.d.ts +5 -3
  30. package/dist/images/materialize.js +24 -109
  31. package/dist/images/router-manifest.d.ts +1 -0
  32. package/dist/images/router-manifest.js +49 -0
  33. package/dist/index.js +8 -2
  34. package/dist/manifest.js +3 -2
  35. package/dist/preview.d.ts +4 -3
  36. package/dist/preview.js +87 -53
  37. package/dist/request-body.d.ts +2 -0
  38. package/dist/request-body.js +13 -0
  39. package/dist/request-origin.d.ts +2 -0
  40. package/dist/request-origin.js +45 -0
  41. package/dist/route-check-support.d.ts +1 -0
  42. package/dist/route-check-support.js +4 -0
  43. package/dist/server-contract.d.ts +15 -0
  44. package/dist/server-contract.js +102 -32
  45. package/dist/server-error.d.ts +4 -0
  46. package/dist/server-error.js +34 -0
  47. package/dist/server-output.d.ts +2 -0
  48. package/dist/server-output.js +13 -0
  49. package/dist/server-runtime/node-server.js +33 -27
  50. package/dist/server-runtime/route-render.d.ts +3 -3
  51. package/dist/server-runtime/route-render.js +20 -31
  52. package/dist/server-script-composition.d.ts +11 -5
  53. package/dist/server-script-composition.js +25 -10
  54. package/package.json +6 -3
@@ -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 { defaultRouteDenyMessage, executeServerRoute, injectSsrPayload, loadRouteManifest, resolveWithinDist, toStaticFilePath } from './preview.js';
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
- if (host === '0.0.0.0' || host === '::') {
94
- return '127.0.0.1';
95
- }
96
- return host;
109
+ return publicHost(host);
97
110
  }
98
111
  function _serverOrigin() {
99
- return `http://${_publicHost()}:${actualPort}`;
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(pathname);
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 requestBase = typeof req.headers.host === 'string' && req.headers.host.length > 0
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 === '/__zenith_hmr') {
474
+ if (pathname === LEGACY_DEV_STREAM_PATH) {
462
475
  res.writeHead(200, {
463
- 'Content-Type': 'text/event-stream',
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': 'text/event-stream',
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 === '/_zenith/image') {
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 === '/__zenith/route-check') {
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(targetUrl, routes);
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 = String(checkResult.result.location || '/');
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 = '/'; // Fallback to root for open redirect attempt
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
- const requestExt = extname(pathname);
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, pathname);
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 resolved = resolveRequestRoute(url, routes);
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, pathname);
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: req.method || 'GET',
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 || defaultRouteDenyMessage(status));
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 && resolved.route?.page_asset) {
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
- ssrData: ssrPayload,
780
- routePathname: resolved.route.path || pathname
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
- content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
787
- res.writeHead(200, { 'Content-Type': 'text/html' });
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} innerHTML={imageHtml}></span>
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
- ssrData?: Record<string, unknown> | null;
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
- const RUNTIME_EXPORTS = {
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, '&amp;')
23
- .replace(/"/g, '&quot;')
24
- .replace(/</g, '&lt;')
25
- .replace(/>/g, '&gt;');
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}="${escapeHtml(value)}"`;
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, (match, tagName, attrs) => {
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 stripModuleSyntax(source) {
69
- let next = source.replace(/^import\s+[^;]+;\s*$/gm, '');
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 buildExpressionContext(ssrData) {
112
- return {
113
- signalMap: new Map(),
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, pageAssetPath, payload, ssrData = null, routePathname = '/' } = options;
124
- if (!pageAssetPath || !html.includes('data-zx-data-zenith-image')) {
125
- return html;
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 context = buildExpressionContext(ssrData);
140
- for (const binding of bindings) {
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 value = exprFn(context);
152
- if (marker.attr === 'innerHTML') {
153
- nextHtml = applyInnerHtmlMarker(nextHtml, marker.selector, value);
154
- continue;
155
- }
156
- nextHtml = applyAttributeMarker(nextHtml, marker.selector, marker.attr || '', value);
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
- const assetPath = typeof route.page_asset === 'string' ? route.page_asset.replace(/^\//, '') : '';
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
- routePathname: typeof route.path === 'string' ? route.path : '/'
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 || !COMMANDS.includes(command)) {
91
+ if (!command) {
92
92
  printUsage(logger);
93
- process.exit(command ? 1 : 0);
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>}