@zenithbuild/cli 0.7.4 → 0.7.7

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 (112) 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 +48 -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 +67 -19
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +1 -0
  11. package/dist/adapters/copy-hosted-page-runtime.js +50 -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/hoisted-code-transforms.d.ts +4 -1
  22. package/dist/build/hoisted-code-transforms.js +5 -3
  23. package/dist/build/page-ir-normalization.d.ts +1 -1
  24. package/dist/build/page-ir-normalization.js +33 -3
  25. package/dist/build/page-loop-state.js +1 -1
  26. package/dist/build/page-loop.js +46 -2
  27. package/dist/build/server-script.d.ts +2 -1
  28. package/dist/build/server-script.js +7 -3
  29. package/dist/build-output-manifest.d.ts +3 -2
  30. package/dist/build-output-manifest.js +3 -0
  31. package/dist/build.js +29 -17
  32. package/dist/dev-build-session/helpers.d.ts +29 -0
  33. package/dist/dev-build-session/helpers.js +223 -0
  34. package/dist/dev-build-session/session.d.ts +24 -0
  35. package/dist/dev-build-session/session.js +204 -0
  36. package/dist/dev-build-session/state.d.ts +37 -0
  37. package/dist/dev-build-session/state.js +17 -0
  38. package/dist/dev-build-session.d.ts +1 -24
  39. package/dist/dev-build-session.js +1 -434
  40. package/dist/dev-server/css-state.d.ts +7 -0
  41. package/dist/dev-server/css-state.js +92 -0
  42. package/dist/dev-server/not-found.d.ts +23 -0
  43. package/dist/dev-server/not-found.js +129 -0
  44. package/dist/dev-server/request-handler.d.ts +1 -0
  45. package/dist/dev-server/request-handler.js +376 -0
  46. package/dist/dev-server/route-check.d.ts +9 -0
  47. package/dist/dev-server/route-check.js +100 -0
  48. package/dist/dev-server/watcher.d.ts +5 -0
  49. package/dist/dev-server/watcher.js +216 -0
  50. package/dist/dev-server.js +136 -883
  51. package/dist/download-result.d.ts +14 -0
  52. package/dist/download-result.js +148 -0
  53. package/dist/images/payload.js +4 -0
  54. package/dist/images/service.d.ts +13 -1
  55. package/dist/images/service.js +45 -15
  56. package/dist/manifest.d.ts +15 -1
  57. package/dist/manifest.js +70 -6
  58. package/dist/preview/create-preview-server.d.ts +18 -0
  59. package/dist/preview/create-preview-server.js +71 -0
  60. package/dist/preview/manifest.d.ts +42 -0
  61. package/dist/preview/manifest.js +57 -0
  62. package/dist/preview/paths.d.ts +3 -0
  63. package/dist/preview/paths.js +38 -0
  64. package/dist/preview/payload.d.ts +6 -0
  65. package/dist/preview/payload.js +34 -0
  66. package/dist/preview/request-handler.d.ts +1 -0
  67. package/dist/preview/request-handler.js +300 -0
  68. package/dist/preview/server-runner.d.ts +49 -0
  69. package/dist/preview/server-runner.js +220 -0
  70. package/dist/preview/server-script-runner-template.d.ts +1 -0
  71. package/dist/preview/server-script-runner-template.js +425 -0
  72. package/dist/preview.d.ts +5 -104
  73. package/dist/preview.js +7 -993
  74. package/dist/request-body.d.ts +0 -1
  75. package/dist/request-body.js +0 -6
  76. package/dist/resource-manifest.d.ts +16 -0
  77. package/dist/resource-manifest.js +53 -0
  78. package/dist/resource-response.d.ts +49 -0
  79. package/dist/resource-response.js +160 -0
  80. package/dist/resource-route-module.d.ts +15 -0
  81. package/dist/resource-route-module.js +129 -0
  82. package/dist/route-check-support.js +1 -1
  83. package/dist/server-contract/constants.d.ts +5 -0
  84. package/dist/server-contract/constants.js +5 -0
  85. package/dist/server-contract/export-validation.d.ts +5 -0
  86. package/dist/server-contract/export-validation.js +59 -0
  87. package/dist/server-contract/json-serializable.d.ts +1 -0
  88. package/dist/server-contract/json-serializable.js +52 -0
  89. package/dist/server-contract/resolve.d.ts +15 -0
  90. package/dist/server-contract/resolve.js +271 -0
  91. package/dist/server-contract/result-helpers.d.ts +51 -0
  92. package/dist/server-contract/result-helpers.js +59 -0
  93. package/dist/server-contract/route-result-validation.d.ts +2 -0
  94. package/dist/server-contract/route-result-validation.js +73 -0
  95. package/dist/server-contract/stage.d.ts +6 -0
  96. package/dist/server-contract/stage.js +22 -0
  97. package/dist/server-contract.d.ts +6 -54
  98. package/dist/server-contract.js +9 -301
  99. package/dist/server-error.d.ts +1 -1
  100. package/dist/server-error.js +2 -0
  101. package/dist/server-middleware.d.ts +10 -0
  102. package/dist/server-middleware.js +30 -0
  103. package/dist/server-output.d.ts +2 -1
  104. package/dist/server-output.js +72 -12
  105. package/dist/server-runtime/node-server.js +59 -7
  106. package/dist/server-runtime/route-render.d.ts +25 -1
  107. package/dist/server-runtime/route-render.js +81 -29
  108. package/dist/server-script-composition.d.ts +4 -2
  109. package/dist/server-script-composition.js +6 -3
  110. package/dist/static-export-paths.d.ts +3 -0
  111. package/dist/static-export-paths.js +160 -0
  112. 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
  }
@@ -21,9 +21,12 @@ export function rewriteStaticImportsInSource(source: string, fromFile: string, t
21
21
  * @param {string} sourceFile
22
22
  * @param {object | null} [transformCache]
23
23
  * @param {Record<string, number> | null} [mergeMetrics]
24
+ * @param {{ target?: 'es5' | 'esnext' }} [options]
24
25
  * @returns {string}
25
26
  */
26
- export function transpileTypeScriptToJs(source: string, sourceFile: string, transformCache?: object | null, mergeMetrics?: Record<string, number> | null): string;
27
+ export function transpileTypeScriptToJs(source: string, sourceFile: string, transformCache?: object | null, mergeMetrics?: Record<string, number> | null, options?: {
28
+ target?: "es5" | "esnext";
29
+ }): string;
27
30
  /**
28
31
  * @param {string} source
29
32
  * @param {Set<string>} seenStaticImports
@@ -76,11 +76,13 @@ export function rewriteStaticImportsInSource(source, fromFile, toFile) {
76
76
  * @param {string} sourceFile
77
77
  * @param {object | null} [transformCache]
78
78
  * @param {Record<string, number> | null} [mergeMetrics]
79
+ * @param {{ target?: 'es5' | 'esnext' }} [options]
79
80
  * @returns {string}
80
81
  */
81
- export function transpileTypeScriptToJs(source, sourceFile, transformCache = null, mergeMetrics = null) {
82
+ export function transpileTypeScriptToJs(source, sourceFile, transformCache = null, mergeMetrics = null, options = {}) {
83
+ const target = options?.target === 'esnext' ? 'esnext' : 'es5';
82
84
  const cacheKey = transformCache?.transpileToJs instanceof Map
83
- ? `${sourceFile}\u0000${source}`
85
+ ? `${sourceFile}\u0000${target}\u0000${source}`
84
86
  : null;
85
87
  if (cacheKey && transformCache.transpileToJs.has(cacheKey)) {
86
88
  if (mergeMetrics && typeof mergeMetrics === 'object') {
@@ -97,7 +99,7 @@ export function transpileTypeScriptToJs(source, sourceFile, transformCache = nul
97
99
  fileName: sourceFile,
98
100
  compilerOptions: {
99
101
  module: ts.ModuleKind.ESNext,
100
- target: ts.ScriptTarget.ES5,
102
+ target: target === 'esnext' ? ts.ScriptTarget.ESNext : ts.ScriptTarget.ES5,
101
103
  importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
102
104
  verbatimModuleSyntax: true,
103
105
  newLine: ts.NewLineKind.LineFeed,
@@ -11,5 +11,5 @@ export function rewriteRefBindingIdentifiers(pageIr: object, preferredKeys?: Set
11
11
  */
12
12
  export function applyExpressionRewrites(pageIr: object, expressionMap: Map<string, string>, bindingMap: Map<string, object>, ambiguous: Set<string>): void;
13
13
  export function normalizeExpressionPayload(pageIr: any): void;
14
- export function normalizeHoistedSourcePayload(pageIr: any): void;
14
+ export function normalizeHoistedSourcePayload(pageIr: any, sourceFile?: string, transformCache?: null, mergeMetrics?: null): void;
15
15
  export function rewriteLegacyMarkupIdentifiers(pageIr: any): void;
@@ -1,4 +1,5 @@
1
1
  import { resolveStateKeyFromBindings } from './expression-rewrites.js';
2
+ import { transpileTypeScriptToJs } from './hoisted-code-transforms.js';
2
3
  import { expandScopedShorthandPropertiesInSource, normalizeTypeScriptExpression } from './typescript-expression-utils.js';
3
4
  /**
4
5
  * @param {object} pageIr
@@ -89,7 +90,7 @@ export function normalizeExpressionPayload(pageIr) {
89
90
  }
90
91
  }
91
92
  }
92
- export function normalizeHoistedSourcePayload(pageIr) {
93
+ export function normalizeHoistedSourcePayload(pageIr, sourceFile = 'component.zen', transformCache = null, mergeMetrics = null) {
93
94
  const declarations = Array.isArray(pageIr?.hoisted?.declarations) ? pageIr.hoisted.declarations : null;
94
95
  if (declarations) {
95
96
  pageIr.hoisted.declarations = declarations.map((entry) => {
@@ -101,11 +102,40 @@ export function normalizeHoistedSourcePayload(pageIr) {
101
102
  }
102
103
  const codeBlocks = Array.isArray(pageIr?.hoisted?.code) ? pageIr.hoisted.code : null;
103
104
  if (codeBlocks) {
104
- pageIr.hoisted.code = codeBlocks.map((entry) => {
105
+ pageIr.hoisted.code = codeBlocks.map((entry, index) => {
105
106
  if (typeof entry !== 'string') {
106
107
  return entry;
107
108
  }
108
- return expandScopedShorthandPropertiesInSource(entry);
109
+ const expanded = expandScopedShorthandPropertiesInSource(entry);
110
+ return transpileTypeScriptToJs(expanded, `${sourceFile}#hoisted-${index}.ts`, transformCache, mergeMetrics, { target: 'esnext' });
111
+ });
112
+ }
113
+ const componentScripts = pageIr?.components_scripts && typeof pageIr.components_scripts === 'object'
114
+ ? pageIr.components_scripts
115
+ : null;
116
+ if (componentScripts) {
117
+ for (const [hoistId, script] of Object.entries(componentScripts)) {
118
+ if (!script || typeof script !== 'object' || typeof script.code !== 'string') {
119
+ continue;
120
+ }
121
+ const expanded = expandScopedShorthandPropertiesInSource(script.code);
122
+ script.code = transpileTypeScriptToJs(expanded, `${sourceFile}#component-${hoistId}.ts`, transformCache, mergeMetrics, { target: 'esnext' });
123
+ }
124
+ }
125
+ const modules = Array.isArray(pageIr?.modules) ? pageIr.modules : null;
126
+ if (modules) {
127
+ pageIr.modules = modules.map((module, index) => {
128
+ if (!module || typeof module !== 'object' || typeof module.source !== 'string') {
129
+ return module;
130
+ }
131
+ const moduleId = typeof module.id === 'string' && module.id.length > 0
132
+ ? module.id
133
+ : `${sourceFile}#module-${index}.ts`;
134
+ const expanded = expandScopedShorthandPropertiesInSource(module.source);
135
+ return {
136
+ ...module,
137
+ source: transpileTypeScriptToJs(expanded, moduleId, transformCache, mergeMetrics, { target: 'esnext' })
138
+ };
109
139
  });
110
140
  }
111
141
  }
@@ -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) {
@@ -165,7 +165,7 @@ export async function buildPageEnvelopes(input) {
165
165
  pagePhase.expressionApplyMs = startupProfile.roundMs(performance.now() - expressionApplyStartedAt);
166
166
  const normalizeStartedAt = performance.now();
167
167
  normalizeExpressionPayload(pageIr);
168
- normalizeHoistedSourcePayload(pageIr);
168
+ normalizeHoistedSourcePayload(pageIr, sourceFile, hoistedCodeTransformCache, expressionRewriteMetrics);
169
169
  if (Array.isArray(pageIr?.hoisted?.code) && pageIr.hoisted.code.length > 0) {
170
170
  pageIr.hoisted.code = pageIr.hoisted.code
171
171
  .map((entry) => deferComponentRuntimeBlock(entry, hoistedCodeTransformCache, expressionRewriteMetrics))
@@ -174,6 +174,26 @@ export async function buildPageEnvelopes(input) {
174
174
  rewriteLegacyMarkupIdentifiers(pageIr);
175
175
  rewriteRefBindingIdentifiers(pageIr, knownRefKeys);
176
176
  pagePhase.normalizeMs = startupProfile.roundMs(performance.now() - normalizeStartedAt);
177
+ const requiresJs = detectRequiresJs(pageIr, routerEnabled);
178
+ if (!requiresJs) {
179
+ console.log(`[DEBUG] Route ${entry.path} is STATIC (no JS required)`);
180
+ }
181
+ else {
182
+ const reasons = [];
183
+ if (routerEnabled)
184
+ reasons.push('routerEnabled');
185
+ if (pageIr.signals.length > 0)
186
+ reasons.push('signals');
187
+ if (pageIr.event_bindings.length > 0)
188
+ reasons.push('event_bindings');
189
+ if (pageIr.marker_bindings.length > 0)
190
+ reasons.push('marker_bindings');
191
+ if (pageIr.component_instances.length > 0)
192
+ reasons.push('component_instances');
193
+ if (pageIr.hoisted?.code?.length > 0)
194
+ reasons.push('hoisted.code');
195
+ console.log(`[DEBUG] Route ${entry.path} is INTERACTIVE. Reasons: ${reasons.join(', ')}`);
196
+ }
177
197
  addBreakdown(pagePhaseTotals, pagePhase);
178
198
  addBreakdown(occurrenceApplyPhaseTotals, pageOccurrenceApplyBreakdown);
179
199
  addBreakdown(bindingResolutionTotals, pageBindingResolutionBreakdown);
@@ -187,7 +207,8 @@ export async function buildPageEnvelopes(input) {
187
207
  image_materialization: Array.isArray(pageIr.image_materialization)
188
208
  ? pageIr.image_materialization
189
209
  : [],
190
- router: routerEnabled
210
+ router: routerEnabled,
211
+ requires_js: requiresJs
191
212
  });
192
213
  recordPageProfile({
193
214
  pageProfiles,
@@ -220,3 +241,26 @@ export async function buildPageEnvelopes(input) {
220
241
  });
221
242
  return { envelopes, expressionRewriteMetrics };
222
243
  }
244
+ /**
245
+ * Detects if a page requires client-side JavaScript based on its IR.
246
+ * This is a conservative pass used for Static Route Omission.
247
+ *
248
+ * @param {object} pageIr
249
+ * @param {boolean} routerEnabled
250
+ * @returns {boolean}
251
+ */
252
+ function detectRequiresJs(pageIr, routerEnabled) {
253
+ if (routerEnabled === true) {
254
+ return true;
255
+ }
256
+ const { signals = [], event_bindings = [], ref_bindings = [], marker_bindings = [], component_instances = [], hoisted = {} } = pageIr;
257
+ const requiresJs = (signals.length > 0 ||
258
+ event_bindings.length > 0 ||
259
+ ref_bindings.length > 0 ||
260
+ (Array.isArray(hoisted.signals) && hoisted.signals.length > 0) ||
261
+ (Array.isArray(hoisted.state) && hoisted.state.length > 0) ||
262
+ (Array.isArray(hoisted.code) && hoisted.code.filter(c => String(c).trim().length > 0).length > 0) ||
263
+ component_instances.some(instance => Array.isArray(instance.props) &&
264
+ instance.props.some(prop => prop.type === 'signal' || prop.type === 'binding' || prop.type === 'callback' || prop.type === 'reactive')));
265
+ return requiresJs;
266
+ }
@@ -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
  }