@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.
- package/README.md +14 -2
- package/dist/adapters/adapter-netlify-static.d.ts +2 -5
- package/dist/adapters/adapter-netlify.d.ts +2 -5
- package/dist/adapters/adapter-netlify.js +22 -5
- package/dist/adapters/adapter-types.d.ts +32 -13
- package/dist/adapters/adapter-types.js +0 -59
- package/dist/adapters/adapter-vercel-static.d.ts +2 -5
- package/dist/adapters/adapter-vercel.d.ts +2 -5
- package/dist/adapters/adapter-vercel.js +21 -6
- package/dist/adapters/copy-hosted-page-runtime.d.ts +2 -1
- package/dist/adapters/copy-hosted-page-runtime.js +68 -3
- package/dist/adapters/resolve-adapter.d.ts +6 -4
- package/dist/build/compiler-runtime.js +3 -0
- package/dist/build/expression-rewrites.d.ts +3 -1
- package/dist/build/expression-rewrites.js +14 -2
- package/dist/build/page-component-loop.d.ts +1 -0
- package/dist/build/page-component-loop.js +66 -6
- package/dist/build/page-ir-normalization.js +7 -0
- package/dist/build/page-loop-state.d.ts +2 -4
- package/dist/build/page-loop-state.js +17 -9
- package/dist/build/page-loop.js +18 -8
- package/dist/build/scoped-expression-context.d.ts +5 -0
- package/dist/build/scoped-expression-context.js +133 -0
- package/dist/build/server-script.js +13 -36
- package/dist/build/type-declarations.d.ts +2 -1
- package/dist/build/type-declarations.js +29 -52
- package/dist/build-output-manifest.d.ts +10 -6
- package/dist/build-output-manifest.js +4 -1
- package/dist/build.js +11 -2
- package/dist/component-instance-ir.js +1 -0
- package/dist/component-occurrences.d.ts +9 -0
- package/dist/component-occurrences.js +18 -0
- package/dist/config-plugins.d.ts +12 -0
- package/dist/config-plugins.js +100 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +56 -5
- package/dist/dev-build-session/helpers.js +27 -7
- package/dist/dev-build-session/session.js +19 -10
- package/dist/dev-server/build-error-response.d.ts +21 -0
- package/dist/dev-server/build-error-response.js +48 -0
- package/dist/dev-server/port-fallback.d.ts +15 -0
- package/dist/dev-server/port-fallback.js +61 -0
- package/dist/dev-server/request-handler.js +58 -5
- package/dist/dev-server/watcher.js +15 -0
- package/dist/dev-server.d.ts +5 -2
- package/dist/dev-server.js +129 -49
- package/dist/global-middleware-runtime-source.d.ts +15 -0
- package/dist/global-middleware-runtime-source.js +62 -0
- package/dist/global-middleware.d.ts +13 -0
- package/dist/global-middleware.js +252 -0
- package/dist/images/remote-fetch.d.ts +12 -0
- package/dist/images/remote-fetch.js +257 -0
- package/dist/images/service.d.ts +10 -0
- package/dist/images/service.js +9 -46
- package/dist/index.js +12 -2
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +70 -25
- package/dist/preview/request-handler.js +78 -5
- package/dist/preview/server-runner.d.ts +7 -2
- package/dist/preview/server-runner.js +19 -6
- package/dist/preview/server-script-runner-template.js +97 -29
- package/dist/resource-response.js +25 -8
- package/dist/resource-route-module.js +5 -22
- package/dist/route-classification.d.ts +11 -0
- package/dist/route-classification.js +21 -0
- package/dist/route-handler-export-analysis.d.ts +22 -0
- package/dist/route-handler-export-analysis.js +41 -0
- package/dist/scoped-server-data/analyze-owner-file.d.ts +3 -0
- package/dist/scoped-server-data/analyze-owner-file.js +149 -0
- package/dist/scoped-server-data/diagnostics.d.ts +18 -0
- package/dist/scoped-server-data/diagnostics.js +32 -0
- package/dist/scoped-server-data/lowering.d.ts +27 -0
- package/dist/scoped-server-data/lowering.js +242 -0
- package/dist/scoped-server-data/manifest-integration.d.ts +4 -0
- package/dist/scoped-server-data/manifest-integration.js +125 -0
- package/dist/scoped-server-data/owner-scanner.d.ts +6 -0
- package/dist/scoped-server-data/owner-scanner.js +55 -0
- package/dist/scoped-server-data/parse-owner-server-block.d.ts +12 -0
- package/dist/scoped-server-data/parse-owner-server-block.js +35 -0
- package/dist/scoped-server-data/runtime.d.ts +24 -0
- package/dist/scoped-server-data/runtime.js +121 -0
- package/dist/scoped-server-data/serialization-set.d.ts +2 -0
- package/dist/scoped-server-data/serialization-set.js +52 -0
- package/dist/scoped-server-data/static-props.d.ts +12 -0
- package/dist/scoped-server-data/static-props.js +307 -0
- package/dist/scoped-server-data/type-declarations.d.ts +10 -0
- package/dist/scoped-server-data/type-declarations.js +368 -0
- package/dist/scoped-server-data/types.d.ts +74 -0
- package/dist/scoped-server-data/types.js +1 -0
- package/dist/server-contract/auth-control-flow.d.ts +1 -0
- package/dist/server-contract/auth-control-flow.js +10 -0
- package/dist/server-contract/resolve.d.ts +19 -0
- package/dist/server-contract/resolve.js +85 -13
- package/dist/server-contract/resolved-envelope.d.ts +9 -0
- package/dist/server-contract/resolved-envelope.js +14 -0
- package/dist/server-contract/stage.js +1 -10
- package/dist/server-module-output.d.ts +9 -0
- package/dist/server-module-output.js +250 -0
- package/dist/server-output.d.ts +7 -1
- package/dist/server-output.js +144 -195
- package/dist/server-route-names.d.ts +2 -0
- package/dist/server-route-names.js +38 -0
- package/dist/server-runtime/matched-route-pipeline.d.ts +1 -0
- package/dist/server-runtime/matched-route-pipeline.js +1 -0
- package/dist/server-runtime/node-server.js +26 -3
- package/dist/server-runtime/route-render.d.ts +12 -3
- package/dist/server-runtime/route-render.js +67 -13
- package/dist/types/generate-env-dts.js +2 -44
- package/dist/types/zenith-env-dts.d.ts +4 -0
- package/dist/types/zenith-env-dts.js +96 -0
- package/package.json +3 -6
package/dist/manifest.js
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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';
|
|
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
|
+
}
|
|
24
39
|
/**
|
|
25
40
|
* @typedef {{
|
|
26
41
|
* path: string,
|
|
@@ -34,6 +49,8 @@ import { validateStaticExportPaths } from './static-export-paths.js';
|
|
|
34
49
|
* has_guard?: boolean,
|
|
35
50
|
* has_load?: boolean,
|
|
36
51
|
* has_action?: boolean,
|
|
52
|
+
* has_scoped_server_data?: boolean,
|
|
53
|
+
* scoped_server_data?: import('./scoped-server-data/types.js').ManifestScopedServerDataEntry[],
|
|
37
54
|
* export_paths?: string[]
|
|
38
55
|
* }} ManifestEntry
|
|
39
56
|
*/
|
|
@@ -42,11 +59,15 @@ import { validateStaticExportPaths } from './static-export-paths.js';
|
|
|
42
59
|
*
|
|
43
60
|
* @param {string} pagesDir - Absolute path to /pages directory
|
|
44
61
|
* @param {string} [extension='.zen'] - File extension to scan for
|
|
45
|
-
* @param {{ compilerOpts?: object }} [options]
|
|
62
|
+
* @param {{ compilerOpts?: object, srcDir?: string, registry?: Map<string, string> }} [options]
|
|
46
63
|
* @returns {Promise<ManifestEntry[]>}
|
|
47
64
|
*/
|
|
48
65
|
export async function generateManifest(pagesDir, extension = '.zen', options = {}) {
|
|
49
|
-
const
|
|
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);
|
|
50
71
|
const apiAliasState = _resolveSrcApiAliasState(pagesDir);
|
|
51
72
|
if (apiAliasState) {
|
|
52
73
|
const aliasEntries = await _scanResourceDir(apiAliasState.aliasDir, apiAliasState.srcDir);
|
|
@@ -68,7 +89,7 @@ export async function generateManifest(pagesDir, extension = '.zen', options = {
|
|
|
68
89
|
* @param {string} ext - Extension to match
|
|
69
90
|
* @returns {Promise<ManifestEntry[]>}
|
|
70
91
|
*/
|
|
71
|
-
async function _scanDir(dir, root, ext,
|
|
92
|
+
async function _scanDir(dir, root, ext, scanContext) {
|
|
72
93
|
/** @type {ManifestEntry[]} */
|
|
73
94
|
const entries = [];
|
|
74
95
|
let items;
|
|
@@ -84,7 +105,7 @@ async function _scanDir(dir, root, ext, compilerOpts) {
|
|
|
84
105
|
const fullPath = join(dir, item);
|
|
85
106
|
const info = await stat(fullPath);
|
|
86
107
|
if (info.isDirectory()) {
|
|
87
|
-
const nested = await _scanDir(fullPath, root, ext,
|
|
108
|
+
const nested = await _scanDir(fullPath, root, ext, scanContext);
|
|
88
109
|
entries.push(...nested);
|
|
89
110
|
}
|
|
90
111
|
else if (item.endsWith(ext)) {
|
|
@@ -93,7 +114,7 @@ async function _scanDir(dir, root, ext, compilerOpts) {
|
|
|
93
114
|
fullPath,
|
|
94
115
|
root,
|
|
95
116
|
routePath,
|
|
96
|
-
|
|
117
|
+
scanContext
|
|
97
118
|
}));
|
|
98
119
|
}
|
|
99
120
|
else if (isResourceRouteFile(item)) {
|
|
@@ -142,7 +163,8 @@ function _resolveSrcApiAliasState(pagesDir) {
|
|
|
142
163
|
srcDir
|
|
143
164
|
};
|
|
144
165
|
}
|
|
145
|
-
function buildPageManifestEntry({ fullPath, root, routePath,
|
|
166
|
+
function buildPageManifestEntry({ fullPath, root, routePath, scanContext }) {
|
|
167
|
+
const { srcDir, registry, compilerOpts } = scanContext;
|
|
146
168
|
const rawSource = readFileSync(fullPath, 'utf8');
|
|
147
169
|
const inlineServerScript = extractServerScript(rawSource, fullPath, compilerOpts).serverScript;
|
|
148
170
|
const { guardPath, loadPath, actionPath } = resolveAdjacentServerModules(fullPath);
|
|
@@ -153,16 +175,39 @@ function buildPageManifestEntry({ fullPath, root, routePath, compilerOpts }) {
|
|
|
153
175
|
adjacentLoadPath: loadPath,
|
|
154
176
|
adjacentActionPath: actionPath
|
|
155
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);
|
|
156
187
|
const exportPaths = Array.isArray(composed.serverScript?.export_paths)
|
|
157
188
|
? validateStaticExportPaths(routePath, composed.serverScript.export_paths, fullPath)
|
|
158
189
|
: [];
|
|
190
|
+
const classification = classifyPageRoute({
|
|
191
|
+
file: manifestFile,
|
|
192
|
+
serverScript: composed.serverScript,
|
|
193
|
+
hasScopedServerData: scopedMetadata.hasScopedServerData
|
|
194
|
+
});
|
|
159
195
|
return {
|
|
160
196
|
path: routePath,
|
|
161
|
-
file:
|
|
197
|
+
file: manifestFile,
|
|
162
198
|
route_kind: 'page',
|
|
163
199
|
path_kind: _isDynamic(routePath) ? 'dynamic' : 'static',
|
|
164
|
-
render_mode:
|
|
200
|
+
render_mode: classification.renderMode,
|
|
165
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
|
+
: {}),
|
|
166
211
|
...(exportPaths.length > 0 ? { export_paths: exportPaths } : {})
|
|
167
212
|
};
|
|
168
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
});
|
|
@@ -15,11 +15,11 @@ const routeFile = process.env.ZENITH_SERVER_ROUTE_FILE || sourcePath || '';
|
|
|
15
15
|
const routeId = process.env.ZENITH_SERVER_ROUTE_ID || routePattern || '';
|
|
16
16
|
const routeKind = process.env.ZENITH_SERVER_ROUTE_KIND || 'page';
|
|
17
17
|
const guardOnly = process.env.ZENITH_SERVER_GUARD_ONLY === '1';
|
|
18
|
+
const globalMiddlewareSource = process.env.ZENITH_GLOBAL_MIDDLEWARE_SOURCE || '';
|
|
19
|
+
const globalMiddlewareSourcePath = process.env.ZENITH_GLOBAL_MIDDLEWARE_SOURCE_PATH || '';
|
|
20
|
+
const scopedServerData = JSON.parse(process.env.ZENITH_SCOPED_SERVER_DATA || '[]'), scopedServerModuleBaseDir = process.env.ZENITH_SCOPED_SERVER_MODULE_BASE_DIR || '', scopedServerModuleSources = JSON.parse(process.env.ZENITH_SCOPED_SERVER_MODULE_SOURCES || '[]');
|
|
18
21
|
|
|
19
|
-
if (!source.trim()) {
|
|
20
|
-
process.stdout.write('null');
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
22
|
+
if (!source.trim() && !(Array.isArray(scopedServerData) && scopedServerData.length > 0)) { process.stdout.write('null'); process.exit(0); }
|
|
23
23
|
|
|
24
24
|
let cachedTypeScript = undefined;
|
|
25
25
|
async function loadTypeScript() {
|
|
@@ -332,6 +332,92 @@ async function linkModule(specifier, parentIdentifier) {
|
|
|
332
332
|
return loadFileModule(resolvedUrl);
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
function configuredFrameworkUrl(specifier) {
|
|
336
|
+
if (specifier === 'zenith:server-contract') {
|
|
337
|
+
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
|
|
338
|
+
const configuredPath = process.env.ZENITH_SERVER_CONTRACT_PATH || '';
|
|
339
|
+
return {
|
|
340
|
+
url: pathToFileURL(configuredPath || defaultPath).href,
|
|
341
|
+
fallbackUrl: pathToFileURL(defaultPath).href,
|
|
342
|
+
hasConfiguredPath: Boolean(configuredPath)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (specifier === 'zenith:route-auth') {
|
|
346
|
+
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'auth', 'route-auth.js');
|
|
347
|
+
const configuredPath = process.env.ZENITH_SERVER_ROUTE_AUTH_PATH || '';
|
|
348
|
+
return {
|
|
349
|
+
url: pathToFileURL(configuredPath || defaultPath).href,
|
|
350
|
+
fallbackUrl: pathToFileURL(defaultPath).href,
|
|
351
|
+
hasConfiguredPath: Boolean(configuredPath)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (specifier === 'zenith:scoped-server-data-runtime') { const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'scoped-server-data', 'runtime.js'); const configuredPath = process.env.ZENITH_SCOPED_SERVER_RUNTIME_PATH || ''; return { url: pathToFileURL(configuredPath || defaultPath).href, fallbackUrl: pathToFileURL(defaultPath).href, hasConfiguredPath: Boolean(configuredPath) }; }
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function linkEntryModule(specifier, referencingModule) {
|
|
359
|
+
const frameworkUrl = configuredFrameworkUrl(specifier);
|
|
360
|
+
if (frameworkUrl) {
|
|
361
|
+
if (frameworkUrl.hasConfiguredPath) {
|
|
362
|
+
return loadFileModule(frameworkUrl.url);
|
|
363
|
+
}
|
|
364
|
+
return loadFileModule(frameworkUrl.url).catch(() =>
|
|
365
|
+
loadFileModule(frameworkUrl.fallbackUrl)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return linkModule(specifier, referencingModule.identifier);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function loadMatchedRoutePipeline() {
|
|
372
|
+
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-runtime', 'matched-route-pipeline.js');
|
|
373
|
+
const configuredPath = process.env.ZENITH_SERVER_MATCHED_ROUTE_PIPELINE_PATH || '';
|
|
374
|
+
const pipelineUrl = pathToFileURL(configuredPath || defaultPath).href;
|
|
375
|
+
const module = await loadFileModule(pipelineUrl).catch((error) => {
|
|
376
|
+
if (configuredPath) {
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
return loadFileModule(pathToFileURL(defaultPath).href);
|
|
380
|
+
});
|
|
381
|
+
await module.evaluate();
|
|
382
|
+
if (typeof module.namespace.executeMatchedRoutePipeline !== 'function') {
|
|
383
|
+
throw new Error('[zenith-preview] matched route pipeline unavailable');
|
|
384
|
+
}
|
|
385
|
+
return module.namespace.executeMatchedRoutePipeline;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function loadGlobalMiddleware() {
|
|
389
|
+
if (!globalMiddlewareSource.trim()) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const identifier = globalMiddlewareSourcePath
|
|
393
|
+
? pathToFileURL(globalMiddlewareSourcePath).href
|
|
394
|
+
: 'zenith:global-middleware';
|
|
395
|
+
const filename = globalMiddlewareSourcePath || 'global-middleware.ts';
|
|
396
|
+
const code = await transpileIfNeeded(filename, globalMiddlewareSource);
|
|
397
|
+
const module = new vm.SourceTextModule(code, {
|
|
398
|
+
context,
|
|
399
|
+
identifier,
|
|
400
|
+
initializeImportMeta(meta) {
|
|
401
|
+
meta.url = identifier;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
moduleCache.set(identifier, module);
|
|
405
|
+
await module.link(linkEntryModule);
|
|
406
|
+
await module.evaluate();
|
|
407
|
+
const middlewareFn = module.namespace.default;
|
|
408
|
+
if (typeof middlewareFn !== 'function') {
|
|
409
|
+
throw new Error('[Zenith:Middleware] Global middleware module must default export a function.');
|
|
410
|
+
}
|
|
411
|
+
return middlewareFn;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function loadScopedServerRuntime() { const module = await linkEntryModule('zenith:scoped-server-data-runtime', null); await module.evaluate(); if (typeof module.namespace.executeScopedServerData !== 'function' || typeof module.namespace.mergeScopedSsrPayload !== 'function') throw new Error('[zenith-preview] scoped server data runtime unavailable'); return module.namespace; }
|
|
415
|
+
async function loadScopedSourceModule(entry) { const modulePath = String(entry && entry.module || ''); const found = Array.isArray(scopedServerModuleSources) ? scopedServerModuleSources.find((item) => item && item.module === modulePath) : null; if (!found || typeof found.source !== 'string') throw new Error('[Zenith:ScopedServerData] Missing scoped server data source for "' + modulePath + '".'); const sourcePath = typeof found.sourcePath === 'string' && found.sourcePath.length > 0 ? found.sourcePath : modulePath; const identifier = sourcePath.startsWith('file:') ? sourcePath : pathToFileURL(sourcePath).href; const filename = sourcePath.toLowerCase().endsWith('.zen') ? sourcePath.replace(/\.zen$/i, '.ts') : sourcePath; const code = await transpileIfNeeded(filename, found.source); const module = new vm.SourceTextModule(code, { context, identifier, initializeImportMeta(meta) { meta.url = identifier; } }); moduleCache.set(identifier, module); await module.link(linkEntryModule); await module.evaluate(); return module.namespace; }
|
|
416
|
+
function invalidScopedModulePath() { throw new Error('[Zenith:ScopedServerData] Invalid scoped server data module path.'); }
|
|
417
|
+
function resolveScopedPackagedModulePath(entry) { const modulePath = String(entry && entry.module || ''); if (!modulePath || path.isAbsolute(modulePath) || /^[A-Za-z]:[\\/]/.test(modulePath)) invalidScopedModulePath(); const normalized = modulePath.replace(/\\/g, '/'); if (!normalized.startsWith('scoped/') || normalized.split('/').some((part) => part === '..' || part === '.')) invalidScopedModulePath(); const scopedRoot = path.resolve(scopedServerModuleBaseDir, 'scoped'); const candidate = path.resolve(scopedServerModuleBaseDir, normalized); if (candidate !== scopedRoot && !candidate.startsWith(scopedRoot + path.sep)) invalidScopedModulePath(); return candidate; }
|
|
418
|
+
async function loadScopedPackagedModule(entry) { if (!scopedServerModuleBaseDir) throw new Error('[Zenith:ScopedServerData] Cannot execute scoped server data without a server module root.'); const module = await loadFileModule(pathToFileURL(resolveScopedPackagedModulePath(entry)).href); await module.evaluate(); return module.namespace; }
|
|
419
|
+
async function executeScopedServerDataAfterRoute(resolved) { if (guardOnly || routeKind === 'resource' || !Array.isArray(scopedServerData) || scopedServerData.length === 0 || !resolved || !resolved.result || resolved.result.kind !== 'data') return resolved; const runtime = await loadScopedServerRuntime(); const scoped = await runtime.executeScopedServerData({ route: { route_kind: routeKind, prerender: false, has_scoped_server_data: true, scoped_server_data: scopedServerData }, ctx: context.ctx, loadModule: (entry) => Array.isArray(scopedServerModuleSources) && scopedServerModuleSources.length > 0 ? loadScopedSourceModule(entry) : loadScopedPackagedModule(entry) }); return { ...resolved, result: { ...resolved.result, data: runtime.mergeScopedSsrPayload(resolved.result.data, scoped) } }; }
|
|
420
|
+
|
|
335
421
|
const allowed = new Set(['data', 'load', 'guard', 'action', 'ssr_data', 'props', 'ssr', 'prerender', 'exportPaths']);
|
|
336
422
|
const prelude = "const params = globalThis.params;\n" +
|
|
337
423
|
"const ctx = globalThis.ctx;\n" +
|
|
@@ -358,29 +444,7 @@ const entryModule = new vm.SourceTextModule(entryCode, {
|
|
|
358
444
|
|
|
359
445
|
moduleCache.set(entryIdentifier, entryModule);
|
|
360
446
|
await entryModule.link((specifier, referencingModule) => {
|
|
361
|
-
|
|
362
|
-
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'server-contract.js');
|
|
363
|
-
const configuredPath = process.env.ZENITH_SERVER_CONTRACT_PATH || '';
|
|
364
|
-
const contractUrl = pathToFileURL(configuredPath || defaultPath).href;
|
|
365
|
-
if (configuredPath) {
|
|
366
|
-
return loadFileModule(contractUrl);
|
|
367
|
-
}
|
|
368
|
-
return loadFileModule(contractUrl).catch(() =>
|
|
369
|
-
loadFileModule(pathToFileURL(defaultPath).href)
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
if (specifier === 'zenith:route-auth') {
|
|
373
|
-
const defaultPath = path.join(process.cwd(), 'node_modules', '@zenithbuild', 'cli', 'src', 'auth', 'route-auth.js');
|
|
374
|
-
const configuredPath = process.env.ZENITH_SERVER_ROUTE_AUTH_PATH || '';
|
|
375
|
-
const authUrl = pathToFileURL(configuredPath || defaultPath).href;
|
|
376
|
-
if (configuredPath) {
|
|
377
|
-
return loadFileModule(authUrl);
|
|
378
|
-
}
|
|
379
|
-
return loadFileModule(authUrl).catch(() =>
|
|
380
|
-
loadFileModule(pathToFileURL(defaultPath).href)
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
return linkModule(specifier, referencingModule.identifier);
|
|
447
|
+
return linkEntryModule(specifier, referencingModule);
|
|
384
448
|
});
|
|
385
449
|
await entryModule.evaluate();
|
|
386
450
|
context.attachRouteAuth(routeContext, {
|
|
@@ -399,13 +463,17 @@ for (const key of namespaceKeys) {
|
|
|
399
463
|
|
|
400
464
|
const exported = entryModule.namespace;
|
|
401
465
|
try {
|
|
402
|
-
const
|
|
466
|
+
const executeMatchedRoutePipeline = await loadMatchedRoutePipeline();
|
|
467
|
+
const globalMiddleware = await loadGlobalMiddleware();
|
|
468
|
+
let resolved = await executeMatchedRoutePipeline({
|
|
403
469
|
exports: exported,
|
|
404
470
|
ctx: context.ctx,
|
|
405
471
|
filePath: sourcePath || 'server_script',
|
|
406
472
|
guardOnly: guardOnly,
|
|
407
|
-
routeKind: routeKind
|
|
473
|
+
routeKind: routeKind,
|
|
474
|
+
globalMiddleware
|
|
408
475
|
});
|
|
476
|
+
resolved = await executeScopedServerDataAfterRoute(resolved);
|
|
409
477
|
|
|
410
478
|
process.stdout.write(JSON.stringify(resolved || null));
|
|
411
479
|
} catch (error) {
|
|
@@ -28,6 +28,24 @@ function createReadableStreamFromAsyncIterable(iterable) {
|
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
const SSE_METADATA_CONTROL_RE = /[\x00-\x1F\x7F]/u;
|
|
32
|
+
function serializeSseMetadata(value, field) {
|
|
33
|
+
const serialized = String(value);
|
|
34
|
+
if (SSE_METADATA_CONTROL_RE.test(serialized)) {
|
|
35
|
+
throw new Error(`[Zenith] sse ${field} metadata must be a single line without control characters.`);
|
|
36
|
+
}
|
|
37
|
+
return serialized;
|
|
38
|
+
}
|
|
39
|
+
function serializeSseRetry(value) {
|
|
40
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
41
|
+
throw new Error('[Zenith] sse retry metadata must be a non-negative safe integer.');
|
|
42
|
+
}
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
function serializeSseData(value) {
|
|
46
|
+
const raw = typeof value === 'string' ? value : JSON.stringify(value);
|
|
47
|
+
return String(raw ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
48
|
+
}
|
|
31
49
|
function createSseStream(events) {
|
|
32
50
|
const iterator = events[Symbol.asyncIterator]();
|
|
33
51
|
const encoder = new TextEncoder();
|
|
@@ -40,14 +58,13 @@ function createSseStream(events) {
|
|
|
40
58
|
return;
|
|
41
59
|
}
|
|
42
60
|
let chunk = '';
|
|
43
|
-
if (value.event)
|
|
44
|
-
chunk += `event: ${value.event}\n`;
|
|
45
|
-
if (value.id)
|
|
46
|
-
chunk += `id: ${value.id}\n`;
|
|
47
|
-
if (value.retry)
|
|
48
|
-
chunk += `retry: ${value.retry}\n`;
|
|
49
|
-
const
|
|
50
|
-
const lines = data.split('\n');
|
|
61
|
+
if (value.event !== undefined)
|
|
62
|
+
chunk += `event: ${serializeSseMetadata(value.event, 'event')}\n`;
|
|
63
|
+
if (value.id !== undefined)
|
|
64
|
+
chunk += `id: ${serializeSseMetadata(value.id, 'id')}\n`;
|
|
65
|
+
if (value.retry !== undefined)
|
|
66
|
+
chunk += `retry: ${serializeSseRetry(value.retry)}\n`;
|
|
67
|
+
const lines = serializeSseData(value.data).split('\n');
|
|
51
68
|
for (const line of lines) {
|
|
52
69
|
chunk += `data: ${line}\n`;
|
|
53
70
|
}
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { relative, sep } from 'node:path';
|
|
3
|
+
import { readRouteHandlerExport } from './route-handler-export-analysis.js';
|
|
3
4
|
const RESOURCE_EXTENSIONS = ['.resource.ts', '.resource.js', '.resource.mts', '.resource.cts', '.resource.mjs', '.resource.cjs'];
|
|
4
5
|
const FORBIDDEN_RESOURCE_EXPORT_RE = /\bexport\s+const\s+(?:data|prerender|exportPaths|ssr_data|props|ssr)\b/;
|
|
5
|
-
function readSingleExport(source, name) {
|
|
6
|
-
const fnMatch = source.match(new RegExp(`\\bexport\\s+(?:async\\s+)?function\\s+${name}\\s*\\(([^)]*)\\)`));
|
|
7
|
-
const constParenMatch = source.match(new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*(?:async\\s*)?\\(([^)]*)\\)\\s*=>`));
|
|
8
|
-
const constSingleArgMatch = source.match(new RegExp(`\\bexport\\s+const\\s+${name}\\s*=\\s*(?:async\\s*)?([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=>`));
|
|
9
|
-
const matchCount = Number(Boolean(fnMatch)) +
|
|
10
|
-
Number(Boolean(constParenMatch)) +
|
|
11
|
-
Number(Boolean(constSingleArgMatch));
|
|
12
|
-
return {
|
|
13
|
-
fnMatch,
|
|
14
|
-
constParenMatch,
|
|
15
|
-
constSingleArgMatch,
|
|
16
|
-
hasExport: matchCount > 0,
|
|
17
|
-
matchCount
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
6
|
function assertSingleCtxArg(sourceFile, name, exportMatch) {
|
|
21
|
-
|
|
22
|
-
const paramsText = String((exportMatch.fnMatch || exportMatch.constParenMatch)?.[1] || '').trim();
|
|
23
|
-
const arity = singleArg ? 1 : paramsText.length === 0 ? 0 : paramsText.split(',').length;
|
|
24
|
-
if (arity !== 1) {
|
|
7
|
+
if (exportMatch.arity !== null && exportMatch.arity !== 1) {
|
|
25
8
|
throw new Error(`Zenith resource route contract violation:\n` +
|
|
26
9
|
` File: ${sourceFile}\n` +
|
|
27
10
|
` Reason: ${name}(ctx) must accept exactly one argument\n` +
|
|
@@ -80,9 +63,9 @@ export function analyzeResourceRouteModule(fullPath, root) {
|
|
|
80
63
|
` Reason: resource routes may only export guard(ctx), load(ctx), and action(ctx)\n` +
|
|
81
64
|
` Example: remove page-only exports such as data/prerender/exportPaths`);
|
|
82
65
|
}
|
|
83
|
-
const guardExport =
|
|
84
|
-
const loadExport =
|
|
85
|
-
const actionExport =
|
|
66
|
+
const guardExport = readRouteHandlerExport(source, 'guard');
|
|
67
|
+
const loadExport = readRouteHandlerExport(source, 'load');
|
|
68
|
+
const actionExport = readRouteHandlerExport(source, 'action');
|
|
86
69
|
for (const [name, exportMatch] of [
|
|
87
70
|
['guard', guardExport],
|
|
88
71
|
['load', loadExport],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function classifyPageRoute({ file, serverScript, hasScopedServerData }: {
|
|
2
|
+
file: any;
|
|
3
|
+
serverScript: any;
|
|
4
|
+
hasScopedServerData?: boolean | undefined;
|
|
5
|
+
}): {
|
|
6
|
+
prerender: boolean;
|
|
7
|
+
renderMode: string;
|
|
8
|
+
hasGuard: boolean;
|
|
9
|
+
hasLoad: boolean;
|
|
10
|
+
hasAction: boolean;
|
|
11
|
+
};
|