@zenithbuild/cli 0.7.11 → 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 (87) hide show
  1. package/README.md +10 -1
  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/expression-rewrites.d.ts +3 -1
  14. package/dist/build/expression-rewrites.js +14 -2
  15. package/dist/build/page-component-loop.d.ts +1 -0
  16. package/dist/build/page-component-loop.js +66 -6
  17. package/dist/build/page-ir-normalization.js +7 -0
  18. package/dist/build/page-loop-state.d.ts +2 -1
  19. package/dist/build/page-loop-state.js +9 -2
  20. package/dist/build/page-loop.js +10 -1
  21. package/dist/build/scoped-expression-context.d.ts +5 -0
  22. package/dist/build/scoped-expression-context.js +133 -0
  23. package/dist/build/type-declarations.d.ts +2 -1
  24. package/dist/build/type-declarations.js +31 -1
  25. package/dist/build-output-manifest.d.ts +10 -6
  26. package/dist/build-output-manifest.js +4 -1
  27. package/dist/build.js +11 -2
  28. package/dist/component-instance-ir.js +1 -0
  29. package/dist/component-occurrences.d.ts +9 -0
  30. package/dist/component-occurrences.js +18 -0
  31. package/dist/config-plugins.d.ts +12 -0
  32. package/dist/config-plugins.js +100 -0
  33. package/dist/config.d.ts +1 -0
  34. package/dist/config.js +56 -5
  35. package/dist/dev-server/request-handler.js +46 -4
  36. package/dist/dev-server.js +92 -4
  37. package/dist/global-middleware-runtime-source.d.ts +15 -0
  38. package/dist/global-middleware-runtime-source.js +62 -0
  39. package/dist/global-middleware.d.ts +13 -0
  40. package/dist/global-middleware.js +252 -0
  41. package/dist/manifest.d.ts +9 -1
  42. package/dist/manifest.js +66 -26
  43. package/dist/preview/request-handler.js +78 -5
  44. package/dist/preview/server-runner.d.ts +7 -2
  45. package/dist/preview/server-runner.js +19 -6
  46. package/dist/preview/server-script-runner-template.js +97 -29
  47. package/dist/route-classification.d.ts +2 -1
  48. package/dist/route-classification.js +6 -2
  49. package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
  50. package/dist/scoped-server-data/analyze-owner-file.js +149 -0
  51. package/dist/scoped-server-data/diagnostics.d.ts +18 -0
  52. package/dist/scoped-server-data/diagnostics.js +32 -0
  53. package/dist/scoped-server-data/lowering.d.ts +27 -0
  54. package/dist/scoped-server-data/lowering.js +242 -0
  55. package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
  56. package/dist/scoped-server-data/manifest-integration.js +125 -0
  57. package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
  58. package/dist/scoped-server-data/owner-scanner.js +55 -0
  59. package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
  60. package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
  61. package/dist/scoped-server-data/runtime.d.ts +24 -0
  62. package/dist/scoped-server-data/runtime.js +121 -0
  63. package/dist/scoped-server-data/serialization-set.d.ts +2 -0
  64. package/dist/scoped-server-data/serialization-set.js +52 -0
  65. package/dist/scoped-server-data/static-props.d.ts +12 -0
  66. package/dist/scoped-server-data/static-props.js +307 -0
  67. package/dist/scoped-server-data/type-declarations.d.ts +10 -0
  68. package/dist/scoped-server-data/type-declarations.js +368 -0
  69. package/dist/scoped-server-data/types.d.ts +74 -0
  70. package/dist/scoped-server-data/types.js +1 -0
  71. package/dist/server-contract/auth-control-flow.d.ts +1 -0
  72. package/dist/server-contract/auth-control-flow.js +10 -0
  73. package/dist/server-contract/resolve.d.ts +19 -0
  74. package/dist/server-contract/resolve.js +85 -13
  75. package/dist/server-contract/resolved-envelope.d.ts +9 -0
  76. package/dist/server-contract/resolved-envelope.js +14 -0
  77. package/dist/server-contract/stage.js +1 -10
  78. package/dist/server-module-output.d.ts +9 -0
  79. package/dist/server-module-output.js +250 -0
  80. package/dist/server-output.d.ts +7 -1
  81. package/dist/server-output.js +138 -179
  82. package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
  83. package/dist/server-runtime/matched-route-pipeline.js +1 -0
  84. package/dist/server-runtime/node-server.js +21 -1
  85. package/dist/server-runtime/route-render.d.ts +12 -3
  86. package/dist/server-runtime/route-render.js +67 -13
  87. package/package.json +3 -3
@@ -1,12 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
- import { createRequire } from 'node:module';
4
- import { basename, dirname, extname, join, relative, resolve } from 'node:path';
3
+ import { basename, dirname, join, resolve } from 'node:path';
5
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
5
  import { loadResourceRouteManifest } from './resource-manifest.js';
7
6
  import { assignServerRouteNames } from './server-route-names.js';
8
- const PACKAGE_REQUIRE = createRequire(import.meta.url);
9
- const RELATIVE_SPECIFIER_RE = /((?:import|export)\s+(?:[^'"]*?\s+from\s+)?|import\s*\()\s*(['"])([^'"]+)\2/g;
7
+ import { normalizeGlobalMiddlewareMetadata } from './global-middleware.js';
8
+ import { writeServerModulePackage } from './server-module-output.js';
9
+ const GLOBAL_MIDDLEWARE_MODULE = 'global-middleware/entry.js';
10
+ const SCOPED_SERVER_DATA_LOWERING_HELPER_UNAVAILABLE = '[Zenith:ScopedServerData] Server-output lowering helper is unavailable. Run the CLI build step before packaging scoped server data modules.';
10
11
  const SERVER_RUNTIME_FILES = [
11
12
  {
12
13
  from: new URL('./server-runtime/route-render.js', import.meta.url),
@@ -21,6 +22,10 @@ const SERVER_RUNTIME_FILES = [
21
22
  to: 'server-contract',
22
23
  recursive: true
23
24
  },
25
+ {
26
+ from: new URL('./scoped-server-data/runtime.js', import.meta.url),
27
+ to: 'scoped-server-data/runtime.js'
28
+ },
24
29
  {
25
30
  from: new URL('./server-middleware.js', import.meta.url),
26
31
  to: 'server-middleware.js'
@@ -70,183 +75,33 @@ const SERVER_RUNTIME_FILES = [
70
75
  to: 'download-result.js'
71
76
  }
72
77
  ];
73
- const SPECIAL_SERVER_SPECIFIERS = new Map([
74
- ['zenith:server-contract', 'server-contract.js'],
75
- ['zenith:route-auth', 'auth/route-auth.js']
76
- ]);
77
- function resolveTypeScriptApi(projectRoot) {
78
- try {
79
- const projectRequire = createRequire(join(projectRoot, '__zenith_server_output_loader__.js'));
80
- return projectRequire('typescript');
81
- }
82
- catch {
83
- try {
84
- return PACKAGE_REQUIRE('typescript');
85
- }
86
- catch {
87
- throw new Error('[Zenith:Build] Server-capable targets require the `typescript` package to transpile route modules.');
88
- }
89
- }
90
- }
91
- function withJsExtension(specifier) {
92
- if (specifier.endsWith('.json')) {
93
- return specifier;
94
- }
95
- return specifier.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
96
- }
97
- function replaceSpecifier(source, original, nextValue) {
98
- return source.replace(new RegExp(`(['"])${escapeRegex(original)}\\1`, 'g'), (_, quote) => `${quote}${nextValue}${quote}`);
99
- }
100
- function escapeRegex(value) {
101
- return String(value).replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
102
- }
103
- function isRelativeSpecifier(specifier) {
104
- return (specifier.startsWith('./') ||
105
- specifier.startsWith('../') ||
106
- specifier.startsWith('/') ||
107
- specifier.startsWith('file:'));
108
- }
109
- function resolveModuleCandidates(basePath) {
110
- if (extname(basePath)) {
111
- return [basePath];
112
- }
78
+ let scopedServerDataLoweringPromise = null;
79
+ function resolveScopedServerDataLoweringPath() {
80
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
113
81
  return [
114
- basePath,
115
- `${basePath}.ts`,
116
- `${basePath}.tsx`,
117
- `${basePath}.mts`,
118
- `${basePath}.cts`,
119
- `${basePath}.js`,
120
- `${basePath}.mjs`,
121
- `${basePath}.cjs`,
122
- `${basePath}.json`,
123
- join(basePath, 'index.ts'),
124
- join(basePath, 'index.tsx'),
125
- join(basePath, 'index.mts'),
126
- join(basePath, 'index.cts'),
127
- join(basePath, 'index.js'),
128
- join(basePath, 'index.mjs'),
129
- join(basePath, 'index.cjs'),
130
- join(basePath, 'index.json')
131
- ];
132
- }
133
- function resolveImportedModule(specifier, sourceFile) {
134
- if (!isRelativeSpecifier(specifier)) {
135
- return null;
136
- }
137
- const baseDir = dirname(sourceFile);
138
- const basePath = specifier.startsWith('file:')
139
- ? new URL(specifier)
140
- : resolve(baseDir, specifier);
141
- const filePath = basePath instanceof URL ? fileURLToPath(basePath) : basePath;
142
- const candidates = resolveModuleCandidates(filePath);
143
- const found = candidates.find((candidate) => existsSync(candidate));
144
- if (!found) {
145
- throw new Error(`[Zenith:Build] Cannot resolve server import "${specifier}" from "${sourceFile}"`);
146
- }
147
- return found;
148
- }
149
- function gatherSpecifiers(source) {
150
- const results = [];
151
- for (const match of source.matchAll(RELATIVE_SPECIFIER_RE)) {
152
- const specifier = String(match[3] || '');
153
- results.push(specifier);
154
- }
155
- return results;
156
- }
157
- function transpileSource(ts, source, filePath) {
158
- return ts.transpileModule(source, {
159
- compilerOptions: {
160
- module: ts.ModuleKind.ESNext,
161
- target: ts.ScriptTarget.ES2022,
162
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
163
- esModuleInterop: true,
164
- allowSyntheticDefaultImports: true
165
- },
166
- fileName: filePath
167
- }).outputText;
168
- }
169
- function outputPathForSource(projectRoot, modulesRoot, sourcePath) {
170
- const relativePath = relative(projectRoot, sourcePath).replaceAll('\\', '/');
171
- const nextRelative = extname(relativePath) === '.json'
172
- ? relativePath
173
- : relativePath.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
174
- return join(modulesRoot, nextRelative);
82
+ join(moduleDir, 'scoped-server-data', 'lowering.js'),
83
+ join(moduleDir, '..', 'dist', 'scoped-server-data', 'lowering.js')
84
+ ].find((candidate) => existsSync(candidate)) || null;
175
85
  }
176
- async function compileImportedModule({ projectRoot, modulesRoot, serverDir, sourcePath, ts, seen }) {
177
- if (seen.has(sourcePath)) {
178
- return outputPathForSource(projectRoot, modulesRoot, sourcePath);
86
+ async function getScopedServerDataLowering() {
87
+ const helperPath = resolveScopedServerDataLoweringPath();
88
+ if (!helperPath) {
89
+ throw new Error(SCOPED_SERVER_DATA_LOWERING_HELPER_UNAVAILABLE);
179
90
  }
180
- seen.add(sourcePath);
181
- const outPath = outputPathForSource(projectRoot, modulesRoot, sourcePath);
182
- await mkdir(dirname(outPath), { recursive: true });
183
- if (extname(sourcePath) === '.json') {
184
- await cp(sourcePath, outPath, { force: true });
185
- return outPath;
186
- }
187
- const source = await readFile(sourcePath, 'utf8');
188
- let output = transpileSource(ts, source, sourcePath);
189
- for (const specifier of gatherSpecifiers(output)) {
190
- const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
191
- if (specialSpecifierPath) {
192
- const nextSpecifier = relative(dirname(outPath), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
193
- output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
194
- continue;
195
- }
196
- if (!isRelativeSpecifier(specifier)) {
197
- continue;
198
- }
199
- const resolvedPath = resolveImportedModule(specifier, sourcePath);
200
- if (!resolvedPath) {
201
- continue;
202
- }
203
- const compiledDependencyPath = await compileImportedModule({
204
- projectRoot,
205
- modulesRoot,
206
- serverDir,
207
- sourcePath: resolvedPath,
208
- ts,
209
- seen
210
- });
211
- const nextSpecifier = relative(dirname(outPath), compiledDependencyPath).replaceAll('\\', '/');
212
- output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
91
+ if (!scopedServerDataLoweringPromise) {
92
+ scopedServerDataLoweringPromise = import(pathToFileURL(helperPath).href);
213
93
  }
214
- await writeFile(outPath, output, 'utf8');
215
- return outPath;
94
+ return scopedServerDataLoweringPromise;
216
95
  }
217
96
  async function writeRouteModulePackage({ projectRoot, serverDir, routeDir, route }) {
218
- const ts = resolveTypeScriptApi(projectRoot);
219
- const modulesRoot = join(routeDir, 'modules');
220
- const seen = new Set();
221
- let entryOutput = transpileSource(ts, route.server_script || '', route.server_script_path || 'route-entry.ts');
222
- for (const specifier of gatherSpecifiers(entryOutput)) {
223
- const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
224
- if (specialSpecifierPath) {
225
- const nextSpecifier = relative(join(routeDir, 'route'), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
226
- entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
227
- continue;
228
- }
229
- if (!isRelativeSpecifier(specifier)) {
230
- continue;
231
- }
232
- const resolvedPath = resolveImportedModule(specifier, route.server_script_path || projectRoot);
233
- if (!resolvedPath) {
234
- continue;
235
- }
236
- const compiledDependencyPath = await compileImportedModule({
237
- projectRoot,
238
- modulesRoot,
239
- serverDir,
240
- sourcePath: resolvedPath,
241
- ts,
242
- seen
243
- });
244
- const nextSpecifier = relative(join(routeDir, 'route'), compiledDependencyPath).replaceAll('\\', '/');
245
- entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
246
- }
247
- const routeModulePath = join(routeDir, 'route', 'entry.js');
248
- await mkdir(dirname(routeModulePath), { recursive: true });
249
- await writeFile(routeModulePath, entryOutput, 'utf8');
97
+ await writeServerModulePackage({
98
+ projectRoot,
99
+ serverDir,
100
+ entrySource: route.server_script || '',
101
+ entrySourcePath: route.server_script_path || 'route-entry.ts',
102
+ entryOutputPath: join(routeDir, 'route', 'entry.js'),
103
+ modulesRoot: join(routeDir, 'modules')
104
+ });
250
105
  }
251
106
  async function copyRuntimeFiles(serverDir) {
252
107
  for (const file of SERVER_RUNTIME_FILES) {
@@ -266,7 +121,81 @@ async function copyOptionalFile(sourcePath, targetPath) {
266
121
  await cp(sourcePath, targetPath, { force: true });
267
122
  return true;
268
123
  }
269
- export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot, config, basePath = '/' }) {
124
+ async function writeGlobalMiddlewarePackage({ projectRoot, serverDir, globalMiddleware }) {
125
+ const metadata = normalizeGlobalMiddlewareMetadata(globalMiddleware);
126
+ if (!metadata) {
127
+ return null;
128
+ }
129
+ const sourcePath = resolve(projectRoot, metadata.source_file);
130
+ if (!existsSync(sourcePath)) {
131
+ throw new Error(`[Zenith:Middleware] Cannot emit global middleware because source file "${metadata.source_file}" was not found.`);
132
+ }
133
+ await writeServerModulePackage({
134
+ projectRoot,
135
+ serverDir,
136
+ entrySource: await readFile(sourcePath, 'utf8'),
137
+ entrySourcePath: sourcePath,
138
+ entryOutputPath: join(serverDir, GLOBAL_MIDDLEWARE_MODULE),
139
+ modulesRoot: join(serverDir, 'global-middleware', 'modules'),
140
+ validateMiddlewareImports: true
141
+ });
142
+ return {
143
+ ...metadata,
144
+ module: GLOBAL_MIDDLEWARE_MODULE
145
+ };
146
+ }
147
+ function mergePageRouteMetadata(route, manifestEntry) {
148
+ if (!manifestEntry) {
149
+ return route;
150
+ }
151
+ return {
152
+ ...route,
153
+ file: manifestEntry.file || route.file || null,
154
+ has_scoped_server_data: manifestEntry.has_scoped_server_data === true || route.has_scoped_server_data === true,
155
+ scoped_server_data: Array.isArray(manifestEntry.scoped_server_data)
156
+ ? manifestEntry.scoped_server_data
157
+ : (Array.isArray(route.scoped_server_data) ? route.scoped_server_data : [])
158
+ };
159
+ }
160
+ async function writeScopedServerDataModules({ projectRoot, serverDir, route, pagesDir, srcDir, registry, compilerOpts }) {
161
+ if (route.route_kind === 'resource' || route.has_scoped_server_data !== true) {
162
+ return [];
163
+ }
164
+ if (!pagesDir || !srcDir || !registry) {
165
+ throw new Error(`[Zenith:ScopedServerData] Cannot package scoped server data for route "${route.path}" without build context.`);
166
+ }
167
+ if (typeof route.file !== 'string' || route.file.length === 0) {
168
+ throw new Error(`[Zenith:ScopedServerData] Cannot package scoped server data for route "${route.path}" without a source file.`);
169
+ }
170
+ const pageFile = resolve(pagesDir, route.file);
171
+ const pageSource = await readFile(pageFile, 'utf8');
172
+ const lowering = await getScopedServerDataLowering();
173
+ const lowered = lowering.lowerRouteScopedServerData({
174
+ pageSource,
175
+ pageFile,
176
+ registry,
177
+ srcDir,
178
+ projectRoot,
179
+ compilerOpts,
180
+ scopedServerData: Array.isArray(route.scoped_server_data) ? route.scoped_server_data : []
181
+ });
182
+ if (!Array.isArray(lowered.scopedServerData) || lowered.scopedServerData.length === 0) {
183
+ throw new Error(`[Zenith:ScopedServerData] Cannot package scoped server data for route "${route.path}" because no owners were lowered.`);
184
+ }
185
+ for (const module of lowered.modules) {
186
+ const entryOutputPath = lowering.resolveScopedServerModuleOutputPath(serverDir, module.module);
187
+ await writeServerModulePackage({
188
+ projectRoot,
189
+ serverDir,
190
+ entrySource: module.source,
191
+ entrySourcePath: module.sourcePath,
192
+ entryOutputPath,
193
+ modulesRoot: join(serverDir, 'scoped', '_modules')
194
+ });
195
+ }
196
+ return lowered.scopedServerData;
197
+ }
198
+ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot, config, basePath = '/', globalMiddleware = null, pageManifest = [], pagesDir = null, srcDir = null, registry = null, compilerOpts = {} }) {
270
199
  const serverDir = join(coreOutputDir, 'server');
271
200
  await rm(serverDir, { recursive: true, force: true });
272
201
  let routerManifest = { routes: [] };
@@ -277,16 +206,28 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
277
206
  routerManifest = { routes: [] };
278
207
  }
279
208
  const resourceManifest = await loadResourceRouteManifest(staticDir, basePath);
280
- const pageRoutes = Array.isArray(routerManifest.routes) ? routerManifest.routes : [];
209
+ const pageManifestByPath = new Map((Array.isArray(pageManifest) ? pageManifest : [])
210
+ .filter((entry) => entry?.route_kind !== 'resource')
211
+ .map((entry) => [entry.path, entry]));
212
+ const pageRoutes = (Array.isArray(routerManifest.routes) ? routerManifest.routes : [])
213
+ .map((route) => mergePageRouteMetadata(route, pageManifestByPath.get(route.path)));
281
214
  const serverRoutes = pageRoutes
282
- .filter((route) => route.server_script && route.prerender !== true)
215
+ .filter((route) => route.prerender !== true && (route.server_script || route.has_scoped_server_data === true))
283
216
  .map((route) => ({ ...route, route_kind: 'page' }))
284
217
  .concat((Array.isArray(resourceManifest.routes) ? resourceManifest.routes : []).map((route) => ({
285
218
  ...route,
286
219
  route_kind: 'resource'
287
220
  })));
221
+ if (serverRoutes.some((route) => route.route_kind !== 'resource' && route.has_scoped_server_data === true)) {
222
+ await getScopedServerDataLowering();
223
+ }
288
224
  await mkdir(serverDir, { recursive: true });
289
225
  await copyRuntimeFiles(serverDir);
226
+ const serverGlobalMiddlewareMetadata = await writeGlobalMiddlewarePackage({
227
+ projectRoot,
228
+ serverDir,
229
+ globalMiddleware
230
+ });
290
231
  const imageManifestSource = join(staticDir, '_zenith', 'image', 'manifest.json');
291
232
  const emittedRoutes = [];
292
233
  for (const { route, name } of assignServerRouteNames(serverRoutes)) {
@@ -314,6 +255,15 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
314
255
  routeDir,
315
256
  route
316
257
  });
258
+ const scopedServerData = await writeScopedServerDataModules({
259
+ projectRoot,
260
+ serverDir,
261
+ route,
262
+ pagesDir,
263
+ srcDir,
264
+ registry,
265
+ compilerOpts
266
+ });
317
267
  const meta = {
318
268
  name,
319
269
  path: route.path,
@@ -336,13 +286,22 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
336
286
  image_manifest_file: route.route_kind === 'resource' ? null : imageManifestFile,
337
287
  image_config: config?.images || {}
338
288
  };
289
+ if (scopedServerData.length > 0) {
290
+ meta.has_scoped_server_data = true;
291
+ meta.scoped_server_data = scopedServerData;
292
+ }
339
293
  if (route.route_kind !== 'resource' && Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
340
294
  meta.image_materialization = route.image_materialization;
341
295
  }
342
296
  await writeFile(join(routeDir, 'route.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
343
297
  emittedRoutes.push(meta);
344
298
  }
345
- await writeFile(join(serverDir, 'manifest.json'), `${JSON.stringify({ base_path: basePath, routes: emittedRoutes }, null, 2)}\n`, 'utf8');
299
+ const serverManifest = {
300
+ base_path: basePath,
301
+ ...(serverGlobalMiddlewareMetadata ? { global_middleware: serverGlobalMiddlewareMetadata } : {}),
302
+ routes: emittedRoutes
303
+ };
304
+ await writeFile(join(serverDir, 'manifest.json'), `${JSON.stringify(serverManifest, null, 2)}\n`, 'utf8');
346
305
  return {
347
306
  serverDir,
348
307
  routes: emittedRoutes
@@ -0,0 +1 @@
1
+ export { executeMatchedRoutePipeline, runGlobalMiddlewareChain } from "../server-contract/resolve.js";
@@ -0,0 +1 @@
1
+ export { executeMatchedRoutePipeline, runGlobalMiddlewareChain } from '../server-contract/resolve.js';
@@ -61,6 +61,22 @@ function resolveWithinRoot(rootDir, requestPath) {
61
61
  }
62
62
  return null;
63
63
  }
64
+ function resolveManifestMiddlewareModulePath(serverDir, serverManifest) {
65
+ const modulePath = serverManifest?.global_middleware?.module;
66
+ if (typeof modulePath !== 'string' || modulePath.trim().length === 0) {
67
+ return null;
68
+ }
69
+ const normalized = normalize(modulePath).replace(/\\/g, '/');
70
+ if (normalized === '..' || normalized.startsWith('../') || normalized.startsWith('/')) {
71
+ throw new Error('[Zenith:Middleware] Invalid global middleware module path in server manifest.');
72
+ }
73
+ const root = resolve(serverDir);
74
+ const candidate = resolve(root, normalized);
75
+ if (candidate !== root && candidate.startsWith(`${root}${sep}`)) {
76
+ return candidate;
77
+ }
78
+ throw new Error('[Zenith:Middleware] Invalid global middleware module path in server manifest.');
79
+ }
64
80
  function toStaticFilePath(staticDir, pathname) {
65
81
  let resolvedPath = pathname;
66
82
  if (resolvedPath === '/') {
@@ -193,6 +209,7 @@ async function loadRuntimeContext(options = {}) {
193
209
  });
194
210
  const serverManifest = await readJson(join(serverDir, 'manifest.json'), { routes: [] });
195
211
  const allServerRoutes = Array.isArray(serverManifest.routes) ? serverManifest.routes : [];
212
+ const globalMiddlewareModulePath = resolveManifestMiddlewareModulePath(serverDir, serverManifest);
196
213
  return {
197
214
  distDir,
198
215
  serverDir,
@@ -202,6 +219,7 @@ async function loadRuntimeContext(options = {}) {
202
219
  serverRoutes: allServerRoutes,
203
220
  pageServerRoutes: allServerRoutes.filter((route) => route?.route_kind !== 'resource'),
204
221
  resourceServerRoutes: allServerRoutes.filter((route) => route?.route_kind === 'resource'),
222
+ globalMiddlewareModulePath,
205
223
  images: config.images || {},
206
224
  basePath: normalizeBasePath(config.base_path || '/')
207
225
  };
@@ -323,7 +341,8 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
323
341
  request,
324
342
  route: resourceResolved.route,
325
343
  params: resourceResolved.params,
326
- routeModulePath: join(routeDir, 'route', 'entry.js')
344
+ routeModulePath: join(routeDir, 'route', 'entry.js'),
345
+ globalMiddlewareModulePath: context.globalMiddlewareModulePath
327
346
  });
328
347
  await sendFetchResponse(res, response, req.method);
329
348
  return;
@@ -337,6 +356,7 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
337
356
  route: serverResolved.route,
338
357
  params: serverResolved.params,
339
358
  routeModulePath: join(routeDir, 'route', 'entry.js'),
359
+ globalMiddlewareModulePath: context.globalMiddlewareModulePath,
340
360
  shellHtmlPath: join(routeDir, 'route', 'page.html'),
341
361
  imageManifestPath: serverResolved.route.image_manifest_file
342
362
  ? join(routeDir, 'route', serverResolved.route.image_manifest_file)
@@ -6,9 +6,10 @@ export function extractInternalParams(requestUrl: any, route: any): {};
6
6
  * route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null },
7
7
  * params: Record<string, string>,
8
8
  * routeModulePath: string,
9
+ * globalMiddlewareModulePath?: string | null,
9
10
  * guardOnly?: boolean
10
11
  * }} options
11
- * @returns {Promise<{ publicUrl: URL, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
12
+ * @returns {Promise<{ publicUrl: URL, ctx: object, result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
12
13
  */
13
14
  export function executeRouteRequest(options: {
14
15
  request: Request;
@@ -21,9 +22,11 @@ export function executeRouteRequest(options: {
21
22
  };
22
23
  params: Record<string, string>;
23
24
  routeModulePath: string;
25
+ globalMiddlewareModulePath?: string | null;
24
26
  guardOnly?: boolean;
25
27
  }): Promise<{
26
28
  publicUrl: URL;
29
+ ctx: object;
27
30
  result: {
28
31
  kind: string;
29
32
  [key: string]: unknown;
@@ -42,9 +45,11 @@ export function executeRouteRequest(options: {
42
45
  * route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null },
43
46
  * params: Record<string, string>,
44
47
  * routeModulePath: string,
48
+ * globalMiddlewareModulePath?: string | null,
45
49
  * shellHtmlPath: string,
46
50
  * imageManifestPath?: string | null,
47
- * imageConfig?: Record<string, unknown>
51
+ * imageConfig?: Record<string, unknown>,
52
+ * scopedModuleBaseDir?: string | null
48
53
  * }} options
49
54
  * @returns {Promise<Response>}
50
55
  */
@@ -59,16 +64,19 @@ export function renderRouteRequest(options: {
59
64
  };
60
65
  params: Record<string, string>;
61
66
  routeModulePath: string;
67
+ globalMiddlewareModulePath?: string | null;
62
68
  shellHtmlPath: string;
63
69
  imageManifestPath?: string | null;
64
70
  imageConfig?: Record<string, unknown>;
71
+ scopedModuleBaseDir?: string | null;
65
72
  }): Promise<Response>;
66
73
  /**
67
74
  * @param {{
68
75
  * request: Request,
69
76
  * route: { path: string, params?: string[], route_id?: string | null, server_script_path?: string | null, file?: string | null, route_kind?: string | null, base_path?: string | null },
70
77
  * params: Record<string, string>,
71
- * routeModulePath: string
78
+ * routeModulePath: string,
79
+ * globalMiddlewareModulePath?: string | null
72
80
  * }} options
73
81
  * @returns {Promise<Response>}
74
82
  */
@@ -85,4 +93,5 @@ export function renderResourceRouteRequest(options: {
85
93
  };
86
94
  params: Record<string, string>;
87
95
  routeModulePath: string;
96
+ globalMiddlewareModulePath?: string | null;
88
97
  }): Promise<Response>;