@zenithbuild/cli 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/adapter-netlify.d.ts +1 -1
  3. package/dist/adapters/adapter-netlify.js +56 -14
  4. package/dist/adapters/adapter-static-export.d.ts +5 -0
  5. package/dist/adapters/adapter-static-export.js +115 -0
  6. package/dist/adapters/adapter-types.d.ts +3 -1
  7. package/dist/adapters/adapter-types.js +5 -2
  8. package/dist/adapters/adapter-vercel.d.ts +1 -1
  9. package/dist/adapters/adapter-vercel.js +70 -14
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +49 -0
  12. package/dist/adapters/resolve-adapter.js +4 -0
  13. package/dist/adapters/route-rules.d.ts +5 -0
  14. package/dist/adapters/route-rules.js +9 -0
  15. package/dist/adapters/validate-hosted-resource-routes.d.ts +1 -0
  16. package/dist/adapters/validate-hosted-resource-routes.js +13 -0
  17. package/dist/auth/route-auth.d.ts +6 -0
  18. package/dist/auth/route-auth.js +236 -0
  19. package/dist/build/compiler-runtime.d.ts +1 -1
  20. package/dist/build/compiler-runtime.js +8 -2
  21. package/dist/build/page-loop-state.js +1 -1
  22. package/dist/build/server-script.d.ts +2 -1
  23. package/dist/build/server-script.js +7 -3
  24. package/dist/build-output-manifest.d.ts +3 -2
  25. package/dist/build-output-manifest.js +3 -0
  26. package/dist/build.js +29 -17
  27. package/dist/dev-server.js +79 -25
  28. package/dist/download-result.d.ts +14 -0
  29. package/dist/download-result.js +148 -0
  30. package/dist/images/service.d.ts +13 -1
  31. package/dist/images/service.js +45 -15
  32. package/dist/manifest.d.ts +15 -1
  33. package/dist/manifest.js +24 -5
  34. package/dist/preview.d.ts +11 -3
  35. package/dist/preview.js +188 -62
  36. package/dist/request-body.d.ts +0 -1
  37. package/dist/request-body.js +0 -6
  38. package/dist/resource-manifest.d.ts +16 -0
  39. package/dist/resource-manifest.js +53 -0
  40. package/dist/resource-response.d.ts +34 -0
  41. package/dist/resource-response.js +71 -0
  42. package/dist/resource-route-module.d.ts +15 -0
  43. package/dist/resource-route-module.js +129 -0
  44. package/dist/route-check-support.js +1 -1
  45. package/dist/server-contract.d.ts +24 -16
  46. package/dist/server-contract.js +217 -25
  47. package/dist/server-error.d.ts +1 -1
  48. package/dist/server-error.js +2 -0
  49. package/dist/server-output.d.ts +2 -1
  50. package/dist/server-output.js +59 -11
  51. package/dist/server-runtime/node-server.js +34 -4
  52. package/dist/server-runtime/route-render.d.ts +25 -1
  53. package/dist/server-runtime/route-render.js +81 -29
  54. package/dist/server-script-composition.d.ts +4 -2
  55. package/dist/server-script-composition.js +6 -3
  56. package/dist/static-export-paths.d.ts +3 -0
  57. package/dist/static-export-paths.js +160 -0
  58. package/package.json +3 -3
@@ -0,0 +1,236 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { assertJsonSerializable } from '../server-contract.js';
3
+ export const SESSION_COOKIE_NAME = 'zenith_session';
4
+ export const SESSION_SECRET_ENV = 'ZENITH_SESSION_SECRET';
5
+ export const STAGED_SET_COOKIES_KEY = '__zenith_staged_set_cookies';
6
+ export const AUTH_CONTROL_FLOW_FLAG = '__zenith_auth_control_flow';
7
+ const SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
8
+ const SESSION_COOKIE_MAX_BYTES = 3800;
9
+ const SESSION_SCHEMA_VERSION = 1;
10
+ function createAuthError(message) {
11
+ return new Error(`[Zenith] ${message}`);
12
+ }
13
+ function base64urlEncode(input) {
14
+ return Buffer.from(input)
15
+ .toString('base64')
16
+ .replace(/\+/g, '-')
17
+ .replace(/\//g, '_')
18
+ .replace(/=+$/g, '');
19
+ }
20
+ function base64urlDecode(input) {
21
+ const normalized = String(input || '')
22
+ .replace(/-/g, '+')
23
+ .replace(/_/g, '/');
24
+ const padded = normalized + '='.repeat((4 - (normalized.length % 4 || 4)) % 4);
25
+ return Buffer.from(padded, 'base64').toString('utf8');
26
+ }
27
+ function isPlainObject(value) {
28
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
+ return false;
30
+ }
31
+ const proto = Object.getPrototypeOf(value);
32
+ return proto === null || proto === Object.prototype || proto?.constructor?.name === 'Object';
33
+ }
34
+ function readSessionSecret() {
35
+ const secret = typeof process?.env?.[SESSION_SECRET_ENV] === 'string'
36
+ ? process.env[SESSION_SECRET_ENV].trim()
37
+ : '';
38
+ if (!secret) {
39
+ throw createAuthError(`ctx.auth requires ${SESSION_SECRET_ENV} to be set`);
40
+ }
41
+ return secret;
42
+ }
43
+ function signPayload(payload, secret) {
44
+ return createHmac('sha256', secret).update(payload).digest('base64url');
45
+ }
46
+ function createSignedSessionValue(session, secret) {
47
+ if (!isPlainObject(session)) {
48
+ throw createAuthError('ctx.auth.signIn(sessionObject) requires a JSON-safe plain object');
49
+ }
50
+ assertJsonSerializable(session, 'ctx.auth.signIn(sessionObject)');
51
+ const envelope = {
52
+ v: SESSION_SCHEMA_VERSION,
53
+ exp: Date.now() + SESSION_COOKIE_MAX_AGE_SECONDS * 1000,
54
+ session
55
+ };
56
+ const json = JSON.stringify(envelope);
57
+ const encodedPayload = base64urlEncode(json);
58
+ const signature = signPayload(encodedPayload, secret);
59
+ const token = `${encodedPayload}.${signature}`;
60
+ if (Buffer.byteLength(token, 'utf8') > SESSION_COOKIE_MAX_BYTES) {
61
+ throw createAuthError('ctx.auth.signIn(sessionObject) produced an oversized session cookie');
62
+ }
63
+ return token;
64
+ }
65
+ function parseSessionValue(rawValue, secret) {
66
+ if (typeof rawValue !== 'string' || rawValue.length === 0) {
67
+ return null;
68
+ }
69
+ const dot = rawValue.lastIndexOf('.');
70
+ if (dot <= 0 || dot === rawValue.length - 1) {
71
+ return null;
72
+ }
73
+ const encodedPayload = rawValue.slice(0, dot);
74
+ const receivedSignature = rawValue.slice(dot + 1);
75
+ const expectedSignature = signPayload(encodedPayload, secret);
76
+ const received = Buffer.from(receivedSignature);
77
+ const expected = Buffer.from(expectedSignature);
78
+ if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
79
+ return null;
80
+ }
81
+ try {
82
+ const envelope = JSON.parse(base64urlDecode(encodedPayload));
83
+ if (!envelope || typeof envelope !== 'object' || envelope.v !== SESSION_SCHEMA_VERSION) {
84
+ return null;
85
+ }
86
+ if (!Number.isFinite(envelope.exp) || envelope.exp <= Date.now()) {
87
+ return null;
88
+ }
89
+ if (!isPlainObject(envelope.session)) {
90
+ return null;
91
+ }
92
+ assertJsonSerializable(envelope.session, 'ctx.auth session payload');
93
+ return envelope.session;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ function shouldUseSecureCookies(requestUrl) {
100
+ try {
101
+ const url = requestUrl instanceof URL ? requestUrl : new URL(String(requestUrl || 'http://localhost/'));
102
+ return url.protocol === 'https:';
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ function buildCookieAttributes(requestUrl) {
109
+ const attributes = ['Path=/', 'HttpOnly', 'SameSite=Lax'];
110
+ if (shouldUseSecureCookies(requestUrl)) {
111
+ attributes.push('Secure');
112
+ }
113
+ return attributes;
114
+ }
115
+ function buildSessionSetCookie(value, requestUrl) {
116
+ const attributes = buildCookieAttributes(requestUrl);
117
+ attributes.push(`Max-Age=${SESSION_COOKIE_MAX_AGE_SECONDS}`);
118
+ attributes.push(`Expires=${new Date(Date.now() + SESSION_COOKIE_MAX_AGE_SECONDS * 1000).toUTCString()}`);
119
+ return `${SESSION_COOKIE_NAME}=${encodeURIComponent(value)}; ${attributes.join('; ')}`;
120
+ }
121
+ function buildSessionClearCookie(requestUrl) {
122
+ const attributes = buildCookieAttributes(requestUrl);
123
+ attributes.push('Max-Age=0');
124
+ attributes.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');
125
+ return `${SESSION_COOKIE_NAME}=; ${attributes.join('; ')}`;
126
+ }
127
+ function stageSetCookie(ctx, value) {
128
+ if (!Array.isArray(ctx[STAGED_SET_COOKIES_KEY])) {
129
+ Object.defineProperty(ctx, STAGED_SET_COOKIES_KEY, {
130
+ value: [],
131
+ enumerable: false,
132
+ configurable: true
133
+ });
134
+ }
135
+ ctx[STAGED_SET_COOKIES_KEY].push(value);
136
+ }
137
+ function toAuthControlFlow(result) {
138
+ const error = new Error('auth control flow');
139
+ error[AUTH_CONTROL_FLOW_FLAG] = true;
140
+ error.result = result;
141
+ return error;
142
+ }
143
+ function normalizeRequirePolicy(options, redirect, deny) {
144
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
145
+ throw createAuthError('ctx.auth.requireSession(...) requires an explicit redirect or deny policy object');
146
+ }
147
+ const hasRedirect = typeof options.redirectTo === 'string' && options.redirectTo.trim().length > 0;
148
+ const hasDeny = Number.isInteger(options.deny);
149
+ if (hasRedirect === hasDeny) {
150
+ throw createAuthError('ctx.auth.requireSession(...) requires exactly one of redirectTo or deny');
151
+ }
152
+ if (hasRedirect) {
153
+ const status = options.status === undefined ? 302 : options.status;
154
+ if (status !== 302 && status !== 303 && status !== 307) {
155
+ throw createAuthError('ctx.auth.requireSession({ redirectTo, status }) only supports 302, 303, or 307');
156
+ }
157
+ return redirect(options.redirectTo, status);
158
+ }
159
+ if (options.deny !== 401 && options.deny !== 403 && options.deny !== 404) {
160
+ throw createAuthError('ctx.auth.requireSession({ deny, message }) only supports 401, 403, or 404');
161
+ }
162
+ if (options.message !== undefined && typeof options.message !== 'string') {
163
+ throw createAuthError('ctx.auth.requireSession({ deny, message }) requires message to be a string when provided');
164
+ }
165
+ return deny(options.deny, options.message);
166
+ }
167
+ export function consumeStagedSetCookies(ctx) {
168
+ if (!ctx || typeof ctx !== 'object' || !Array.isArray(ctx[STAGED_SET_COOKIES_KEY])) {
169
+ return [];
170
+ }
171
+ return ctx[STAGED_SET_COOKIES_KEY].slice();
172
+ }
173
+ export function attachRouteAuth(ctx, options = {}) {
174
+ if (!ctx || typeof ctx !== 'object') {
175
+ throw createAuthError('attachRouteAuth(ctx) requires a route context object');
176
+ }
177
+ const requestUrl = options.requestUrl instanceof URL
178
+ ? options.requestUrl
179
+ : new URL(String(options.requestUrl || ctx.url || 'http://localhost/'));
180
+ const guardOnly = options.guardOnly === true;
181
+ const redirect = options.redirect;
182
+ const deny = options.deny;
183
+ if (typeof redirect !== 'function' || typeof deny !== 'function') {
184
+ throw createAuthError('attachRouteAuth(ctx) requires redirect() and deny() constructors');
185
+ }
186
+ let activeSession;
187
+ let activeSessionInitialized = false;
188
+ function readActiveSession() {
189
+ if (activeSessionInitialized) {
190
+ return activeSession;
191
+ }
192
+ const secret = readSessionSecret();
193
+ const rawCookieValue = typeof ctx.cookies?.[SESSION_COOKIE_NAME] === 'string'
194
+ ? ctx.cookies[SESSION_COOKIE_NAME]
195
+ : '';
196
+ activeSession = parseSessionValue(rawCookieValue, secret);
197
+ activeSessionInitialized = true;
198
+ return activeSession;
199
+ }
200
+ Object.defineProperty(ctx, STAGED_SET_COOKIES_KEY, {
201
+ value: [],
202
+ enumerable: false,
203
+ configurable: true
204
+ });
205
+ ctx.auth = {
206
+ async getSession() {
207
+ return readActiveSession();
208
+ },
209
+ async requireSession(policy) {
210
+ const session = await this.getSession();
211
+ if (session) {
212
+ return session;
213
+ }
214
+ throw toAuthControlFlow(normalizeRequirePolicy(policy, redirect, deny));
215
+ },
216
+ async signIn(sessionObject) {
217
+ if (guardOnly) {
218
+ throw createAuthError('ctx.auth.signIn(...) is unavailable during advisory route-check execution');
219
+ }
220
+ const secret = readSessionSecret();
221
+ const cookieValue = createSignedSessionValue(sessionObject, secret);
222
+ stageSetCookie(ctx, buildSessionSetCookie(cookieValue, requestUrl));
223
+ activeSession = sessionObject;
224
+ activeSessionInitialized = true;
225
+ },
226
+ async signOut() {
227
+ if (guardOnly) {
228
+ throw createAuthError('ctx.auth.signOut() is unavailable during advisory route-check execution');
229
+ }
230
+ stageSetCookie(ctx, buildSessionClearCookie(requestUrl));
231
+ activeSession = null;
232
+ activeSessionInitialized = true;
233
+ }
234
+ };
235
+ return ctx.auth;
236
+ }
@@ -42,7 +42,7 @@ export function createTimedCompilerRunner(startupProfile: ReturnType<typeof impo
42
42
  * @param {object | null} [logger]
43
43
  * @param {boolean} [showInfo]
44
44
  * @param {string|object} [bundlerBin]
45
- * @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string, basePath?: string, routeCheck?: boolean }} [bundlerOptions]
45
+ * @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string, basePath?: string, routeCheck?: boolean, imageRuntimePayload?: object }} [bundlerOptions]
46
46
  * @returns {Promise<void>}
47
47
  */
48
48
  /**
@@ -171,7 +171,7 @@ export function createTimedCompilerRunner(startupProfile, compilerTotals) {
171
171
  * @param {object | null} [logger]
172
172
  * @param {boolean} [showInfo]
173
173
  * @param {string|object} [bundlerBin]
174
- * @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string, basePath?: string, routeCheck?: boolean }} [bundlerOptions]
174
+ * @param {{ devStableAssets?: boolean, rebuildStrategy?: 'full'|'bundle-only'|'page-only', changedRoutes?: string[], fastPath?: boolean, globalGraphHash?: string, basePath?: string, routeCheck?: boolean, imageRuntimePayload?: object }} [bundlerOptions]
175
175
  * @returns {Promise<void>}
176
176
  */
177
177
  /**
@@ -284,7 +284,13 @@ export function runBundler(envelope, outDir, projectRoot, logger = null, showInf
284
284
  }
285
285
  rejectPromise(new Error(`Bundler failed with exit code ${code}`));
286
286
  });
287
- child.stdin.write(JSON.stringify(envelope));
287
+ const bundlerPayload = bundlerOptions.imageRuntimePayload
288
+ ? {
289
+ inputs: Array.isArray(envelope) ? envelope : [envelope],
290
+ image_runtime_payload: bundlerOptions.imageRuntimePayload
291
+ }
292
+ : envelope;
293
+ child.stdin.write(JSON.stringify(bundlerPayload));
288
294
  child.stdin.end();
289
295
  });
290
296
  }
@@ -48,7 +48,7 @@ export function preparePageIrForMerge(pageIr) {
48
48
  }
49
49
  export function applyServerEnvelopeToPageIr({ pageIr, composedServer, hasGuard, hasLoad, hasAction, entry, srcDir, sourceFile }) {
50
50
  if (composedServer.serverScript) {
51
- const { has_action: _unusedHasAction, ...serverScript } = composedServer.serverScript;
51
+ const { has_action: _unusedHasAction, export_paths: _unusedExportPaths, ...serverScript } = composedServer.serverScript;
52
52
  pageIr.server_script = serverScript;
53
53
  pageIr.prerender = composedServer.serverScript.prerender === true;
54
54
  if (pageIr.ssr_data === undefined) {
@@ -2,7 +2,7 @@
2
2
  * @param {string} source
3
3
  * @param {string} sourceFile
4
4
  * @param {object} [compilerOpts]
5
- * @returns {{ source: string, serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null }}
5
+ * @returns {{ source: string, serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null }}
6
6
  */
7
7
  export function extractServerScript(source: string, sourceFile: string, compilerOpts?: object): {
8
8
  source: string;
@@ -13,6 +13,7 @@ export function extractServerScript(source: string, sourceFile: string, compiler
13
13
  has_load: boolean;
14
14
  has_action: boolean;
15
15
  source_path: string;
16
+ export_paths?: string[];
16
17
  } | null;
17
18
  };
18
19
  /**
@@ -1,10 +1,11 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { findNextKnownComponentTag } from '../component-tag-parser.js';
3
+ import { extractStaticExportPaths } from '../static-export-paths.js';
3
4
  /**
4
5
  * @param {string} source
5
6
  * @param {string} sourceFile
6
7
  * @param {object} [compilerOpts]
7
- * @returns {{ source: string, serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string } | null }}
8
+ * @returns {{ source: string, serverScript: { source: string, prerender: boolean, has_guard: boolean, has_load: boolean, has_action: boolean, source_path: string, export_paths?: string[] } | null }}
8
9
  */
9
10
  export function extractServerScript(source, sourceFile, compilerOpts = {}) {
10
11
  const scriptRe = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
@@ -155,6 +156,7 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
155
156
  }
156
157
  prerender = rawValue.startsWith('true');
157
158
  }
159
+ const exportPaths = extractStaticExportPaths(serverSource, sourceFile) || [];
158
160
  const start = match.index ?? -1;
159
161
  if (start < 0) {
160
162
  return {
@@ -165,7 +167,8 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
165
167
  has_guard: hasGuard,
166
168
  has_load: hasLoad,
167
169
  has_action: hasAction,
168
- source_path: sourceFile
170
+ source_path: sourceFile,
171
+ export_paths: exportPaths
169
172
  }
170
173
  };
171
174
  }
@@ -179,7 +182,8 @@ export function extractServerScript(source, sourceFile, compilerOpts = {}) {
179
182
  has_guard: hasGuard,
180
183
  has_load: hasLoad,
181
184
  has_action: hasAction,
182
- source_path: sourceFile
185
+ source_path: sourceFile,
186
+ export_paths: exportPaths
183
187
  }
184
188
  };
185
189
  }
@@ -11,14 +11,15 @@ export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, rou
11
11
  base_path: string;
12
12
  content_hash: any;
13
13
  routes: {
14
+ html: any;
15
+ assets: any[];
16
+ export_paths?: any[] | undefined;
14
17
  path: any;
15
18
  file: any;
16
19
  path_kind: any;
17
20
  render_mode: any;
18
21
  requires_hydration: boolean;
19
22
  params: any[];
20
- html: any;
21
- assets: any[];
22
23
  }[];
23
24
  assets: {
24
25
  js: any[];
@@ -67,6 +67,9 @@ export async function writeBuildOutputManifest({ coreOutputDir, staticDir, targe
67
67
  render_mode: entry.render_mode,
68
68
  requires_hydration: /<script\b[^>]*type="module"/i.test(html),
69
69
  params: [...entry.params],
70
+ ...(Array.isArray(entry.export_paths) && entry.export_paths.length > 0
71
+ ? { export_paths: [...entry.export_paths] }
72
+ : {}),
70
73
  html: htmlPath,
71
74
  assets
72
75
  });
package/dist/build.js CHANGED
@@ -1,15 +1,15 @@
1
- import { mkdir, rm } from 'node:fs/promises';
1
+ import { cp, mkdir, rm } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { resolveBuildAdapter } from './adapters/resolve-adapter.js';
4
4
  import { normalizeBasePath } from './base-path.js';
5
5
  import { rewriteSoftNavigationHrefBasePathInHtmlFiles } from './base-path-html.js';
6
6
  import { writeBuildOutputManifest } from './build-output-manifest.js';
7
7
  import { generateManifest } from './manifest.js';
8
+ import { writeResourceRouteManifest } from './resource-manifest.js';
8
9
  import { buildComponentRegistry } from './resolve-components.js';
9
10
  import { collectAssets, createCompilerWarningEmitter, runBundler } from './build/compiler-runtime.js';
10
11
  import { buildPageEnvelopes } from './build/page-loop.js';
11
12
  import { deriveProjectRootFromPagesDir, ensureZenithTypeDeclarations } from './build/type-declarations.js';
12
- import { materializeImageMarkupInHtmlFiles } from './images/materialize.js';
13
13
  import { buildImageArtifacts } from './images/service.js';
14
14
  import { injectImageMaterializationIntoRouterManifest } from './images/router-manifest.js';
15
15
  import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from './images/payload.js';
@@ -50,6 +50,7 @@ export async function build(options) {
50
50
  const projectRoot = deriveProjectRootFromPagesDir(pagesDir);
51
51
  const coreOutputDir = join(projectRoot, '.zenith-output');
52
52
  const staticOutputDir = join(coreOutputDir, 'static');
53
+ const imageStageDir = join(coreOutputDir, 'image-materialization-stage');
53
54
  const srcDir = resolve(pagesDir, '..');
54
55
  const compilerBin = createCompilerToolchain({ projectRoot, logger });
55
56
  const bundlerBin = createBundlerToolchain({ projectRoot, logger });
@@ -75,11 +76,12 @@ export async function build(options) {
75
76
  }
76
77
  const registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
77
78
  const manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(pagesDir, '.zen', { compilerOpts }));
79
+ const pageManifest = manifest.filter((entry) => entry?.route_kind !== 'resource');
78
80
  if (mode !== 'legacy') {
79
81
  adapter.validateRoutes(manifest);
80
82
  }
81
83
  await startupProfile.measureAsync('ensure_zenith_type_declarations', () => ensureZenithTypeDeclarations({
82
- manifest,
84
+ manifest: pageManifest,
83
85
  pagesDir
84
86
  }));
85
87
  await startupProfile.measureAsync('reset_core_output', () => rm(coreOutputDir, { recursive: true, force: true }));
@@ -92,7 +94,7 @@ export async function build(options) {
92
94
  console.warn(line);
93
95
  });
94
96
  const { envelopes, expressionRewriteMetrics } = await buildPageEnvelopes({
95
- manifest,
97
+ manifest: pageManifest,
96
98
  pagesDir,
97
99
  srcDir,
98
100
  registry,
@@ -103,27 +105,37 @@ export async function build(options) {
103
105
  compilerTotals,
104
106
  emitCompilerWarning
105
107
  });
106
- if (envelopes.length > 0) {
107
- await startupProfile.measureAsync('run_bundler', () => runBundler(envelopes, staticOutputDir, projectRoot, logger, showBundlerInfo, bundlerBin, { basePath, routeCheck: routeCheckEnabled }), { envelopes: envelopes.length });
108
- await startupProfile.measureAsync('inject_image_materialization_manifest', () => injectImageMaterializationIntoRouterManifest(staticOutputDir, envelopes), { envelopes: envelopes.length });
109
- }
110
- await startupProfile.measureAsync('rewrite_soft_navigation_base_path', () => rewriteSoftNavigationHrefBasePathInHtmlFiles(staticOutputDir, basePath));
111
108
  const { manifest: imageManifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
112
109
  projectRoot,
113
- outDir: staticOutputDir,
110
+ outDir: imageStageDir,
114
111
  config: config.images
115
112
  }));
116
113
  const imageRuntimePayload = createImageRuntimePayload(config.images, imageManifest, 'passthrough', basePath);
117
- await startupProfile.measureAsync('materialize_image_markup', () => materializeImageMarkupInHtmlFiles({
118
- distDir: staticOutputDir,
119
- payload: imageRuntimePayload
120
- }));
114
+ if (envelopes.length > 0) {
115
+ await startupProfile.measureAsync('run_bundler', () => runBundler(envelopes, staticOutputDir, projectRoot, logger, showBundlerInfo, bundlerBin, {
116
+ basePath,
117
+ routeCheck: routeCheckEnabled,
118
+ imageRuntimePayload
119
+ }), { envelopes: envelopes.length });
120
+ await startupProfile.measureAsync('inject_image_materialization_manifest', () => injectImageMaterializationIntoRouterManifest(staticOutputDir, envelopes), { envelopes: envelopes.length });
121
+ }
122
+ await startupProfile.measureAsync('write_resource_manifest', () => writeResourceRouteManifest(staticOutputDir, manifest, basePath));
123
+ await startupProfile.measureAsync('rewrite_soft_navigation_base_path', () => rewriteSoftNavigationHrefBasePathInHtmlFiles(staticOutputDir, basePath));
124
+ await startupProfile.measureAsync('stage_image_artifacts_into_static_output', async () => {
125
+ if (Object.keys(imageManifest).length === 0) {
126
+ return;
127
+ }
128
+ await cp(join(imageStageDir, '_zenith'), join(staticOutputDir, '_zenith'), {
129
+ recursive: true,
130
+ force: true
131
+ });
132
+ });
121
133
  await startupProfile.measureAsync('inject_image_runtime_payload', () => injectImageRuntimePayloadIntoHtmlFiles(staticOutputDir, imageRuntimePayload));
122
134
  const buildManifest = await startupProfile.measureAsync('write_core_manifest', () => writeBuildOutputManifest({
123
135
  coreOutputDir,
124
136
  staticDir: staticOutputDir,
125
137
  target,
126
- routeManifest: manifest,
138
+ routeManifest: pageManifest,
127
139
  basePath
128
140
  }));
129
141
  await startupProfile.measureAsync('write_server_output', () => writeServerOutput({
@@ -136,11 +148,11 @@ export async function build(options) {
136
148
  await startupProfile.measureAsync('adapt_output', () => adapter.adapt({ coreOutput: coreOutputDir, outDir, manifest: buildManifest, config }));
137
149
  const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
138
150
  startupProfile.emit('build_complete', {
139
- pages: manifest.length,
151
+ pages: pageManifest.length,
140
152
  assets: assets.length,
141
153
  target,
142
154
  compilerTotals,
143
155
  expressionRewriteMetrics
144
156
  });
145
- return { pages: manifest.length, assets };
157
+ return { pages: pageManifest.length, assets };
146
158
  }