@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
@@ -0,0 +1,252 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
+ const PACKAGE_REQUIRE = createRequire(import.meta.url);
6
+ const STATIC_MIDDLEWARE_TARGETS = new Set([
7
+ 'static',
8
+ 'static-export',
9
+ 'vercel-static',
10
+ 'netlify-static'
11
+ ]);
12
+ function toPosixRelative(from, to) {
13
+ const relativePath = relative(from, to).replaceAll('\\', '/');
14
+ return relativePath || '.';
15
+ }
16
+ function middlewareError(sourceFile, message) {
17
+ return new Error(`[Zenith:Middleware] Invalid global middleware in ${sourceFile}: ${message}`);
18
+ }
19
+ function resolveTypeScriptApi(projectRoot) {
20
+ try {
21
+ const projectRequire = createRequire(join(projectRoot, '__zenith_middleware_parser__.js'));
22
+ return projectRequire('typescript');
23
+ }
24
+ catch {
25
+ try {
26
+ return PACKAGE_REQUIRE('typescript');
27
+ }
28
+ catch {
29
+ throw new Error('[Zenith:Middleware] Global middleware validation requires the `typescript` package to be installed.');
30
+ }
31
+ }
32
+ }
33
+ function hasModifier(ts, node, kind) {
34
+ return Boolean(node?.modifiers?.some((modifier) => modifier.kind === kind));
35
+ }
36
+ function isAllowedTypeOnlyNamedExport(ts, node) {
37
+ if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
38
+ return hasModifier(ts, node, ts.SyntaxKind.ExportKeyword)
39
+ && !hasModifier(ts, node, ts.SyntaxKind.DefaultKeyword);
40
+ }
41
+ if (ts.isExportDeclaration(node)) {
42
+ if (node.isTypeOnly) {
43
+ return true;
44
+ }
45
+ const elements = node.exportClause && ts.isNamedExports(node.exportClause)
46
+ ? node.exportClause.elements
47
+ : [];
48
+ return elements.length > 0 && elements.every((specifier) => specifier.isTypeOnly === true);
49
+ }
50
+ return false;
51
+ }
52
+ function unwrapExpression(ts, expression) {
53
+ let current = expression;
54
+ while (current && ts.isParenthesizedExpression(current)) {
55
+ current = current.expression;
56
+ }
57
+ return current;
58
+ }
59
+ function isFunctionLikeDefault(ts, expression) {
60
+ const unwrapped = unwrapExpression(ts, expression);
61
+ return ts.isFunctionExpression(unwrapped) || ts.isArrowFunction(unwrapped)
62
+ ? unwrapped
63
+ : null;
64
+ }
65
+ function propertyAccessPath(ts, node) {
66
+ const parts = [];
67
+ let current = node;
68
+ while (current && ts.isPropertyAccessExpression(current)) {
69
+ parts.unshift(current.name.text);
70
+ current = current.expression;
71
+ }
72
+ if (current && ts.isIdentifier(current)) {
73
+ parts.unshift(current.text);
74
+ }
75
+ return parts;
76
+ }
77
+ function hasCommonJsExport(ts, node) {
78
+ let found = false;
79
+ function visit(current) {
80
+ if (found) {
81
+ return;
82
+ }
83
+ if (ts.isBinaryExpression(current)
84
+ && current.operatorToken.kind === ts.SyntaxKind.EqualsToken
85
+ && ts.isPropertyAccessExpression(current.left)) {
86
+ const parts = propertyAccessPath(ts, current.left);
87
+ if (parts[0] === 'module' && parts[1] === 'exports') {
88
+ found = true;
89
+ return;
90
+ }
91
+ if (parts[0] === 'exports') {
92
+ found = true;
93
+ return;
94
+ }
95
+ }
96
+ ts.forEachChild(current, visit);
97
+ }
98
+ ts.forEachChild(node, visit);
99
+ return found;
100
+ }
101
+ function assertTwoNonRestParams(fn, sourceFile) {
102
+ const params = Array.isArray(fn?.parameters) ? fn.parameters : [];
103
+ const hasRest = params.some((param) => param.dotDotDotToken);
104
+ if (params.length !== 2 || hasRest) {
105
+ throw middlewareError(sourceFile, 'default function must accept exactly two arguments: ctx and next.');
106
+ }
107
+ }
108
+ export function validateGlobalMiddlewareSource(source, sourceFile, projectRoot = process.cwd()) {
109
+ const ts = resolveTypeScriptApi(projectRoot);
110
+ const parsed = ts.createSourceFile(sourceFile, String(source || ''), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
111
+ if (parsed.parseDiagnostics.length > 0) {
112
+ throw middlewareError(sourceFile, 'unable to parse middleware module.');
113
+ }
114
+ if (hasCommonJsExport(ts, parsed)) {
115
+ throw middlewareError(sourceFile, 'CommonJS middleware exports are not supported. Use `export default function middleware(ctx, next) { ... }`.');
116
+ }
117
+ let defaultExportCount = 0;
118
+ let defaultFunction = null;
119
+ let defaultExportWasNonFunction = false;
120
+ let hasNamedRuntimeExport = false;
121
+ for (const statement of parsed.statements) {
122
+ if (ts.isExportDeclaration(statement)) {
123
+ if (!isAllowedTypeOnlyNamedExport(ts, statement)) {
124
+ hasNamedRuntimeExport = true;
125
+ }
126
+ continue;
127
+ }
128
+ if (ts.isExportAssignment(statement)) {
129
+ if (statement.isExportEquals) {
130
+ throw middlewareError(sourceFile, 'CommonJS middleware exports are not supported. Use `export default function middleware(ctx, next) { ... }`.');
131
+ }
132
+ defaultExportCount += 1;
133
+ const fn = isFunctionLikeDefault(ts, statement.expression);
134
+ if (fn) {
135
+ defaultFunction = fn;
136
+ }
137
+ else {
138
+ defaultExportWasNonFunction = true;
139
+ }
140
+ continue;
141
+ }
142
+ const hasExport = hasModifier(ts, statement, ts.SyntaxKind.ExportKeyword);
143
+ if (!hasExport) {
144
+ continue;
145
+ }
146
+ const hasDefault = hasModifier(ts, statement, ts.SyntaxKind.DefaultKeyword);
147
+ if (hasDefault) {
148
+ defaultExportCount += 1;
149
+ if (ts.isFunctionDeclaration(statement)) {
150
+ defaultFunction = statement;
151
+ }
152
+ else {
153
+ defaultExportWasNonFunction = true;
154
+ }
155
+ continue;
156
+ }
157
+ if (!isAllowedTypeOnlyNamedExport(ts, statement)) {
158
+ hasNamedRuntimeExport = true;
159
+ }
160
+ }
161
+ if (hasNamedRuntimeExport) {
162
+ throw middlewareError(sourceFile, 'named runtime exports are not supported. Export only `default function middleware(ctx, next)`.');
163
+ }
164
+ if (defaultExportCount !== 1) {
165
+ throw middlewareError(sourceFile, 'expected exactly one default export function.');
166
+ }
167
+ if (!defaultFunction || defaultExportWasNonFunction) {
168
+ throw middlewareError(sourceFile, 'default export must be a function. Use `export default function middleware(ctx, next) { ... }`.');
169
+ }
170
+ assertTwoNonRestParams(defaultFunction, sourceFile);
171
+ }
172
+ async function findNestedMiddlewareFiles(dir, projectRoot) {
173
+ const matches = [];
174
+ let entries;
175
+ try {
176
+ entries = await readdir(dir, { withFileTypes: true });
177
+ }
178
+ catch {
179
+ return matches;
180
+ }
181
+ entries.sort((left, right) => left.name.localeCompare(right.name));
182
+ for (const entry of entries) {
183
+ const fullPath = join(dir, entry.name);
184
+ if (entry.isDirectory()) {
185
+ if (entry.name === 'middleware' && existsSync(join(fullPath, 'index.ts'))) {
186
+ matches.push(join(fullPath, 'index.ts'));
187
+ }
188
+ matches.push(...await findNestedMiddlewareFiles(fullPath, projectRoot));
189
+ continue;
190
+ }
191
+ if (entry.isFile() && entry.name === 'middleware.ts') {
192
+ matches.push(fullPath);
193
+ }
194
+ }
195
+ return matches.sort((left, right) => (toPosixRelative(projectRoot, left).localeCompare(toPosixRelative(projectRoot, right))));
196
+ }
197
+ function createMetadata(sourceFile) {
198
+ return { source_file: sourceFile };
199
+ }
200
+ export function normalizeGlobalMiddlewareMetadata(globalMiddleware) {
201
+ const sourceFile = typeof globalMiddleware?.source_file === 'string'
202
+ ? globalMiddleware.source_file
203
+ : typeof globalMiddleware?.sourceFile === 'string'
204
+ ? globalMiddleware.sourceFile
205
+ : null;
206
+ return sourceFile ? createMetadata(sourceFile) : null;
207
+ }
208
+ export function assertGlobalMiddlewareTargetSupported(target, globalMiddleware) {
209
+ if (!globalMiddleware || !STATIC_MIDDLEWARE_TARGETS.has(target)) {
210
+ return;
211
+ }
212
+ throw new Error(`[Zenith:Middleware] target "${target}" cannot use global middleware. ` +
213
+ 'Global middleware requires a server-capable target ("node", "vercel", or "netlify"). ' +
214
+ `File: ${globalMiddleware.sourceFile}.`);
215
+ }
216
+ export async function resolveGlobalMiddleware({ projectRoot, pagesDir, target } = {}) {
217
+ const resolvedProjectRoot = resolve(projectRoot || process.cwd());
218
+ const resolvedPagesDir = resolve(resolvedProjectRoot, pagesDir || 'pages');
219
+ const middlewareRoot = dirname(resolvedPagesDir);
220
+ const rootCandidates = [
221
+ join(middlewareRoot, 'middleware.ts'),
222
+ join(middlewareRoot, 'middleware', 'index.ts')
223
+ ].filter((candidate) => existsSync(candidate));
224
+ if (rootCandidates.length > 1) {
225
+ throw new Error(`[Zenith:Middleware] Multiple global middleware files found in "${middlewareRoot}". ` +
226
+ 'Keep exactly one of: middleware.ts, middleware/index.ts.');
227
+ }
228
+ const nestedMatches = await findNestedMiddlewareFiles(resolvedPagesDir, resolvedProjectRoot);
229
+ if (nestedMatches.length > 0) {
230
+ const relativePath = toPosixRelative(resolvedProjectRoot, nestedMatches[0]);
231
+ const middlewareRootRelative = toPosixRelative(resolvedProjectRoot, middlewareRoot);
232
+ const targetPath = middlewareRootRelative === '.'
233
+ ? 'middleware.ts'
234
+ : `${middlewareRootRelative}/middleware.ts`;
235
+ throw new Error('[Zenith:Middleware] Nested middleware files are not supported in V1. ' +
236
+ `Move "${relativePath}" to "${targetPath}" or remove it.`);
237
+ }
238
+ if (rootCandidates.length === 0) {
239
+ return null;
240
+ }
241
+ const sourcePath = rootCandidates[0];
242
+ const sourceFile = toPosixRelative(resolvedProjectRoot, sourcePath);
243
+ const globalMiddleware = {
244
+ sourcePath,
245
+ sourceFile,
246
+ root: middlewareRoot,
247
+ metadata: createMetadata(sourceFile)
248
+ };
249
+ assertGlobalMiddlewareTargetSupported(target, globalMiddleware);
250
+ validateGlobalMiddlewareSource(await readFile(sourcePath, 'utf8'), sourceFile, resolvedProjectRoot);
251
+ return globalMiddleware;
252
+ }
@@ -1,3 +1,5 @@
1
+ export function analyzeRouteScopedServerMetadata(options: import("./scoped-server-data/types.js").AnalyzeRouteScopedServerMetadataOptions): import("./scoped-server-data/types.js").AnalyzeRouteScopedServerMetadataResult;
2
+ export function assertNoScopedServerBuildErrors(diagnostics: import("./scoped-server-data/types.js").ScopedServerDiagnostic[], contextFile: string): void;
1
3
  /**
2
4
  * @typedef {{
3
5
  * path: string,
@@ -11,6 +13,8 @@
11
13
  * has_guard?: boolean,
12
14
  * has_load?: boolean,
13
15
  * has_action?: boolean,
16
+ * has_scoped_server_data?: boolean,
17
+ * scoped_server_data?: import('./scoped-server-data/types.js').ManifestScopedServerDataEntry[],
14
18
  * export_paths?: string[]
15
19
  * }} ManifestEntry
16
20
  */
@@ -19,11 +23,13 @@
19
23
  *
20
24
  * @param {string} pagesDir - Absolute path to /pages directory
21
25
  * @param {string} [extension='.zen'] - File extension to scan for
22
- * @param {{ compilerOpts?: object }} [options]
26
+ * @param {{ compilerOpts?: object, srcDir?: string, registry?: Map<string, string> }} [options]
23
27
  * @returns {Promise<ManifestEntry[]>}
24
28
  */
25
29
  export function generateManifest(pagesDir: string, extension?: string, options?: {
26
30
  compilerOpts?: object;
31
+ srcDir?: string;
32
+ registry?: Map<string, string>;
27
33
  }): Promise<ManifestEntry[]>;
28
34
  /**
29
35
  * Generate a JavaScript module string from manifest entries.
@@ -45,5 +51,7 @@ export type ManifestEntry = {
45
51
  has_guard?: boolean;
46
52
  has_load?: boolean;
47
53
  has_action?: boolean;
54
+ has_scoped_server_data?: boolean;
55
+ scoped_server_data?: import("./scoped-server-data/types.js").ManifestScopedServerDataEntry[];
48
56
  export_paths?: string[];
49
57
  };
package/dist/manifest.js CHANGED
@@ -1,27 +1,41 @@
1
- // ---------------------------------------------------------------------------
2
- // manifest.js Zenith CLI V0
3
- // ---------------------------------------------------------------------------
4
- // File-based manifest engine.
5
- //
6
- // Scans a /pages directory and produces a deterministic RouteManifest.
7
- //
8
- // Rules:
9
- // - index.zen → parent directory path
10
- // - [param].zen → :param dynamic segment
11
- // - [...slug].zen → *slug catch-all segment (must be terminal, 1+ segments;
12
- // root '/*slug' may match '/' in router matcher)
13
- // - [[...slug]].zen → *slug? optional catch-all segment (must be terminal, 0+ segments)
14
- // - Deterministic precedence: static > :param > *catchall
15
- // - Tie-breaker: lexicographic route path
16
- // ---------------------------------------------------------------------------
17
- import { readFileSync } from 'node:fs';
1
+ // File-based manifest engine. Scans /pages and produces deterministic RouteManifest entries.
2
+ // Rules: static > :param > *catchall, then lexicographic tie-breaker.
3
+ import { readFileSync, existsSync } from 'node:fs';
18
4
  import { readdir, stat } from 'node:fs/promises';
19
5
  import { join, relative, sep, basename, extname, dirname, resolve } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
7
  import { extractServerScript } from './build/server-script.js';
21
8
  import { analyzeResourceRouteModule, isResourceRouteFile } from './resource-route-module.js';
22
9
  import { composeServerScriptEnvelope, resolveAdjacentServerModules } from './server-script-composition.js';
23
10
  import { validateStaticExportPaths } from './static-export-paths.js';
24
11
  import { classifyPageRoute } from './route-classification.js';
12
+ import { buildComponentRegistry } from './resolve-components.js';
13
+ const SCOPED_SERVER_DATA_HELPER_UNAVAILABLE = '[Zenith:ScopedServerData] Manifest integration helper is unavailable. Run the CLI build step before using scoped server data manifest integration.';
14
+ function resolveManifestIntegrationPath() {
15
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
16
+ return [
17
+ join(moduleDir, 'scoped-server-data', 'manifest-integration.js'),
18
+ join(moduleDir, '..', 'dist', 'scoped-server-data', 'manifest-integration.js')
19
+ ].find((candidate) => existsSync(candidate)) || null;
20
+ }
21
+ const manifestIntegrationPath = resolveManifestIntegrationPath();
22
+ const manifestIntegration = manifestIntegrationPath
23
+ ? await import(pathToFileURL(manifestIntegrationPath).href)
24
+ : null;
25
+ function getManifestIntegration() {
26
+ if (!manifestIntegration) {
27
+ throw new Error(SCOPED_SERVER_DATA_HELPER_UNAVAILABLE);
28
+ }
29
+ return manifestIntegration;
30
+ }
31
+ /** @type {typeof import('./scoped-server-data/manifest-integration.js').analyzeRouteScopedServerMetadata} */
32
+ export function analyzeRouteScopedServerMetadata(options) {
33
+ return getManifestIntegration().analyzeRouteScopedServerMetadata(options);
34
+ }
35
+ /** @type {typeof import('./scoped-server-data/manifest-integration.js').assertNoScopedServerBuildErrors} */
36
+ export function assertNoScopedServerBuildErrors(diagnostics, contextFile) {
37
+ return getManifestIntegration().assertNoScopedServerBuildErrors(diagnostics, contextFile);
38
+ }
25
39
  /**
26
40
  * @typedef {{
27
41
  * path: string,
@@ -35,6 +49,8 @@ import { classifyPageRoute } from './route-classification.js';
35
49
  * has_guard?: boolean,
36
50
  * has_load?: boolean,
37
51
  * has_action?: boolean,
52
+ * has_scoped_server_data?: boolean,
53
+ * scoped_server_data?: import('./scoped-server-data/types.js').ManifestScopedServerDataEntry[],
38
54
  * export_paths?: string[]
39
55
  * }} ManifestEntry
40
56
  */
@@ -43,11 +59,15 @@ import { classifyPageRoute } from './route-classification.js';
43
59
  *
44
60
  * @param {string} pagesDir - Absolute path to /pages directory
45
61
  * @param {string} [extension='.zen'] - File extension to scan for
46
- * @param {{ compilerOpts?: object }} [options]
62
+ * @param {{ compilerOpts?: object, srcDir?: string, registry?: Map<string, string> }} [options]
47
63
  * @returns {Promise<ManifestEntry[]>}
48
64
  */
49
65
  export async function generateManifest(pagesDir, extension = '.zen', options = {}) {
50
- const entries = await _scanDir(pagesDir, pagesDir, extension, options.compilerOpts || {});
66
+ const resolvedPagesDir = resolve(pagesDir);
67
+ const srcDir = resolve(options.srcDir || resolve(resolvedPagesDir, '..'));
68
+ const registry = options.registry || buildComponentRegistry(srcDir);
69
+ const scanContext = { srcDir, registry, compilerOpts: options.compilerOpts || {} };
70
+ const entries = await _scanDir(resolvedPagesDir, resolvedPagesDir, extension, scanContext);
51
71
  const apiAliasState = _resolveSrcApiAliasState(pagesDir);
52
72
  if (apiAliasState) {
53
73
  const aliasEntries = await _scanResourceDir(apiAliasState.aliasDir, apiAliasState.srcDir);
@@ -69,7 +89,7 @@ export async function generateManifest(pagesDir, extension = '.zen', options = {
69
89
  * @param {string} ext - Extension to match
70
90
  * @returns {Promise<ManifestEntry[]>}
71
91
  */
72
- async function _scanDir(dir, root, ext, compilerOpts) {
92
+ async function _scanDir(dir, root, ext, scanContext) {
73
93
  /** @type {ManifestEntry[]} */
74
94
  const entries = [];
75
95
  let items;
@@ -85,7 +105,7 @@ async function _scanDir(dir, root, ext, compilerOpts) {
85
105
  const fullPath = join(dir, item);
86
106
  const info = await stat(fullPath);
87
107
  if (info.isDirectory()) {
88
- const nested = await _scanDir(fullPath, root, ext, compilerOpts);
108
+ const nested = await _scanDir(fullPath, root, ext, scanContext);
89
109
  entries.push(...nested);
90
110
  }
91
111
  else if (item.endsWith(ext)) {
@@ -94,7 +114,7 @@ async function _scanDir(dir, root, ext, compilerOpts) {
94
114
  fullPath,
95
115
  root,
96
116
  routePath,
97
- compilerOpts
117
+ scanContext
98
118
  }));
99
119
  }
100
120
  else if (isResourceRouteFile(item)) {
@@ -143,7 +163,8 @@ function _resolveSrcApiAliasState(pagesDir) {
143
163
  srcDir
144
164
  };
145
165
  }
146
- function buildPageManifestEntry({ fullPath, root, routePath, compilerOpts }) {
166
+ function buildPageManifestEntry({ fullPath, root, routePath, scanContext }) {
167
+ const { srcDir, registry, compilerOpts } = scanContext;
147
168
  const rawSource = readFileSync(fullPath, 'utf8');
148
169
  const inlineServerScript = extractServerScript(rawSource, fullPath, compilerOpts).serverScript;
149
170
  const { guardPath, loadPath, actionPath } = resolveAdjacentServerModules(fullPath);
@@ -154,20 +175,39 @@ function buildPageManifestEntry({ fullPath, root, routePath, compilerOpts }) {
154
175
  adjacentLoadPath: loadPath,
155
176
  adjacentActionPath: actionPath
156
177
  });
178
+ const scopedMetadata = analyzeRouteScopedServerMetadata({
179
+ pageSource: rawSource,
180
+ pageFile: fullPath,
181
+ registry,
182
+ srcDir,
183
+ compilerOpts
184
+ });
185
+ const manifestFile = relative(root, fullPath);
186
+ assertNoScopedServerBuildErrors(scopedMetadata.diagnostics, manifestFile);
157
187
  const exportPaths = Array.isArray(composed.serverScript?.export_paths)
158
188
  ? validateStaticExportPaths(routePath, composed.serverScript.export_paths, fullPath)
159
189
  : [];
160
190
  const classification = classifyPageRoute({
161
- file: relative(root, fullPath),
162
- serverScript: composed.serverScript
191
+ file: manifestFile,
192
+ serverScript: composed.serverScript,
193
+ hasScopedServerData: scopedMetadata.hasScopedServerData
163
194
  });
164
195
  return {
165
196
  path: routePath,
166
- file: relative(root, fullPath),
197
+ file: manifestFile,
167
198
  route_kind: 'page',
168
199
  path_kind: _isDynamic(routePath) ? 'dynamic' : 'static',
169
200
  render_mode: classification.renderMode,
170
201
  params: extractRouteParams(routePath),
202
+ has_guard: classification.hasGuard,
203
+ has_load: classification.hasLoad,
204
+ has_action: classification.hasAction,
205
+ ...(scopedMetadata.hasScopedServerData
206
+ ? {
207
+ has_scoped_server_data: true,
208
+ scoped_server_data: scopedMetadata.scopedServerData
209
+ }
210
+ : {}),
171
211
  ...(exportPaths.length > 0 ? { export_paths: exportPaths } : {})
172
212
  };
173
213
  }
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { extname, join } from 'node:path';
2
+ import { dirname, extname, join } from 'node:path';
3
3
  import { appLocalRedirectLocation, imageEndpointPath, routeCheckPath, stripBasePath } from '../base-path.js';
4
4
  import { materializeImageMarkup } from '../images/materialize.js';
5
5
  import { createImageRuntimePayload, injectImageRuntimePayload } from '../images/payload.js';
@@ -8,6 +8,7 @@ import { readRequestBodyBuffer } from '../request-body.js';
8
8
  import { buildResourceResponseDescriptor } from '../resource-response.js';
9
9
  import { clientFacingRouteMessage, logServerException, sanitizeRouteResult } from '../server-error.js';
10
10
  import { resolveRequestRoute } from '../server/resolve-request-route.js';
11
+ import { loadPreviewGlobalMiddlewareSource } from '../global-middleware-runtime-source.js';
11
12
  import { loadRouteSurfaceState } from './manifest.js';
12
13
  import { injectSsrPayload } from './payload.js';
13
14
  import { fileExists, resolveWithinDist, toStaticFilePath } from './paths.js';
@@ -32,6 +33,47 @@ function appendSetCookieHeaders(headers, setCookies = []) {
32
33
  }
33
34
  return headers;
34
35
  }
36
+ function respondWithMiddlewareSourceError(res, error) {
37
+ logServerException('preview server route execution failed', error);
38
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
39
+ res.end(clientFacingRouteMessage(500));
40
+ }
41
+ function hasRouteScopedServerData(route) {
42
+ return route?.has_scoped_server_data === true &&
43
+ Array.isArray(route?.scoped_server_data) &&
44
+ route.scoped_server_data.length > 0;
45
+ }
46
+ async function loadPreviewScopedServerRoutes(serverModuleBaseDir) {
47
+ try {
48
+ const parsed = JSON.parse(await readFile(join(serverModuleBaseDir, 'manifest.json'), 'utf8'));
49
+ return Array.isArray(parsed?.routes) ? parsed.routes : [];
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
55
+ function mergePreviewScopedServerRoutes(routeState, serverRoutes) {
56
+ const scopedByPath = new Map((Array.isArray(serverRoutes) ? serverRoutes : [])
57
+ .filter((route) => hasRouteScopedServerData(route))
58
+ .map((route) => [route.path, route]));
59
+ if (scopedByPath.size === 0) {
60
+ return routeState;
61
+ }
62
+ return {
63
+ ...routeState,
64
+ pageRoutes: (Array.isArray(routeState.pageRoutes) ? routeState.pageRoutes : []).map((route) => {
65
+ const scoped = scopedByPath.get(route.path);
66
+ if (!scoped) {
67
+ return route;
68
+ }
69
+ return {
70
+ ...route,
71
+ has_scoped_server_data: true,
72
+ scoped_server_data: scoped.scoped_server_data
73
+ };
74
+ })
75
+ };
76
+ }
35
77
  export function createPreviewRequestHandler(options) {
36
78
  const { distDir, projectRoot, config, logger, verboseLogging, configuredBasePath, routeCheckEnabled, isStaticExportTarget, serverOrigin } = options;
37
79
  async function loadImageManifest() {
@@ -44,9 +86,14 @@ export function createPreviewRequestHandler(options) {
44
86
  return {};
45
87
  }
46
88
  }
89
+ async function loadGlobalMiddlewareForRoute() {
90
+ return loadPreviewGlobalMiddlewareSource({ projectRoot, distDir });
91
+ }
47
92
  return async function previewRequestHandler(req, res) {
48
93
  const url = new URL(req.url, serverOrigin());
49
- const { basePath, pageRoutes, resourceRoutes } = await loadRouteSurfaceState(distDir, configuredBasePath);
94
+ const serverModuleBaseDir = join(dirname(distDir), 'server');
95
+ const routeState = mergePreviewScopedServerRoutes(await loadRouteSurfaceState(distDir, configuredBasePath), await loadPreviewScopedServerRoutes(serverModuleBaseDir));
96
+ const { basePath, pageRoutes, resourceRoutes } = routeState;
50
97
  const canonicalPath = stripBasePath(url.pathname, basePath);
51
98
  try {
52
99
  if (url.pathname === routeCheckPath(basePath)) {
@@ -171,6 +218,14 @@ export function createPreviewRequestHandler(options) {
171
218
  canonicalUrl.pathname = canonicalPath;
172
219
  const resolvedResource = resolveRequestRoute(canonicalUrl, resourceRoutes);
173
220
  if (resolvedResource.matched && resolvedResource.route) {
221
+ let globalMiddleware = null;
222
+ try {
223
+ globalMiddleware = await loadGlobalMiddlewareForRoute();
224
+ }
225
+ catch (error) {
226
+ respondWithMiddlewareSourceError(res, error);
227
+ return;
228
+ }
174
229
  const requestMethod = req.method || 'GET';
175
230
  const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
176
231
  ? null
@@ -186,7 +241,9 @@ export function createPreviewRequestHandler(options) {
186
241
  routePattern: resolvedResource.route.path,
187
242
  routeFile: resolvedResource.route.server_script_path || '',
188
243
  routeId: resolvedResource.route.route_id || routeIdFromSourcePath(resolvedResource.route.server_script_path || ''),
189
- routeKind: 'resource'
244
+ routeKind: 'resource',
245
+ globalMiddlewareSource: globalMiddleware?.source || '',
246
+ globalMiddlewareSourcePath: globalMiddleware?.sourcePath || ''
190
247
  });
191
248
  const descriptor = buildResourceResponseDescriptor(execution?.result, basePath, Array.isArray(execution?.setCookies) ? execution.setCookies : []);
192
249
  res.writeHead(descriptor.status, appendSetCookieHeaders(descriptor.headers, descriptor.setCookies));
@@ -216,7 +273,17 @@ export function createPreviewRequestHandler(options) {
216
273
  }
217
274
  let ssrPayload = null;
218
275
  let routeExecution = null;
219
- if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
276
+ if (resolved.matched &&
277
+ resolved.route?.prerender !== true &&
278
+ (resolved.route?.server_script || hasRouteScopedServerData(resolved.route))) {
279
+ let globalMiddleware = null;
280
+ try {
281
+ globalMiddleware = await loadGlobalMiddlewareForRoute();
282
+ }
283
+ catch (error) {
284
+ respondWithMiddlewareSourceError(res, error);
285
+ return;
286
+ }
220
287
  try {
221
288
  const requestMethod = req.method || 'GET';
222
289
  const requestBodyBuffer = requestMethod === 'GET' || requestMethod === 'HEAD'
@@ -232,7 +299,13 @@ export function createPreviewRequestHandler(options) {
232
299
  requestBodyBuffer,
233
300
  routePattern: resolved.route.path,
234
301
  routeFile: resolved.route.server_script_path || '',
235
- routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '')
302
+ routeId: resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || ''),
303
+ globalMiddlewareSource: globalMiddleware?.source || '',
304
+ globalMiddlewareSourcePath: globalMiddleware?.sourcePath || '',
305
+ scopedServerData: Array.isArray(resolved.route.scoped_server_data)
306
+ ? resolved.route.scoped_server_data
307
+ : [],
308
+ scopedServerModuleBaseDir: serverModuleBaseDir
236
309
  });
237
310
  }
238
311
  catch (error) {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource' }} input
2
+ * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource', globalMiddlewareSource?: string, globalMiddlewareSourcePath?: string, scopedServerData?: unknown[], scopedServerModuleBaseDir?: string, scopedServerModuleSources?: unknown[] }} input
3
3
  * @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
4
4
  */
5
- export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind, guardOnly }: {
5
+ export function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind, guardOnly, globalMiddlewareSource, globalMiddlewareSourcePath, scopedServerData, scopedServerModuleBaseDir, scopedServerModuleSources }: {
6
6
  source: string;
7
7
  sourcePath: string;
8
8
  params: Record<string, string>;
@@ -14,6 +14,11 @@ export function executeServerRoute({ source, sourcePath, params, requestUrl, req
14
14
  routeFile?: string;
15
15
  routeId?: string;
16
16
  routeKind?: "page" | "resource";
17
+ globalMiddlewareSource?: string;
18
+ globalMiddlewareSourcePath?: string;
19
+ scopedServerData?: unknown[];
20
+ scopedServerModuleBaseDir?: string;
21
+ scopedServerModuleSources?: unknown[];
17
22
  }): Promise<{
18
23
  result: {
19
24
  kind: string;
@@ -5,11 +5,12 @@ import { clientFacingRouteMessage, defaultRouteDenyMessage } from '../server-err
5
5
  import { SERVER_SCRIPT_RUNNER } from './server-script-runner-template.js';
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  /**
8
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource' }} input
8
+ * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl?: string, requestMethod?: string, requestHeaders?: Record<string, string | string[] | undefined>, requestBodyBuffer?: Buffer | null, routePattern?: string, routeFile?: string, routeId?: string, routeKind?: 'page' | 'resource', globalMiddlewareSource?: string, globalMiddlewareSourcePath?: string, scopedServerData?: unknown[], scopedServerModuleBaseDir?: string, scopedServerModuleSources?: unknown[] }} input
9
9
  * @returns {Promise<{ result: { kind: string, [key: string]: unknown }, trace: { guard: string, action: string, load: string }, status?: number, setCookies?: string[] }>}
10
10
  */
11
- export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false }) {
12
- if (!source || !String(source).trim()) {
11
+ export async function executeServerRoute({ source, sourcePath, params, requestUrl, requestMethod, requestHeaders, requestBodyBuffer, routePattern, routeFile, routeId, routeKind = 'page', guardOnly = false, globalMiddlewareSource = '', globalMiddlewareSourcePath = '', scopedServerData = [], scopedServerModuleBaseDir = '', scopedServerModuleSources = [] }) {
12
+ const hasScopedServerData = Array.isArray(scopedServerData) && scopedServerData.length > 0;
13
+ if ((!source || !String(source).trim()) && !hasScopedServerData) {
13
14
  return {
14
15
  result: { kind: 'data', data: {} },
15
16
  trace: { guard: 'none', action: 'none', load: 'none' }
@@ -27,7 +28,12 @@ export async function executeServerRoute({ source, sourcePath, params, requestUr
27
28
  routeFile: routeFile || sourcePath || '',
28
29
  routeId: routeId || routeIdFromSourcePath(sourcePath || ''),
29
30
  routeKind,
30
- guardOnly
31
+ guardOnly,
32
+ globalMiddlewareSource,
33
+ globalMiddlewareSourcePath,
34
+ scopedServerData,
35
+ scopedServerModuleBaseDir,
36
+ scopedServerModuleSources
31
37
  });
32
38
  if (payload === null || payload === undefined) {
33
39
  return {
@@ -110,7 +116,7 @@ export async function executeServerScript(input) {
110
116
  return {};
111
117
  }
112
118
  /**
113
- * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, requestBodyBuffer?: Buffer | null, routePattern: string, routeFile: string, routeId: string, routeKind?: 'page' | 'resource' }} input
119
+ * @param {{ source: string, sourcePath: string, params: Record<string, string>, requestUrl: string, requestMethod: string, requestHeaders: Record<string, string>, requestBodyBuffer?: Buffer | null, routePattern: string, routeFile: string, routeId: string, routeKind?: 'page' | 'resource', globalMiddlewareSource?: string, globalMiddlewareSourcePath?: string, scopedServerData?: unknown[], scopedServerModuleBaseDir?: string, scopedServerModuleSources?: unknown[] }} input
114
120
  * @returns {Promise<unknown>}
115
121
  */
116
122
  function spawnNodeServerRunner(input) {
@@ -130,7 +136,14 @@ function spawnNodeServerRunner(input) {
130
136
  ZENITH_SERVER_ROUTE_KIND: input.routeKind || 'page',
131
137
  ZENITH_SERVER_GUARD_ONLY: input.guardOnly ? '1' : '',
132
138
  ZENITH_SERVER_CONTRACT_PATH: join(__dirname, '..', 'server-contract.js'),
133
- ZENITH_SERVER_ROUTE_AUTH_PATH: join(__dirname, '..', 'auth', 'route-auth.js')
139
+ ZENITH_SERVER_ROUTE_AUTH_PATH: join(__dirname, '..', 'auth', 'route-auth.js'),
140
+ ZENITH_SERVER_MATCHED_ROUTE_PIPELINE_PATH: join(__dirname, '..', 'server-runtime', 'matched-route-pipeline.js'),
141
+ ZENITH_GLOBAL_MIDDLEWARE_SOURCE: input.globalMiddlewareSource || '',
142
+ ZENITH_GLOBAL_MIDDLEWARE_SOURCE_PATH: input.globalMiddlewareSourcePath || '',
143
+ ZENITH_SCOPED_SERVER_DATA: JSON.stringify(Array.isArray(input.scopedServerData) ? input.scopedServerData : []),
144
+ ZENITH_SCOPED_SERVER_MODULE_BASE_DIR: input.scopedServerModuleBaseDir || '',
145
+ ZENITH_SCOPED_SERVER_MODULE_SOURCES: JSON.stringify(Array.isArray(input.scopedServerModuleSources) ? input.scopedServerModuleSources : []),
146
+ ZENITH_SCOPED_SERVER_RUNTIME_PATH: join(__dirname, '..', 'scoped-server-data', 'runtime.js')
134
147
  },
135
148
  stdio: ['pipe', 'pipe', 'pipe']
136
149
  });