@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,11 +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
- const PACKAGE_REQUIRE = createRequire(import.meta.url);
8
- const RELATIVE_SPECIFIER_RE = /((?:import|export)\s+(?:[^'"]*?\s+from\s+)?|import\s*\()\s*(['"])([^'"]+)\2/g;
6
+ import { assignServerRouteNames } from './server-route-names.js';
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.';
9
11
  const SERVER_RUNTIME_FILES = [
10
12
  {
11
13
  from: new URL('./server-runtime/route-render.js', import.meta.url),
@@ -20,6 +22,10 @@ const SERVER_RUNTIME_FILES = [
20
22
  to: 'server-contract',
21
23
  recursive: true
22
24
  },
25
+ {
26
+ from: new URL('./scoped-server-data/runtime.js', import.meta.url),
27
+ to: 'scoped-server-data/runtime.js'
28
+ },
23
29
  {
24
30
  from: new URL('./server-middleware.js', import.meta.url),
25
31
  to: 'server-middleware.js'
@@ -44,6 +50,10 @@ const SERVER_RUNTIME_FILES = [
44
50
  from: new URL('./images/shared.js', import.meta.url),
45
51
  to: 'images/shared.js'
46
52
  },
53
+ {
54
+ from: new URL('./images/remote-fetch.js', import.meta.url),
55
+ to: 'images/remote-fetch.js'
56
+ },
47
57
  {
48
58
  from: new URL('./images/runtime.js', import.meta.url),
49
59
  to: 'images/runtime.js'
@@ -65,197 +75,33 @@ const SERVER_RUNTIME_FILES = [
65
75
  to: 'download-result.js'
66
76
  }
67
77
  ];
68
- const SPECIAL_SERVER_SPECIFIERS = new Map([
69
- ['zenith:server-contract', 'server-contract.js'],
70
- ['zenith:route-auth', 'auth/route-auth.js']
71
- ]);
72
- function normalizeRouteName(routePath) {
73
- if (routePath === '/') {
74
- return 'index';
75
- }
76
- return routePath
77
- .replace(/^\//, '')
78
- .replace(/\/+/g, '_')
79
- .replace(/:/g, 'param_')
80
- .replace(/\*/g, 'splat_')
81
- .replace(/\?/g, 'opt')
82
- .replace(/[^a-zA-Z0-9_]/g, '_')
83
- .replace(/_+/g, '_')
84
- .replace(/^_+|_+$/g, '');
85
- }
86
- function resolveTypeScriptApi(projectRoot) {
87
- try {
88
- const projectRequire = createRequire(join(projectRoot, '__zenith_server_output_loader__.js'));
89
- return projectRequire('typescript');
90
- }
91
- catch {
92
- try {
93
- return PACKAGE_REQUIRE('typescript');
94
- }
95
- catch {
96
- throw new Error('[Zenith:Build] Server-capable targets require the `typescript` package to transpile route modules.');
97
- }
98
- }
99
- }
100
- function withJsExtension(specifier) {
101
- if (specifier.endsWith('.json')) {
102
- return specifier;
103
- }
104
- return specifier.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
105
- }
106
- function replaceSpecifier(source, original, nextValue) {
107
- return source.replace(new RegExp(`(['"])${escapeRegex(original)}\\1`, 'g'), (_, quote) => `${quote}${nextValue}${quote}`);
108
- }
109
- function escapeRegex(value) {
110
- return String(value).replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
111
- }
112
- function isRelativeSpecifier(specifier) {
113
- return (specifier.startsWith('./') ||
114
- specifier.startsWith('../') ||
115
- specifier.startsWith('/') ||
116
- specifier.startsWith('file:'));
117
- }
118
- function resolveModuleCandidates(basePath) {
119
- if (extname(basePath)) {
120
- return [basePath];
121
- }
78
+ let scopedServerDataLoweringPromise = null;
79
+ function resolveScopedServerDataLoweringPath() {
80
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
122
81
  return [
123
- basePath,
124
- `${basePath}.ts`,
125
- `${basePath}.tsx`,
126
- `${basePath}.mts`,
127
- `${basePath}.cts`,
128
- `${basePath}.js`,
129
- `${basePath}.mjs`,
130
- `${basePath}.cjs`,
131
- `${basePath}.json`,
132
- join(basePath, 'index.ts'),
133
- join(basePath, 'index.tsx'),
134
- join(basePath, 'index.mts'),
135
- join(basePath, 'index.cts'),
136
- join(basePath, 'index.js'),
137
- join(basePath, 'index.mjs'),
138
- join(basePath, 'index.cjs'),
139
- join(basePath, 'index.json')
140
- ];
82
+ join(moduleDir, 'scoped-server-data', 'lowering.js'),
83
+ join(moduleDir, '..', 'dist', 'scoped-server-data', 'lowering.js')
84
+ ].find((candidate) => existsSync(candidate)) || null;
141
85
  }
142
- function resolveImportedModule(specifier, sourceFile) {
143
- if (!isRelativeSpecifier(specifier)) {
144
- return null;
145
- }
146
- const baseDir = dirname(sourceFile);
147
- const basePath = specifier.startsWith('file:')
148
- ? new URL(specifier)
149
- : resolve(baseDir, specifier);
150
- const filePath = basePath instanceof URL ? fileURLToPath(basePath) : basePath;
151
- const candidates = resolveModuleCandidates(filePath);
152
- const found = candidates.find((candidate) => existsSync(candidate));
153
- if (!found) {
154
- throw new Error(`[Zenith:Build] Cannot resolve server import "${specifier}" from "${sourceFile}"`);
86
+ async function getScopedServerDataLowering() {
87
+ const helperPath = resolveScopedServerDataLoweringPath();
88
+ if (!helperPath) {
89
+ throw new Error(SCOPED_SERVER_DATA_LOWERING_HELPER_UNAVAILABLE);
155
90
  }
156
- return found;
157
- }
158
- function gatherSpecifiers(source) {
159
- const results = [];
160
- for (const match of source.matchAll(RELATIVE_SPECIFIER_RE)) {
161
- const specifier = String(match[3] || '');
162
- results.push(specifier);
91
+ if (!scopedServerDataLoweringPromise) {
92
+ scopedServerDataLoweringPromise = import(pathToFileURL(helperPath).href);
163
93
  }
164
- return results;
165
- }
166
- function transpileSource(ts, source, filePath) {
167
- return ts.transpileModule(source, {
168
- compilerOptions: {
169
- module: ts.ModuleKind.ESNext,
170
- target: ts.ScriptTarget.ES2022,
171
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
172
- esModuleInterop: true,
173
- allowSyntheticDefaultImports: true
174
- },
175
- fileName: filePath
176
- }).outputText;
177
- }
178
- function outputPathForSource(projectRoot, modulesRoot, sourcePath) {
179
- const relativePath = relative(projectRoot, sourcePath).replaceAll('\\', '/');
180
- const nextRelative = extname(relativePath) === '.json'
181
- ? relativePath
182
- : relativePath.replace(/\.(tsx|ts|mts|cts|jsx|js|mjs|cjs)$/i, '.js');
183
- return join(modulesRoot, nextRelative);
184
- }
185
- async function compileImportedModule({ projectRoot, modulesRoot, serverDir, sourcePath, ts, seen }) {
186
- if (seen.has(sourcePath)) {
187
- return outputPathForSource(projectRoot, modulesRoot, sourcePath);
188
- }
189
- seen.add(sourcePath);
190
- const outPath = outputPathForSource(projectRoot, modulesRoot, sourcePath);
191
- await mkdir(dirname(outPath), { recursive: true });
192
- if (extname(sourcePath) === '.json') {
193
- await cp(sourcePath, outPath, { force: true });
194
- return outPath;
195
- }
196
- const source = await readFile(sourcePath, 'utf8');
197
- let output = transpileSource(ts, source, sourcePath);
198
- for (const specifier of gatherSpecifiers(output)) {
199
- const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
200
- if (specialSpecifierPath) {
201
- const nextSpecifier = relative(dirname(outPath), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
202
- output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
203
- continue;
204
- }
205
- if (!isRelativeSpecifier(specifier)) {
206
- continue;
207
- }
208
- const resolvedPath = resolveImportedModule(specifier, sourcePath);
209
- if (!resolvedPath) {
210
- continue;
211
- }
212
- const compiledDependencyPath = await compileImportedModule({
213
- projectRoot,
214
- modulesRoot,
215
- serverDir,
216
- sourcePath: resolvedPath,
217
- ts,
218
- seen
219
- });
220
- const nextSpecifier = relative(dirname(outPath), compiledDependencyPath).replaceAll('\\', '/');
221
- output = replaceSpecifier(output, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
222
- }
223
- await writeFile(outPath, output, 'utf8');
224
- return outPath;
94
+ return scopedServerDataLoweringPromise;
225
95
  }
226
96
  async function writeRouteModulePackage({ projectRoot, serverDir, routeDir, route }) {
227
- const ts = resolveTypeScriptApi(projectRoot);
228
- const modulesRoot = join(routeDir, 'modules');
229
- const seen = new Set();
230
- let entryOutput = transpileSource(ts, route.server_script || '', route.server_script_path || 'route-entry.ts');
231
- for (const specifier of gatherSpecifiers(entryOutput)) {
232
- const specialSpecifierPath = SPECIAL_SERVER_SPECIFIERS.get(specifier);
233
- if (specialSpecifierPath) {
234
- const nextSpecifier = relative(join(routeDir, 'route'), join(serverDir, specialSpecifierPath)).replaceAll('\\', '/');
235
- entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
236
- continue;
237
- }
238
- if (!isRelativeSpecifier(specifier)) {
239
- continue;
240
- }
241
- const resolvedPath = resolveImportedModule(specifier, route.server_script_path || projectRoot);
242
- if (!resolvedPath) {
243
- continue;
244
- }
245
- const compiledDependencyPath = await compileImportedModule({
246
- projectRoot,
247
- modulesRoot,
248
- serverDir,
249
- sourcePath: resolvedPath,
250
- ts,
251
- seen
252
- });
253
- const nextSpecifier = relative(join(routeDir, 'route'), compiledDependencyPath).replaceAll('\\', '/');
254
- entryOutput = replaceSpecifier(entryOutput, specifier, nextSpecifier.startsWith('.') ? nextSpecifier : `./${nextSpecifier}`);
255
- }
256
- const routeModulePath = join(routeDir, 'route', 'entry.js');
257
- await mkdir(dirname(routeModulePath), { recursive: true });
258
- 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
+ });
259
105
  }
260
106
  async function copyRuntimeFiles(serverDir) {
261
107
  for (const file of SERVER_RUNTIME_FILES) {
@@ -275,7 +121,81 @@ async function copyOptionalFile(sourcePath, targetPath) {
275
121
  await cp(sourcePath, targetPath, { force: true });
276
122
  return true;
277
123
  }
278
- 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 = {} }) {
279
199
  const serverDir = join(coreOutputDir, 'server');
280
200
  await rm(serverDir, { recursive: true, force: true });
281
201
  let routerManifest = { routes: [] };
@@ -286,20 +206,31 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
286
206
  routerManifest = { routes: [] };
287
207
  }
288
208
  const resourceManifest = await loadResourceRouteManifest(staticDir, basePath);
289
- 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)));
290
214
  const serverRoutes = pageRoutes
291
- .filter((route) => route.server_script && route.prerender !== true)
215
+ .filter((route) => route.prerender !== true && (route.server_script || route.has_scoped_server_data === true))
292
216
  .map((route) => ({ ...route, route_kind: 'page' }))
293
217
  .concat((Array.isArray(resourceManifest.routes) ? resourceManifest.routes : []).map((route) => ({
294
218
  ...route,
295
219
  route_kind: 'resource'
296
220
  })));
221
+ if (serverRoutes.some((route) => route.route_kind !== 'resource' && route.has_scoped_server_data === true)) {
222
+ await getScopedServerDataLowering();
223
+ }
297
224
  await mkdir(serverDir, { recursive: true });
298
225
  await copyRuntimeFiles(serverDir);
226
+ const serverGlobalMiddlewareMetadata = await writeGlobalMiddlewarePackage({
227
+ projectRoot,
228
+ serverDir,
229
+ globalMiddleware
230
+ });
299
231
  const imageManifestSource = join(staticDir, '_zenith', 'image', 'manifest.json');
300
232
  const emittedRoutes = [];
301
- for (const route of serverRoutes) {
302
- const name = normalizeRouteName(route.path);
233
+ for (const { route, name } of assignServerRouteNames(serverRoutes)) {
303
234
  const routeDir = join(serverDir, 'routes', name);
304
235
  await mkdir(routeDir, { recursive: true });
305
236
  if (route.route_kind !== 'resource') {
@@ -324,6 +255,15 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
324
255
  routeDir,
325
256
  route
326
257
  });
258
+ const scopedServerData = await writeScopedServerDataModules({
259
+ projectRoot,
260
+ serverDir,
261
+ route,
262
+ pagesDir,
263
+ srcDir,
264
+ registry,
265
+ compilerOpts
266
+ });
327
267
  const meta = {
328
268
  name,
329
269
  path: route.path,
@@ -346,13 +286,22 @@ export async function writeServerOutput({ coreOutputDir, staticDir, projectRoot,
346
286
  image_manifest_file: route.route_kind === 'resource' ? null : imageManifestFile,
347
287
  image_config: config?.images || {}
348
288
  };
289
+ if (scopedServerData.length > 0) {
290
+ meta.has_scoped_server_data = true;
291
+ meta.scoped_server_data = scopedServerData;
292
+ }
349
293
  if (route.route_kind !== 'resource' && Array.isArray(route.image_materialization) && route.image_materialization.length > 0) {
350
294
  meta.image_materialization = route.image_materialization;
351
295
  }
352
296
  await writeFile(join(routeDir, 'route.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
353
297
  emittedRoutes.push(meta);
354
298
  }
355
- 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');
356
305
  return {
357
306
  serverDir,
358
307
  routes: emittedRoutes
@@ -0,0 +1,2 @@
1
+ export function normalizeRouteName(routePath: any): any;
2
+ export function assignServerRouteNames(routes: any): any;
@@ -0,0 +1,38 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function normalizeRouteName(routePath) {
3
+ if (routePath === '/') {
4
+ return 'index';
5
+ }
6
+ return routePath
7
+ .replace(/^\//, '')
8
+ .replace(/\/+/g, '_')
9
+ .replace(/:/g, 'param_')
10
+ .replace(/\*/g, 'splat_')
11
+ .replace(/\?/g, 'opt')
12
+ .replace(/[^a-zA-Z0-9_]/g, '_')
13
+ .replace(/_+/g, '_')
14
+ .replace(/^_+|_+$/g, '') || 'route';
15
+ }
16
+ function routeNameHash(route) {
17
+ return createHash('sha256')
18
+ .update(`${route.route_kind || 'page'}:${route.path || ''}`)
19
+ .digest('hex')
20
+ .slice(0, 8);
21
+ }
22
+ export function assignServerRouteNames(routes) {
23
+ const used = new Set();
24
+ return routes.map((route) => {
25
+ const baseName = normalizeRouteName(route.path);
26
+ let name = baseName;
27
+ if (used.has(name)) {
28
+ name = `${baseName}_${routeNameHash(route)}`;
29
+ }
30
+ let suffix = 2;
31
+ while (used.has(name)) {
32
+ name = `${baseName}_${routeNameHash(route)}_${suffix}`;
33
+ suffix += 1;
34
+ }
35
+ used.add(name);
36
+ return { route, name };
37
+ });
38
+ }
@@ -0,0 +1 @@
1
+ export { executeMatchedRoutePipeline, runGlobalMiddlewareChain } from "../server-contract/resolve.js";
@@ -0,0 +1 @@
1
+ export { executeMatchedRoutePipeline, runGlobalMiddlewareChain } from '../server-contract/resolve.js';
@@ -11,6 +11,7 @@ import { executeRouteRequest, renderResourceRouteRequest, renderRouteRequest } f
11
11
  import { resolveRequestRoute } from './resolve-request-route.js';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
14
+ const PUBLIC_ORIGIN_ENV = 'ZENITH_PUBLIC_ORIGIN';
14
15
  const MIME_TYPES = {
15
16
  '.html': 'text/html; charset=utf-8',
16
17
  '.js': 'application/javascript; charset=utf-8',
@@ -60,6 +61,22 @@ function resolveWithinRoot(rootDir, requestPath) {
60
61
  }
61
62
  return null;
62
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
+ }
63
80
  function toStaticFilePath(staticDir, pathname) {
64
81
  let resolvedPath = pathname;
65
82
  if (resolvedPath === '/') {
@@ -192,6 +209,7 @@ async function loadRuntimeContext(options = {}) {
192
209
  });
193
210
  const serverManifest = await readJson(join(serverDir, 'manifest.json'), { routes: [] });
194
211
  const allServerRoutes = Array.isArray(serverManifest.routes) ? serverManifest.routes : [];
212
+ const globalMiddlewareModulePath = resolveManifestMiddlewareModulePath(serverDir, serverManifest);
195
213
  return {
196
214
  distDir,
197
215
  serverDir,
@@ -201,6 +219,7 @@ async function loadRuntimeContext(options = {}) {
201
219
  serverRoutes: allServerRoutes,
202
220
  pageServerRoutes: allServerRoutes.filter((route) => route?.route_kind !== 'resource'),
203
221
  resourceServerRoutes: allServerRoutes.filter((route) => route?.route_kind === 'resource'),
222
+ globalMiddlewareModulePath,
204
223
  images: config.images || {},
205
224
  basePath: normalizeBasePath(config.base_path || '/')
206
225
  };
@@ -322,7 +341,8 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
322
341
  request,
323
342
  route: resourceResolved.route,
324
343
  params: resourceResolved.params,
325
- routeModulePath: join(routeDir, 'route', 'entry.js')
344
+ routeModulePath: join(routeDir, 'route', 'entry.js'),
345
+ globalMiddlewareModulePath: context.globalMiddlewareModulePath
326
346
  });
327
347
  await sendFetchResponse(res, response, req.method);
328
348
  return;
@@ -336,6 +356,7 @@ async function handleNodeRequest(req, res, context, serverOrigin) {
336
356
  route: serverResolved.route,
337
357
  params: serverResolved.params,
338
358
  routeModulePath: join(routeDir, 'route', 'entry.js'),
359
+ globalMiddlewareModulePath: context.globalMiddlewareModulePath,
339
360
  shellHtmlPath: join(routeDir, 'route', 'page.html'),
340
361
  imageManifestPath: serverResolved.route.image_manifest_file
341
362
  ? join(routeDir, 'route', serverResolved.route.image_manifest_file)
@@ -375,8 +396,9 @@ function createNodeRequestHandler(context, resolveServerOrigin) {
375
396
  }
376
397
  export async function createRequestHandler(options = {}) {
377
398
  const context = await loadRuntimeContext(options);
399
+ const publicOrigin = options.publicOrigin ?? process.env[PUBLIC_ORIGIN_ENV];
378
400
  const resolveServerOrigin = createTrustedOriginResolver({
379
- publicOrigin: options.publicOrigin,
401
+ publicOrigin,
380
402
  host: options.host || '127.0.0.1',
381
403
  port: Number.isInteger(options.port) ? options.port : undefined,
382
404
  label: 'createRequestHandler()'
@@ -388,8 +410,9 @@ export async function createNodeServer(options = {}) {
388
410
  const { port = 3000, host = '127.0.0.1' } = options;
389
411
  const context = await loadRuntimeContext(options);
390
412
  let actualPort = Number.isInteger(port) && port > 0 ? port : 0;
413
+ const publicOrigin = options.publicOrigin ?? process.env[PUBLIC_ORIGIN_ENV];
391
414
  const resolveServerOrigin = createTrustedOriginResolver({
392
- publicOrigin: options.publicOrigin,
415
+ publicOrigin,
393
416
  host,
394
417
  getPort: () => actualPort,
395
418
  label: 'createNodeServer()'
@@ -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>;