@zenithbuild/cli 0.7.10 → 0.7.12

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 (111) hide show
  1. package/README.md +14 -2
  2. package/dist/adapters/adapter-netlify-static.d.ts +2 -5
  3. package/dist/adapters/adapter-netlify.d.ts +2 -5
  4. package/dist/adapters/adapter-netlify.js +22 -5
  5. package/dist/adapters/adapter-types.d.ts +32 -13
  6. package/dist/adapters/adapter-types.js +0 -59
  7. package/dist/adapters/adapter-vercel-static.d.ts +2 -5
  8. package/dist/adapters/adapter-vercel.d.ts +2 -5
  9. package/dist/adapters/adapter-vercel.js +21 -6
  10. package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
  11. package/dist/adapters/copy-hosted-page-runtime.js +68 -3
  12. package/dist/adapters/resolve-adapter.d.ts +6 -4
  13. package/dist/build/compiler-runtime.js +3 -0
  14. package/dist/build/expression-rewrites.d.ts +3 -1
  15. package/dist/build/expression-rewrites.js +14 -2
  16. package/dist/build/page-component-loop.d.ts +1 -0
  17. package/dist/build/page-component-loop.js +66 -6
  18. package/dist/build/page-ir-normalization.js +7 -0
  19. package/dist/build/page-loop-state.d.ts +2 -4
  20. package/dist/build/page-loop-state.js +17 -9
  21. package/dist/build/page-loop.js +18 -8
  22. package/dist/build/scoped-expression-context.d.ts +5 -0
  23. package/dist/build/scoped-expression-context.js +133 -0
  24. package/dist/build/server-script.js +13 -36
  25. package/dist/build/type-declarations.d.ts +2 -1
  26. package/dist/build/type-declarations.js +29 -52
  27. package/dist/build-output-manifest.d.ts +10 -6
  28. package/dist/build-output-manifest.js +4 -1
  29. package/dist/build.js +11 -2
  30. package/dist/component-instance-ir.js +1 -0
  31. package/dist/component-occurrences.d.ts +9 -0
  32. package/dist/component-occurrences.js +18 -0
  33. package/dist/config-plugins.d.ts +12 -0
  34. package/dist/config-plugins.js +100 -0
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.js +56 -5
  37. package/dist/dev-build-session/helpers.js +27 -7
  38. package/dist/dev-build-session/session.js +19 -10
  39. package/dist/dev-server/build-error-response.d.ts +21 -0
  40. package/dist/dev-server/build-error-response.js +48 -0
  41. package/dist/dev-server/port-fallback.d.ts +15 -0
  42. package/dist/dev-server/port-fallback.js +61 -0
  43. package/dist/dev-server/request-handler.js +58 -5
  44. package/dist/dev-server/watcher.js +15 -0
  45. package/dist/dev-server.d.ts +5 -2
  46. package/dist/dev-server.js +129 -49
  47. package/dist/global-middleware-runtime-source.d.ts +15 -0
  48. package/dist/global-middleware-runtime-source.js +62 -0
  49. package/dist/global-middleware.d.ts +13 -0
  50. package/dist/global-middleware.js +252 -0
  51. package/dist/images/remote-fetch.d.ts +12 -0
  52. package/dist/images/remote-fetch.js +257 -0
  53. package/dist/images/service.d.ts +10 -0
  54. package/dist/images/service.js +9 -46
  55. package/dist/index.js +12 -2
  56. package/dist/manifest.d.ts +9 -1
  57. package/dist/manifest.js +70 -25
  58. package/dist/preview/request-handler.js +78 -5
  59. package/dist/preview/server-runner.d.ts +7 -2
  60. package/dist/preview/server-runner.js +19 -6
  61. package/dist/preview/server-script-runner-template.js +97 -29
  62. package/dist/resource-response.js +25 -8
  63. package/dist/resource-route-module.js +5 -22
  64. package/dist/route-classification.d.ts +11 -0
  65. package/dist/route-classification.js +21 -0
  66. package/dist/route-handler-export-analysis.d.ts +22 -0
  67. package/dist/route-handler-export-analysis.js +41 -0
  68. package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
  69. package/dist/scoped-server-data/analyze-owner-file.js +149 -0
  70. package/dist/scoped-server-data/diagnostics.d.ts +18 -0
  71. package/dist/scoped-server-data/diagnostics.js +32 -0
  72. package/dist/scoped-server-data/lowering.d.ts +27 -0
  73. package/dist/scoped-server-data/lowering.js +242 -0
  74. package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
  75. package/dist/scoped-server-data/manifest-integration.js +125 -0
  76. package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
  77. package/dist/scoped-server-data/owner-scanner.js +55 -0
  78. package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
  79. package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
  80. package/dist/scoped-server-data/runtime.d.ts +24 -0
  81. package/dist/scoped-server-data/runtime.js +121 -0
  82. package/dist/scoped-server-data/serialization-set.d.ts +2 -0
  83. package/dist/scoped-server-data/serialization-set.js +52 -0
  84. package/dist/scoped-server-data/static-props.d.ts +12 -0
  85. package/dist/scoped-server-data/static-props.js +307 -0
  86. package/dist/scoped-server-data/type-declarations.d.ts +10 -0
  87. package/dist/scoped-server-data/type-declarations.js +368 -0
  88. package/dist/scoped-server-data/types.d.ts +74 -0
  89. package/dist/scoped-server-data/types.js +1 -0
  90. package/dist/server-contract/auth-control-flow.d.ts +1 -0
  91. package/dist/server-contract/auth-control-flow.js +10 -0
  92. package/dist/server-contract/resolve.d.ts +19 -0
  93. package/dist/server-contract/resolve.js +85 -13
  94. package/dist/server-contract/resolved-envelope.d.ts +9 -0
  95. package/dist/server-contract/resolved-envelope.js +14 -0
  96. package/dist/server-contract/stage.js +1 -10
  97. package/dist/server-module-output.d.ts +9 -0
  98. package/dist/server-module-output.js +250 -0
  99. package/dist/server-output.d.ts +7 -1
  100. package/dist/server-output.js +144 -195
  101. package/dist/server-route-names.d.ts +2 -0
  102. package/dist/server-route-names.js +38 -0
  103. package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
  104. package/dist/server-runtime/matched-route-pipeline.js +1 -0
  105. package/dist/server-runtime/node-server.js +26 -3
  106. package/dist/server-runtime/route-render.d.ts +12 -3
  107. package/dist/server-runtime/route-render.js +67 -13
  108. package/dist/types/generate-env-dts.js +2 -44
  109. package/dist/types/zenith-env-dts.d.ts +4 -0
  110. package/dist/types/zenith-env-dts.js +96 -0
  111. package/package.json +3 -6
@@ -1,15 +1,11 @@
1
- export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath }: {
1
+ export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath, globalMiddleware }: {
2
2
  coreOutputDir: any;
3
3
  staticDir: any;
4
4
  target: any;
5
5
  routeManifest: any;
6
6
  basePath?: string | undefined;
7
+ globalMiddleware?: null | undefined;
7
8
  }): Promise<{
8
- schema_version: number;
9
- zenith_version: any;
10
- target: any;
11
- base_path: string;
12
- content_hash: any;
13
9
  routes: {
14
10
  html: any;
15
11
  assets: any[];
@@ -26,4 +22,12 @@ export function writeBuildOutputManifest({ coreOutputDir, staticDir, target, rou
26
22
  css: any[];
27
23
  vendor: any;
28
24
  };
25
+ global_middleware?: {
26
+ source_file: any;
27
+ } | undefined;
28
+ schema_version: number;
29
+ zenith_version: any;
30
+ target: any;
31
+ base_path: string;
32
+ content_hash: any;
29
33
  }>;
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { normalizeGlobalMiddlewareMetadata } from './global-middleware.js';
4
5
  const CLI_VERSION = (() => {
5
6
  try {
6
7
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -39,7 +40,7 @@ async function readRouteHtml(staticDir, htmlPath) {
39
40
  return '';
40
41
  }
41
42
  }
42
- export async function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath = '/' }) {
43
+ export async function writeBuildOutputManifest({ coreOutputDir, staticDir, target, routeManifest, basePath = '/', globalMiddleware = null }) {
43
44
  const bundlerManifest = await readJson(join(staticDir, 'manifest.json'), {});
44
45
  const routerManifest = await readJson(join(staticDir, 'assets', 'router-manifest.json'), { routes: [] });
45
46
  const routeByPath = new Map((Array.isArray(routerManifest.routes) ? routerManifest.routes : []).map((entry) => [entry.path, entry]));
@@ -85,12 +86,14 @@ export async function writeBuildOutputManifest({ coreOutputDir, staticDir, targe
85
86
  bundlerManifest.css,
86
87
  ...routeAssetCss
87
88
  ].filter((value) => typeof value === 'string' && value.endsWith('.css')));
89
+ const globalMiddlewareMetadata = normalizeGlobalMiddlewareMetadata(globalMiddleware);
88
90
  const buildManifest = {
89
91
  schema_version: 1,
90
92
  zenith_version: CLI_VERSION,
91
93
  target,
92
94
  base_path: basePath,
93
95
  content_hash: typeof bundlerManifest.hash === 'string' ? bundlerManifest.hash : '',
96
+ ...(globalMiddlewareMetadata ? { global_middleware: globalMiddlewareMetadata } : {}),
94
97
  routes,
95
98
  assets: {
96
99
  js: [...jsAssets].sort(),
package/dist/build.js CHANGED
@@ -16,6 +16,7 @@ import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } fro
16
16
  import { supportsTargetRouteCheck } from './route-check-support.js';
17
17
  import { createStartupProfiler } from './startup-profile.js';
18
18
  import { writeServerOutput } from './server-output.js';
19
+ import { resolveGlobalMiddleware } from './global-middleware.js';
19
20
  import { resolveBundlerBin } from './toolchain-paths.js';
20
21
  import { createBundlerToolchain, createCompilerToolchain, ensureToolchainCompatibility, getActiveToolchainCandidate } from './toolchain-runner.js';
21
22
  import { maybeWarnAboutZenithVersionMismatch } from './version-check.js';
@@ -75,6 +76,7 @@ export async function build(options) {
75
76
  }));
76
77
  }
77
78
  const registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
79
+ const globalMiddleware = await startupProfile.measureAsync('resolve_global_middleware', () => resolveGlobalMiddleware({ projectRoot, pagesDir, target }));
78
80
  const manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(pagesDir, '.zen', { compilerOpts }));
79
81
  const pageManifest = manifest.filter((entry) => entry?.route_kind !== 'resource');
80
82
  if (mode !== 'legacy') {
@@ -136,14 +138,21 @@ export async function build(options) {
136
138
  staticDir: staticOutputDir,
137
139
  target,
138
140
  routeManifest: pageManifest,
139
- basePath
141
+ basePath,
142
+ globalMiddleware: globalMiddleware?.metadata || null
140
143
  }));
141
144
  await startupProfile.measureAsync('write_server_output', () => writeServerOutput({
142
145
  coreOutputDir,
143
146
  staticDir: staticOutputDir,
144
147
  projectRoot,
145
148
  config,
146
- basePath
149
+ basePath,
150
+ globalMiddleware: globalMiddleware?.metadata || null,
151
+ pageManifest,
152
+ pagesDir,
153
+ srcDir,
154
+ registry,
155
+ compilerOpts
147
156
  }));
148
157
  await startupProfile.measureAsync('adapt_output', () => adapter.adapt({ coreOutput: coreOutputDir, outDir, manifest: buildManifest, config }));
149
158
  const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
@@ -388,6 +388,7 @@ export function applyOccurrenceRewritePlans(pageIr, occurrencePlans, resolveBind
388
388
  binding.state_index = resolved.state_index;
389
389
  binding.component_instance = resolved.component_instance;
390
390
  binding.component_binding = resolved.component_binding;
391
+ binding.scoped_data_key = resolved.scoped_data_key;
391
392
  }
392
393
  }
393
394
  exprCursor = found + 1;
@@ -4,3 +4,12 @@ export function collectExpandedComponentOccurrences(source: any, registry: any,
4
4
  ownerPath: string;
5
5
  componentPath: string;
6
6
  }[];
7
+ /**
8
+ * Unique layout/component `.zen` paths reachable from a page dependency graph.
9
+ *
10
+ * @param {string} source
11
+ * @param {Map<string, string>} registry
12
+ * @param {string} sourceFile
13
+ * @returns {string[]}
14
+ */
15
+ export function collectReachableOwnerPaths(source: string, registry: Map<string, string>, sourceFile: string): string[];
@@ -10,6 +10,24 @@ export function collectExpandedComponentOccurrences(source, registry, sourceFile
10
10
  }, [], occurrences);
11
11
  return occurrences;
12
12
  }
13
+ /**
14
+ * Unique layout/component `.zen` paths reachable from a page dependency graph.
15
+ *
16
+ * @param {string} source
17
+ * @param {Map<string, string>} registry
18
+ * @param {string} sourceFile
19
+ * @returns {string[]}
20
+ */
21
+ export function collectReachableOwnerPaths(source, registry, sourceFile) {
22
+ const occurrences = collectExpandedComponentOccurrences(source, registry, sourceFile);
23
+ const paths = new Set();
24
+ for (const occurrence of occurrences) {
25
+ if (occurrence.componentPath) {
26
+ paths.add(occurrence.componentPath);
27
+ }
28
+ }
29
+ return [...paths];
30
+ }
13
31
  function walkSource(source, registry, context, chain, occurrences) {
14
32
  let cursor = 0;
15
33
  while (cursor < source.length) {
@@ -0,0 +1,12 @@
1
+ export function normalizePlugins(value: any): ({
2
+ name: any;
3
+ config: any;
4
+ } | {
5
+ name: any;
6
+ config?: undefined;
7
+ })[];
8
+ export function assertPluginConfigPatch(value: any): void;
9
+ export function cloneConfigValue(value: any, seen?: Map<any, any>): any;
10
+ export function deepFreeze(value: any, seen?: Set<any>): any;
11
+ export function pluginHookError(pluginName: any, hookName: any, error: any): Error;
12
+ export const PLUGIN_CONFIG_PATCH_KEYS: Set<string>;
@@ -0,0 +1,100 @@
1
+ const PLUGIN_OBJECT_KEYS = new Set(['name', 'config']);
2
+ const freezeObject = Object.freeze;
3
+ export const PLUGIN_CONFIG_PATCH_KEYS = new Set([
4
+ 'router',
5
+ 'embeddedMarkupExpressions',
6
+ 'typescriptDefault',
7
+ 'strictDomLints',
8
+ 'images',
9
+ 'basePath',
10
+ 'outDir'
11
+ ]);
12
+ function isPlainObject(value) {
13
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
14
+ return false;
15
+ }
16
+ const proto = Object.getPrototypeOf(value);
17
+ return proto === Object.prototype || proto === null;
18
+ }
19
+ function describePlugin(index, plugin) {
20
+ if (plugin && typeof plugin === 'object' && typeof plugin.name === 'string' && plugin.name.trim()) {
21
+ return `"${plugin.name.trim()}"`;
22
+ }
23
+ return `at index ${index}`;
24
+ }
25
+ export function normalizePlugins(value) {
26
+ if (!Array.isArray(value)) {
27
+ throw new Error('[Zenith:Config] Key "plugins" must be an array');
28
+ }
29
+ const seen = new Set();
30
+ return value.map((plugin, index) => {
31
+ if (!isPlainObject(plugin)) {
32
+ throw new Error(`[Zenith:Config] Plugin at index ${index} must be a plain object`);
33
+ }
34
+ for (const key of Object.keys(plugin)) {
35
+ if (!PLUGIN_OBJECT_KEYS.has(key)) {
36
+ throw new Error(`[Zenith:Config] Plugin ${describePlugin(index, plugin)} uses unsupported key "${key}"`);
37
+ }
38
+ }
39
+ if (typeof plugin.name !== 'string' || plugin.name.trim().length === 0) {
40
+ throw new Error(`[Zenith:Config] Plugin at index ${index} must have a non-empty name`);
41
+ }
42
+ const name = plugin.name.trim();
43
+ if (seen.has(name)) {
44
+ throw new Error(`[Zenith:Config] Duplicate plugin name: "${name}"`);
45
+ }
46
+ seen.add(name);
47
+ if ('config' in plugin && typeof plugin.config !== 'function') {
48
+ throw new Error(`[Zenith:Config] Plugin "${name}" key "config" must be a function`);
49
+ }
50
+ return 'config' in plugin ? { name, config: plugin.config } : { name };
51
+ });
52
+ }
53
+ export function assertPluginConfigPatch(value) {
54
+ if (!isPlainObject(value)) {
55
+ throw new Error('config hook must return a plain object patch');
56
+ }
57
+ for (const key of Object.keys(value)) {
58
+ if (!PLUGIN_CONFIG_PATCH_KEYS.has(key)) {
59
+ throw new Error(`${key} is not patchable`);
60
+ }
61
+ }
62
+ }
63
+ export function cloneConfigValue(value, seen = new Map()) {
64
+ if (!value || typeof value !== 'object') {
65
+ return value;
66
+ }
67
+ if (seen.has(value)) {
68
+ return seen.get(value);
69
+ }
70
+ if (Array.isArray(value)) {
71
+ const out = [];
72
+ seen.set(value, out);
73
+ for (const item of value) {
74
+ out.push(cloneConfigValue(item, seen));
75
+ }
76
+ return out;
77
+ }
78
+ const out = {};
79
+ seen.set(value, out);
80
+ for (const [key, child] of Object.entries(value)) {
81
+ out[key] = cloneConfigValue(child, seen);
82
+ }
83
+ return out;
84
+ }
85
+ export function deepFreeze(value, seen = new Set()) {
86
+ if (!value || (typeof value !== 'object' && typeof value !== 'function') || seen.has(value)) {
87
+ return value;
88
+ }
89
+ seen.add(value);
90
+ for (const key of Object.keys(value)) {
91
+ deepFreeze(value[key], seen);
92
+ }
93
+ return freezeObject(value);
94
+ }
95
+ export function pluginHookError(pluginName, hookName, error) {
96
+ const message = error && typeof error.message === 'string'
97
+ ? error.message
98
+ : String(error);
99
+ return new Error(`[Zenith plugin ${pluginName}] ${hookName} failed: ${message}`);
100
+ }
package/dist/config.d.ts CHANGED
@@ -27,4 +27,5 @@ export namespace DEFAULT_CONFIG {
27
27
  minimumCacheTTL: number;
28
28
  dangerouslyAllowLocalNetwork: boolean;
29
29
  };
30
+ let plugins: never[];
30
31
  }
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { KNOWN_TARGETS } from './adapters/adapter-types.js';
7
7
  import { normalizeBasePath } from './base-path.js';
8
+ import { assertPluginConfigPatch, cloneConfigValue, deepFreeze, normalizePlugins, pluginHookError } from './config-plugins.js';
8
9
  import { normalizeImageConfig } from './images/shared.js';
9
10
  const PACKAGE_REQUIRE = createRequire(import.meta.url);
10
11
  const CONFIG_FILES = ['zenith.config.ts', 'zenith.config.js'];
@@ -19,7 +20,8 @@ export const DEFAULT_CONFIG = {
19
20
  target: 'static',
20
21
  adapter: null,
21
22
  strictDomLints: false,
22
- images: normalizeImageConfig()
23
+ images: normalizeImageConfig(),
24
+ plugins: []
23
25
  };
24
26
  const TOP_LEVEL_SCHEMA = {
25
27
  router: 'boolean',
@@ -31,7 +33,8 @@ const TOP_LEVEL_SCHEMA = {
31
33
  target: 'string',
32
34
  adapter: 'object',
33
35
  strictDomLints: 'boolean',
34
- images: 'object'
36
+ images: 'object',
37
+ plugins: 'array'
35
38
  };
36
39
  function attachConfigMeta(config, explicitKeys) {
37
40
  Object.defineProperty(config, CONFIG_META, {
@@ -160,7 +163,7 @@ export function resolveConfigOutDir(projectRoot, config) {
160
163
  }
161
164
  export function validateConfig(config) {
162
165
  if (config === null || config === undefined) {
163
- return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig() }, []);
166
+ return attachConfigMeta({ ...DEFAULT_CONFIG, images: normalizeImageConfig(), plugins: [] }, []);
164
167
  }
165
168
  if (typeof config !== 'object' || Array.isArray(config)) {
166
169
  throw new Error('[Zenith:Config] Config must be a plain object');
@@ -175,7 +178,8 @@ export function validateConfig(config) {
175
178
  }
176
179
  const result = {
177
180
  ...DEFAULT_CONFIG,
178
- images: normalizeImageConfig(DEFAULT_CONFIG.images)
181
+ images: normalizeImageConfig(DEFAULT_CONFIG.images),
182
+ plugins: []
179
183
  };
180
184
  for (const [key, expectedType] of Object.entries(TOP_LEVEL_SCHEMA)) {
181
185
  if (!(key in config)) {
@@ -190,6 +194,10 @@ export function validateConfig(config) {
190
194
  result.adapter = validateAdapterValue(value);
191
195
  continue;
192
196
  }
197
+ if (key === 'plugins') {
198
+ result.plugins = normalizePlugins(value);
199
+ continue;
200
+ }
193
201
  if (typeof value !== expectedType) {
194
202
  throw new Error(`[Zenith:Config] Key "${key}" must be ${expectedType}, got ${typeof value}`);
195
203
  }
@@ -207,6 +215,48 @@ export function validateConfig(config) {
207
215
  }
208
216
  return attachConfigMeta(result, Object.keys(config));
209
217
  }
218
+ function normalizeConfigPatch(patch) {
219
+ assertPluginConfigPatch(patch);
220
+ const keys = Object.keys(patch);
221
+ const normalized = validateConfig(patch);
222
+ const out = {};
223
+ for (const key of keys) {
224
+ out[key] = cloneConfigValue(normalized[key]);
225
+ }
226
+ return out;
227
+ }
228
+ async function runPluginConfigHooks(config, projectRoot) {
229
+ let current = config;
230
+ for (const plugin of current.plugins) {
231
+ if (typeof plugin.config !== 'function') {
232
+ continue;
233
+ }
234
+ let patch;
235
+ try {
236
+ const snapshot = deepFreeze(cloneConfigValue(current));
237
+ patch = await plugin.config(snapshot, { projectRoot });
238
+ }
239
+ catch (error) {
240
+ throw pluginHookError(plugin.name, 'config', error);
241
+ }
242
+ if (patch === undefined || patch === null) {
243
+ continue;
244
+ }
245
+ let normalizedPatch;
246
+ try {
247
+ normalizedPatch = normalizeConfigPatch(patch);
248
+ }
249
+ catch (error) {
250
+ throw pluginHookError(plugin.name, 'config', error);
251
+ }
252
+ const explicitKeys = new Set(current?.[CONFIG_META]?.explicitKeys || []);
253
+ for (const key of Object.keys(normalizedPatch)) {
254
+ explicitKeys.add(key);
255
+ }
256
+ current = attachConfigMeta({ ...current, ...normalizedPatch, plugins: current.plugins }, explicitKeys);
257
+ }
258
+ return current;
259
+ }
210
260
  export async function loadConfig(projectRoot) {
211
261
  const resolvedProjectRoot = resolve(projectRoot);
212
262
  const configPath = resolveConfigFile(resolvedProjectRoot);
@@ -216,5 +266,6 @@ export async function loadConfig(projectRoot) {
216
266
  const mod = configPath.endsWith('.ts')
217
267
  ? await importTypescriptConfig(configPath, resolvedProjectRoot)
218
268
  : await importJavascriptConfig(configPath, resolvedProjectRoot);
219
- return markLoaded(validateConfig(mod.default || mod));
269
+ const config = validateConfig(mod.default || mod);
270
+ return markLoaded(await runPluginConfigHooks(config, resolvedProjectRoot));
220
271
  }
@@ -143,18 +143,38 @@ function collectTemplateClassSignature(envelope) {
143
143
  return [...classes].sort();
144
144
  }
145
145
  export function buildPageOnlyFastPathSignature(envelope) {
146
+ const ir = envelope.ir || {};
146
147
  return stableJson({
147
148
  route: envelope.route,
148
149
  router: envelope.router === true,
150
+ interactivityShape: {
151
+ requiresJs: envelope.requires_js === true,
152
+ signals: Array.isArray(ir.signals) ? ir.signals.length : 0,
153
+ refs: Array.isArray(ir.ref_bindings) ? ir.ref_bindings.length : 0,
154
+ events: Array.isArray(ir.event_bindings)
155
+ ? ir.event_bindings.map((entry) => [entry.index, entry.event]).sort()
156
+ : [],
157
+ markers: Array.isArray(ir.marker_bindings)
158
+ ? ir.marker_bindings.map((entry) => [entry.index, entry.kind]).sort()
159
+ : [],
160
+ componentInstances: Array.isArray(ir.component_instances) ? ir.component_instances.length : 0,
161
+ hoistedCode: Array.isArray(ir.hoisted?.code)
162
+ ? ir.hoisted.code.filter((entry) => String(entry || '').trim().length > 0).length
163
+ : 0,
164
+ hoistedState: Array.isArray(ir.hoisted?.state) ? ir.hoisted.state.length : 0,
165
+ hoistedSignals: Array.isArray(ir.hoisted?.signals) ? ir.hoisted.signals.length : 0
166
+ },
149
167
  assetContract: collectEnvelopeAssetContract(envelope),
150
168
  templateClassSignature: collectTemplateClassSignature(envelope),
151
- styleBlocks: envelope.ir.style_blocks || [],
152
- serverScript: envelope.ir.server_script || null,
153
- prerender: envelope.ir.prerender === true,
154
- hasGuard: envelope.ir.has_guard === true,
155
- hasLoad: envelope.ir.has_load === true,
156
- guardModuleRef: envelope.ir.guard_module_ref || null,
157
- loadModuleRef: envelope.ir.load_module_ref || null
169
+ styleBlocks: ir.style_blocks || [],
170
+ serverScript: ir.server_script || null,
171
+ prerender: ir.prerender === true,
172
+ hasGuard: ir.has_guard === true,
173
+ hasLoad: ir.has_load === true,
174
+ hasAction: ir.has_action === true,
175
+ guardModuleRef: ir.guard_module_ref || null,
176
+ loadModuleRef: ir.load_module_ref || null,
177
+ actionModuleRef: ir.action_module_ref || null
158
178
  });
159
179
  }
160
180
  export function buildGlobalGraphHash(envelopes) {
@@ -5,10 +5,12 @@ import { collectAssets, runBundler } from '../build/compiler-runtime.js';
5
5
  import { buildPageEnvelopes } from '../build/page-loop.js';
6
6
  import { createPageLoopCaches } from '../build/page-loop-state.js';
7
7
  import { deriveProjectRootFromPagesDir, ensureZenithTypeDeclarations } from '../build/type-declarations.js';
8
+ import { rewriteSoftNavigationHrefBasePathInHtmlFiles } from '../base-path-html.js';
8
9
  import { injectImageMaterializationIntoRouterManifest } from '../images/router-manifest.js';
9
10
  import { buildImageArtifacts } from '../images/service.js';
10
11
  import { materializeImageMarkupInHtmlFiles } from '../images/materialize.js';
11
12
  import { createImageRuntimePayload, injectImageRuntimePayloadIntoHtmlFiles } from '../images/payload.js';
13
+ import { writeResourceRouteManifest } from '../resource-manifest.js';
12
14
  import { createStartupProfiler } from '../startup-profile.js';
13
15
  import { resolveBuildAdapter } from '../adapters/resolve-adapter.js';
14
16
  import { supportsTargetRouteCheck } from '../route-check-support.js';
@@ -34,6 +36,9 @@ export function createDevBuildSession(options) {
34
36
  };
35
37
  ensureToolchainCompatibility(bundlerBin);
36
38
  const state = createDevBuildState(config, basePath);
39
+ function pageManifestEntries() {
40
+ return state.manifest.filter((entry) => entry?.route_kind !== 'resource');
41
+ }
37
42
  async function syncImageState(startupProfile) {
38
43
  const { manifest } = await startupProfile.measureAsync('build_image_artifacts', () => buildImageArtifacts({
39
44
  projectRoot,
@@ -50,12 +55,13 @@ export function createDevBuildSession(options) {
50
55
  }
51
56
  async function runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, bundlerOptions = {}) {
52
57
  const orderedEnvelopes = bundlerOptions.envelopesOverride
53
- || orderEnvelopes(state.manifest, resolvedPagesDir, state.envelopeByFile);
58
+ || orderEnvelopes(pageManifestEntries(), resolvedPagesDir, state.envelopeByFile);
54
59
  if (!orderedEnvelopes || orderedEnvelopes.length === 0) {
55
60
  throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
56
61
  }
57
62
  await startupProfile.measureAsync('run_bundler', () => runBundler(orderedEnvelopes, outDir, projectRoot, activeLogger, showBundlerInfo, bundlerBin, {
58
63
  routeCheck: routeCheckEnabled,
64
+ basePath,
59
65
  devStableAssets: true,
60
66
  rebuildStrategy: bundlerOptions.rebuildStrategy || 'full',
61
67
  changedRoutes: bundlerOptions.changedRoutes || [],
@@ -63,6 +69,7 @@ export function createDevBuildSession(options) {
63
69
  globalGraphHash: bundlerOptions.globalGraphHash || ''
64
70
  }), { envelopes: orderedEnvelopes.length });
65
71
  await startupProfile.measureAsync('inject_image_materialization_manifest', () => injectImageMaterializationIntoRouterManifest(outDir, orderedEnvelopes), { envelopes: orderedEnvelopes.length });
72
+ await startupProfile.measureAsync('rewrite_soft_navigation_base_path', () => rewriteSoftNavigationHrefBasePathInHtmlFiles(outDir, basePath));
66
73
  const assets = await startupProfile.measureAsync('collect_assets', () => collectAssets(outDir));
67
74
  return { assets, envelopeCount: orderedEnvelopes.length };
68
75
  }
@@ -78,14 +85,15 @@ export function createDevBuildSession(options) {
78
85
  });
79
86
  state.registry = startupProfile.measureSync('build_component_registry', () => buildComponentRegistry(srcDir));
80
87
  state.manifest = await startupProfile.measureAsync('generate_manifest', () => generateManifest(resolvedPagesDir));
88
+ const pageManifest = pageManifestEntries();
81
89
  await startupProfile.measureAsync('ensure_zenith_type_declarations', () => ensureZenithTypeDeclarations({
82
- manifest: state.manifest,
90
+ manifest: pageManifest,
83
91
  pagesDir: resolvedPagesDir
84
92
  }));
85
93
  state.pageLoopCaches = createPageLoopCaches();
86
94
  const emitCompilerWarning = buildCompilerWarningEmitter(activeLogger);
87
95
  const { envelopes, expressionRewriteMetrics } = await buildPageEnvelopes({
88
- manifest: state.manifest,
96
+ manifest: pageManifest,
89
97
  pagesDir: resolvedPagesDir,
90
98
  srcDir,
91
99
  registry: state.registry,
@@ -98,20 +106,21 @@ export function createDevBuildSession(options) {
98
106
  pageLoopCaches: state.pageLoopCaches
99
107
  });
100
108
  state.envelopeByFile = new Map(envelopes.map((entry) => [entry.file, entry]));
101
- state.manifestEntryByPath = toManifestEntryMap(state.manifest, resolvedPagesDir);
109
+ state.manifestEntryByPath = toManifestEntryMap(pageManifest, resolvedPagesDir);
102
110
  state.pageOnlyFastPathSignatureByFile = new Map(envelopes.map((entry) => [entry.file, buildPageOnlyFastPathSignature(entry)]));
103
111
  state.globalGraphHash = buildGlobalGraphHash(envelopes);
104
112
  const { assets } = await runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo);
113
+ await startupProfile.measureAsync('write_resource_manifest', () => writeResourceRouteManifest(outDir, state.manifest, basePath));
105
114
  await syncImageState(startupProfile);
106
115
  startupProfile.emit('build_complete', {
107
- pages: state.manifest.length,
116
+ pages: pageManifest.length,
108
117
  assets: assets.length,
109
118
  compilerTotals,
110
119
  expressionRewriteMetrics,
111
120
  strategy: 'full'
112
121
  });
113
122
  state.hasSuccessfulBuild = true;
114
- return { pages: state.manifest.length, assets, strategy: 'full' };
123
+ return { pages: pageManifest.length, assets, strategy: 'full' };
115
124
  }
116
125
  async function runBundleOnlyBuild(activeLogger, showBundlerInfo) {
117
126
  const startupProfile = createStartupProfiler('cli-build');
@@ -120,7 +129,7 @@ export function createDevBuildSession(options) {
120
129
  const { assets } = await runBundlerWithCachedEnvelopes(startupProfile, activeLogger, showBundlerInfo, { rebuildStrategy: 'bundle-only' });
121
130
  await syncImageState(startupProfile);
122
131
  startupProfile.emit('build_complete', {
123
- pages: state.manifest.length,
132
+ pages: pageManifestEntries().length,
124
133
  assets: assets.length,
125
134
  compilerTotals,
126
135
  expressionRewriteMetrics,
@@ -155,7 +164,7 @@ export function createDevBuildSession(options) {
155
164
  state.envelopeByFile.set(envelope.file, envelope);
156
165
  state.pageOnlyFastPathSignatureByFile.set(envelope.file, buildPageOnlyFastPathSignature(envelope));
157
166
  }
158
- const orderedEnvelopes = orderEnvelopes(state.manifest, resolvedPagesDir, state.envelopeByFile);
167
+ const orderedEnvelopes = orderEnvelopes(pageManifestEntries(), resolvedPagesDir, state.envelopeByFile);
159
168
  if (!orderedEnvelopes || orderedEnvelopes.length === 0) {
160
169
  throw new Error('Dev rebuild cache is incomplete; full rebuild required.');
161
170
  }
@@ -169,14 +178,14 @@ export function createDevBuildSession(options) {
169
178
  });
170
179
  await syncImageState(startupProfile);
171
180
  startupProfile.emit('build_complete', {
172
- pages: state.manifest.length,
181
+ pages: pageManifestEntries().length,
173
182
  assets: assets.length,
174
183
  compilerTotals,
175
184
  expressionRewriteMetrics,
176
185
  strategy: 'page-only',
177
186
  rebuiltPages: entries.length
178
187
  });
179
- return { pages: state.manifest.length, assets, strategy: 'page-only', rebuiltPages: entries.length };
188
+ return { pages: pageManifestEntries().length, assets, strategy: 'page-only', rebuiltPages: entries.length };
180
189
  }
181
190
  return {
182
191
  async build(buildOptions = {}) {
@@ -0,0 +1,21 @@
1
+ export function buildDevErrorPayload({ pathname, state }: {
2
+ pathname: any;
3
+ state: any;
4
+ }): {
5
+ kind: string;
6
+ requestedPath: any;
7
+ buildId: any;
8
+ pendingBuildId: any;
9
+ buildStatus: any;
10
+ error: {
11
+ message: string;
12
+ };
13
+ hint: string;
14
+ };
15
+ export function respondWithDevBuildError({ req, res, pathname, state, looksLikeJsonRequest }: {
16
+ req: any;
17
+ res: any;
18
+ pathname: any;
19
+ state: any;
20
+ looksLikeJsonRequest: any;
21
+ }): void;
@@ -0,0 +1,48 @@
1
+ function truncateMessage(message, limit = 1000) {
2
+ const value = String(message || 'Dev build failed.');
3
+ return value.length > limit ? `${value.slice(0, limit - 3)}...` : value;
4
+ }
5
+ function escapeHtml(value) {
6
+ return String(value)
7
+ .replace(/&/g, '&amp;')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;');
11
+ }
12
+ export function buildDevErrorPayload({ pathname, state }) {
13
+ const message = truncateMessage(state.buildError?.message || 'Dev build failed.');
14
+ return {
15
+ kind: 'zenith_dev_build_failed',
16
+ requestedPath: pathname,
17
+ buildId: state.buildId,
18
+ pendingBuildId: state.pendingBuildId,
19
+ buildStatus: state.buildStatus,
20
+ error: { message },
21
+ hint: 'Fix the build error and save again.'
22
+ };
23
+ }
24
+ export function respondWithDevBuildError({ req, res, pathname, state, looksLikeJsonRequest }) {
25
+ const payload = buildDevErrorPayload({ pathname, state });
26
+ if (looksLikeJsonRequest(req, pathname)) {
27
+ res.writeHead(503, {
28
+ 'Content-Type': 'application/json',
29
+ 'Cache-Control': 'no-store',
30
+ 'X-Zenith-Dev-Error': 'build-failed'
31
+ });
32
+ res.end(JSON.stringify(payload));
33
+ return;
34
+ }
35
+ res.writeHead(503, {
36
+ 'Content-Type': 'text/html; charset=utf-8',
37
+ 'Cache-Control': 'no-store',
38
+ 'X-Zenith-Dev-Error': 'build-failed'
39
+ });
40
+ res.end([
41
+ '<!DOCTYPE html>',
42
+ '<html><head><meta charset="utf-8"><title>Zenith Dev Build Failed</title></head>',
43
+ '<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
44
+ '<h1 style="margin-top:0;">Zenith Dev Build Failed</h1>',
45
+ `<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${escapeHtml(pathname)}\nStatus: build failed\nError: ${escapeHtml(payload.error.message)}\nHint: ${escapeHtml(payload.hint)}</pre>`,
46
+ '</body></html>'
47
+ ].join(''));
48
+ }
@@ -0,0 +1,15 @@
1
+ export function isPortConflict(error: any): any;
2
+ export function listenWithPortFallback({ server, port, host, maxAttempts }: {
3
+ server: any;
4
+ port: any;
5
+ host: any;
6
+ maxAttempts?: number | undefined;
7
+ }): Promise<{
8
+ port: any;
9
+ requestedPort: any;
10
+ portFallback: {
11
+ requestedPort: any;
12
+ occupiedPorts: any[];
13
+ finalPort: any;
14
+ } | null;
15
+ }>;