veryfront 0.1.73 → 0.1.75
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/esm/cli/commands/knowledge/command-help.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command-help.js +3 -1
- package/esm/cli/commands/knowledge/command.d.ts +34 -5
- package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command.js +151 -22
- package/esm/cli/commands/knowledge/parser-source.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/parser-source.js +110 -5
- package/esm/deno.d.ts +2 -0
- package/esm/deno.js +3 -1
- package/esm/src/data/data-fetcher.d.ts +11 -1
- package/esm/src/data/data-fetcher.d.ts.map +1 -1
- package/esm/src/data/data-fetcher.js +5 -2
- package/esm/src/data/index.d.ts +1 -1
- package/esm/src/data/index.d.ts.map +1 -1
- package/esm/src/data/server-data-fetcher.d.ts +14 -1
- package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
- package/esm/src/data/server-data-fetcher.js +49 -3
- package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
- package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
- package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +6 -1
- package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
- package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
- package/esm/src/routing/api/handler.d.ts.map +1 -1
- package/esm/src/routing/api/handler.js +6 -2
- package/esm/src/routing/api/route-executor.d.ts +8 -2
- package/esm/src/routing/api/route-executor.d.ts.map +1 -1
- package/esm/src/routing/api/route-executor.js +131 -3
- package/esm/src/security/deno-permissions.d.ts +6 -0
- package/esm/src/security/deno-permissions.d.ts.map +1 -1
- package/esm/src/security/deno-permissions.js +10 -0
- package/esm/src/security/sandbox/project-worker.d.ts +61 -0
- package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
- package/esm/src/security/sandbox/project-worker.js +318 -0
- package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
- package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
- package/esm/src/security/sandbox/worker-permissions.js +60 -0
- package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
- package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
- package/esm/src/security/sandbox/worker-pool.js +356 -0
- package/esm/src/security/sandbox/worker-types.d.ts +165 -0
- package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
- package/esm/src/security/sandbox/worker-types.js +17 -0
- package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts +2 -0
- package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/ssr/ssr.handler.js +6 -2
- package/esm/src/server/project-env/storage.d.ts +6 -0
- package/esm/src/server/project-env/storage.d.ts.map +1 -1
- package/esm/src/server/project-env/storage.js +8 -0
- package/esm/src/server/runtime-handler/adapter-factory.d.ts +3 -0
- package/esm/src/server/runtime-handler/adapter-factory.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/adapter-factory.js +6 -5
- package/esm/src/server/runtime-handler/index.d.ts +33 -0
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +103 -37
- package/esm/src/server/runtime-handler/local-project-discovery.d.ts +32 -4
- package/esm/src/server/runtime-handler/local-project-discovery.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/local-project-discovery.js +46 -16
- package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
- package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/project-isolation.js +44 -0
- package/esm/src/server/services/rendering/ssr.service.d.ts +19 -1
- package/esm/src/server/services/rendering/ssr.service.d.ts.map +1 -1
- package/esm/src/server/services/rendering/ssr.service.js +9 -1
- package/esm/src/server/shared/renderer/adapter.d.ts +25 -0
- package/esm/src/server/shared/renderer/adapter.d.ts.map +1 -1
- package/esm/src/server/shared/renderer/adapter.js +83 -10
- package/esm/src/server/shared/renderer/index.d.ts +1 -1
- package/esm/src/server/shared/renderer/index.d.ts.map +1 -1
- package/esm/src/server/shared/renderer/index.js +1 -1
- package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
- package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
- package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
- package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
- package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
- package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
- package/esm/src/utils/index.d.ts +10 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +9 -1
- package/esm/src/utils/logger/index.d.ts +1 -1
- package/esm/src/utils/logger/index.d.ts.map +1 -1
- package/esm/src/utils/logger/index.js +1 -1
- package/esm/src/utils/logger/logger.d.ts +14 -0
- package/esm/src/utils/logger/logger.d.ts.map +1 -1
- package/esm/src/utils/logger/logger.js +17 -0
- package/esm/src/workflow/claude-code/tool.d.ts +5 -5
- package/package.json +4 -1
- package/src/cli/commands/knowledge/command-help.ts +3 -1
- package/src/cli/commands/knowledge/command.ts +180 -22
- package/src/cli/commands/knowledge/parser-source.ts +110 -5
- package/src/deno.js +3 -1
- package/src/src/data/data-fetcher.ts +18 -2
- package/src/src/data/index.ts +1 -1
- package/src/src/data/server-data-fetcher.ts +78 -3
- package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
- package/src/src/rendering/orchestrator/pipeline.ts +7 -2
- package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
- package/src/src/routing/api/handler.ts +16 -3
- package/src/src/routing/api/route-executor.ts +222 -1
- package/src/src/security/deno-permissions.ts +11 -0
- package/src/src/security/sandbox/project-worker.ts +416 -0
- package/src/src/security/sandbox/worker-permissions.ts +74 -0
- package/src/src/security/sandbox/worker-pool.ts +451 -0
- package/src/src/security/sandbox/worker-types.ts +209 -0
- package/src/src/server/handlers/request/ssr/ssr.handler.ts +11 -2
- package/src/src/server/project-env/storage.ts +9 -0
- package/src/src/server/runtime-handler/adapter-factory.ts +13 -5
- package/src/src/server/runtime-handler/index.ts +132 -39
- package/src/src/server/runtime-handler/local-project-discovery.ts +51 -17
- package/src/src/server/runtime-handler/project-isolation.ts +53 -0
- package/src/src/server/services/rendering/ssr.service.ts +34 -3
- package/src/src/server/shared/renderer/adapter.ts +107 -8
- package/src/src/server/shared/renderer/index.ts +7 -1
- package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
- package/src/src/utils/index.ts +11 -0
- package/src/src/utils/logger/index.ts +1 -0
- package/src/src/utils/logger/logger.ts +34 -0
|
@@ -38,6 +38,8 @@ export interface LifecycleOptions {
|
|
|
38
38
|
projectId?: string;
|
|
39
39
|
/** Content source identifier for cache isolation (branch or release) */
|
|
40
40
|
contentSourceId?: string;
|
|
41
|
+
/** Injectable factory for testing — bypasses real service construction */
|
|
42
|
+
servicesFactory?: (adapter: RuntimeAdapter) => RendererServices;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface RendererServices {
|
|
@@ -62,6 +64,7 @@ export class RendererLifecycle {
|
|
|
62
64
|
private contentSourceId?: string;
|
|
63
65
|
private services?: RendererServices;
|
|
64
66
|
private adapter!: RuntimeAdapter;
|
|
67
|
+
private servicesFactory?: (adapter: RuntimeAdapter) => RendererServices;
|
|
65
68
|
|
|
66
69
|
constructor(options: LifecycleOptions) {
|
|
67
70
|
this.configManager = options.configManager;
|
|
@@ -69,6 +72,7 @@ export class RendererLifecycle {
|
|
|
69
72
|
this.moduleServerUrl = options.moduleServerUrl;
|
|
70
73
|
this.projectId = options.projectId;
|
|
71
74
|
this.contentSourceId = options.contentSourceId;
|
|
75
|
+
this.servicesFactory = options.servicesFactory;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
async initialize(): Promise<RendererServices> {
|
|
@@ -83,6 +87,13 @@ export class RendererLifecycle {
|
|
|
83
87
|
this.adapter = await runtime.get();
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// Allow tests to bypass the full service graph construction
|
|
91
|
+
if (this.servicesFactory) {
|
|
92
|
+
this.services = this.servicesFactory(this.adapter);
|
|
93
|
+
logger.debug("Renderer services initialized via injected factory");
|
|
94
|
+
return this.services;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
const projectDir = this.configManager.getProjectDir();
|
|
87
98
|
const mode = this.configManager.getMode();
|
|
88
99
|
const debugMode = this.configManager.isDebugMode();
|
|
@@ -36,7 +36,7 @@ import type { PageResolver } from "../page-resolution/index.js";
|
|
|
36
36
|
import type { LayoutOrchestrator } from "./layout.js";
|
|
37
37
|
import type { SSROrchestrator } from "./ssr-orchestrator.js";
|
|
38
38
|
import type { PageDataResponse, RenderOptions, RenderResult } from "./types.js";
|
|
39
|
-
import { DataFetcher } from "../../data/index.js";
|
|
39
|
+
import { DataFetcher, type FetchDataOptions } from "../../data/index.js";
|
|
40
40
|
import type { DataContext, PageWithData } from "../../data/types.js";
|
|
41
41
|
import { clearSSRModuleCacheForProject } from "../../modules/react-loader/index.js";
|
|
42
42
|
import { setupSSRGlobals } from "../ssr-globals.js";
|
|
@@ -303,8 +303,13 @@ export class RenderPipeline {
|
|
|
303
303
|
Promise.all(
|
|
304
304
|
dataJobs.map(async (job) => {
|
|
305
305
|
try {
|
|
306
|
+
const jobPath = (job as LoadedModule & { path?: string }).path;
|
|
307
|
+
const fetchOptions: FetchDataOptions = {
|
|
308
|
+
modulePath: jobPath,
|
|
309
|
+
projectDir: this.config.projectDir,
|
|
310
|
+
};
|
|
306
311
|
const result = await this.dataFetcher
|
|
307
|
-
.fetchData(job.mod as PageWithData, dataContext, this.config.mode);
|
|
312
|
+
.fetchData(job.mod as PageWithData, dataContext, this.config.mode, fetchOptions);
|
|
308
313
|
return { ...job, result, error: null as Error | null };
|
|
309
314
|
} catch (error) {
|
|
310
315
|
return { ...job, result: null, error: error as Error };
|
|
@@ -10,6 +10,8 @@ import { computeHash } from "../utils/index.js";
|
|
|
10
10
|
import type { HTMLGenerationContext, HTMLGenerator } from "./html.js";
|
|
11
11
|
import type { RenderOptions } from "./types.js";
|
|
12
12
|
import { runWithHeadCollector } from "../../react/head-collector.js";
|
|
13
|
+
import { getWorkerPool, isSSRIsolationEnabled } from "../../security/sandbox/worker-pool.js";
|
|
14
|
+
import type { WorkerResponse } from "../../security/sandbox/worker-types.js";
|
|
13
15
|
|
|
14
16
|
const logger = rendererLogger.component("ssr-orchestrator");
|
|
15
17
|
|
|
@@ -27,6 +29,24 @@ export interface SSRRenderingResult {
|
|
|
27
29
|
ssrHash: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Options for isolated SSR rendering through the Worker pool.
|
|
34
|
+
* When provided and SSR isolation is enabled, the rendering happens
|
|
35
|
+
* in a per-project Worker instead of the main process.
|
|
36
|
+
*/
|
|
37
|
+
export interface SSRIsolationOptions {
|
|
38
|
+
/** Temp file path for the page component module */
|
|
39
|
+
pageModulePath: string;
|
|
40
|
+
/** Ordered layout module temp paths (innermost → outermost) */
|
|
41
|
+
layoutModulePaths: string[];
|
|
42
|
+
/** Page component props */
|
|
43
|
+
pageProps: Record<string, unknown>;
|
|
44
|
+
/** Layout props (one entry per layout, matching layoutModulePaths order) */
|
|
45
|
+
layoutProps: Record<string, unknown>[];
|
|
46
|
+
/** Project directory for worker scoping */
|
|
47
|
+
projectDir: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
function getElementTypeName(el: React.ReactElement | null | undefined): string {
|
|
31
51
|
if (!el?.type) return "unknown";
|
|
32
52
|
if (typeof el.type === "string") return el.type;
|
|
@@ -46,7 +66,18 @@ export class SSROrchestrator {
|
|
|
46
66
|
pageElement: React.ReactElement,
|
|
47
67
|
generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
|
|
48
68
|
options?: RenderOptions,
|
|
69
|
+
isolationOptions?: SSRIsolationOptions,
|
|
49
70
|
): Promise<SSRRenderingResult> {
|
|
71
|
+
// Isolated SSR path: render in per-project Worker
|
|
72
|
+
if (
|
|
73
|
+
isSSRIsolationEnabled() &&
|
|
74
|
+
isolationOptions?.pageModulePath &&
|
|
75
|
+
isolationOptions?.projectDir
|
|
76
|
+
) {
|
|
77
|
+
return this.performIsolatedSSR(generationContext, options, isolationOptions);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Default path: render in main process
|
|
50
81
|
logger.debug("performSSRRendering called", {
|
|
51
82
|
elementType: getElementTypeName(pageElement),
|
|
52
83
|
hasChildren: !!(pageElement.props as Record<string, unknown>)?.children,
|
|
@@ -133,6 +164,94 @@ export class SSROrchestrator {
|
|
|
133
164
|
};
|
|
134
165
|
}
|
|
135
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Perform SSR rendering in an isolated per-project Worker.
|
|
169
|
+
*
|
|
170
|
+
* The Worker imports user modules from their temp file paths,
|
|
171
|
+
* constructs the React element tree, and renders to HTML.
|
|
172
|
+
* For streaming, the Worker sends chunks via postMessage.
|
|
173
|
+
*/
|
|
174
|
+
private async performIsolatedSSR(
|
|
175
|
+
generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
|
|
176
|
+
options: RenderOptions | undefined,
|
|
177
|
+
isolation: SSRIsolationOptions,
|
|
178
|
+
): Promise<SSRRenderingResult> {
|
|
179
|
+
const wantsStream = options?.delivery === "stream";
|
|
180
|
+
const pool = getWorkerPool();
|
|
181
|
+
const requestId = dntShim.crypto.randomUUID();
|
|
182
|
+
|
|
183
|
+
return withSpan(
|
|
184
|
+
"ssr.isolated_render",
|
|
185
|
+
async () => {
|
|
186
|
+
const worker = pool.getOrCreateWorker(isolation.projectDir, [isolation.projectDir]);
|
|
187
|
+
|
|
188
|
+
if (wantsStream) {
|
|
189
|
+
// Streaming mode: get a ReadableStream of chunks from the Worker
|
|
190
|
+
const stream = worker.executeStream({
|
|
191
|
+
type: "render-ssr",
|
|
192
|
+
id: requestId,
|
|
193
|
+
pageModulePath: isolation.pageModulePath,
|
|
194
|
+
layoutModulePaths: isolation.layoutModulePaths,
|
|
195
|
+
pageProps: isolation.pageProps,
|
|
196
|
+
layoutProps: isolation.layoutProps,
|
|
197
|
+
delivery: "stream",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const ssrHash = `stream-isolated-${Date.now()}`;
|
|
201
|
+
|
|
202
|
+
// Generate HTML stream using the framework's HTML generator
|
|
203
|
+
const finalStream = await this.config.htmlGenerator.generateHTMLStream(stream, {
|
|
204
|
+
...generationContext,
|
|
205
|
+
ssrHash,
|
|
206
|
+
options: { ...generationContext.options, ...options },
|
|
207
|
+
collectedHead: undefined,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return { fullHtml: "", finalStream, ssrHash };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// String mode: render to HTML in Worker, get result back
|
|
214
|
+
const workerResponse: WorkerResponse = await worker.execute({
|
|
215
|
+
type: "render-ssr",
|
|
216
|
+
id: requestId,
|
|
217
|
+
pageModulePath: isolation.pageModulePath,
|
|
218
|
+
layoutModulePaths: isolation.layoutModulePaths,
|
|
219
|
+
pageProps: isolation.pageProps,
|
|
220
|
+
layoutProps: isolation.layoutProps,
|
|
221
|
+
delivery: "string",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (workerResponse.type === "error") {
|
|
225
|
+
const err = new Error(workerResponse.error.message);
|
|
226
|
+
err.name = workerResponse.error.name;
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (workerResponse.type !== "ssr-result") {
|
|
231
|
+
throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const html = workerResponse.html;
|
|
235
|
+
const ssrHash = await computeHash(html);
|
|
236
|
+
|
|
237
|
+
const fullHtml = await this.config.htmlGenerator.generateFullHTML({
|
|
238
|
+
...generationContext,
|
|
239
|
+
html,
|
|
240
|
+
ssrHash,
|
|
241
|
+
options: { ...generationContext.options, ...options },
|
|
242
|
+
collectedHead: undefined,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return { fullHtml, finalStream: null, ssrHash };
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"ssr.isolated": true,
|
|
249
|
+
"ssr.wants_stream": wantsStream,
|
|
250
|
+
"ssr.project_dir": isolation.projectDir,
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
136
255
|
private createStream(html: string): ReadableStream | null {
|
|
137
256
|
try {
|
|
138
257
|
return new dntShim.Response(html).body ?? null;
|
|
@@ -13,7 +13,7 @@ import { ApiRouteMatcher, type RouteMatch } from "./api-route-matcher.js";
|
|
|
13
13
|
import type { APIRoute } from "./module-loader/types.js";
|
|
14
14
|
import { loadHandlerModule } from "./module-loader/loader.js";
|
|
15
15
|
import { discoverAppRoutes, discoverPagesRoutes } from "./route-discovery.js";
|
|
16
|
-
import { executeAppRoute, executePagesRoute } from "./route-executor.js";
|
|
16
|
+
import { executeAppRoute, executePagesRoute, type ExecuteRouteOptions } from "./route-executor.js";
|
|
17
17
|
import { withSpan } from "../../observability/tracing/otlp-setup.js";
|
|
18
18
|
|
|
19
19
|
/** Max entries in the loaded-handler LRU cache */
|
|
@@ -188,9 +188,22 @@ export class APIRouteHandler {
|
|
|
188
188
|
// Note: Cannot use path-based detection (/app/) as projectDir may be '/app' in production
|
|
189
189
|
const isAppRoute = /\/route\.(ts|js|tsx|jsx)$/.test(match.route.page);
|
|
190
190
|
|
|
191
|
+
const isolationOptions: ExecuteRouteOptions = {
|
|
192
|
+
modulePath: match.route.page,
|
|
193
|
+
projectDir: this.projectDir,
|
|
194
|
+
};
|
|
195
|
+
|
|
191
196
|
const response = isAppRoute
|
|
192
|
-
? await executeAppRoute(handler, request, match, pathname, adapter)
|
|
193
|
-
: await executePagesRoute(
|
|
197
|
+
? await executeAppRoute(handler, request, match, pathname, adapter, isolationOptions)
|
|
198
|
+
: await executePagesRoute(
|
|
199
|
+
handler,
|
|
200
|
+
request,
|
|
201
|
+
match,
|
|
202
|
+
pathname,
|
|
203
|
+
adapter,
|
|
204
|
+
this.projectDir,
|
|
205
|
+
isolationOptions,
|
|
206
|
+
);
|
|
194
207
|
|
|
195
208
|
const corsResponse = await applyCORSHeaders({
|
|
196
209
|
request,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as dntShim from "../../../_dnt.shims.js";
|
|
2
2
|
import type { FileSystemAdapter, RuntimeAdapter } from "../../platform/adapters/base.js";
|
|
3
|
-
import { createContext, normalizeParams } from "./context-builder.js";
|
|
3
|
+
import { createContext, normalizeParams, parseCookies } from "./context-builder.js";
|
|
4
4
|
import type { RouteMatch } from "./api-route-matcher.js";
|
|
5
5
|
import { createError, toError } from "../../errors/veryfront-error.js";
|
|
6
6
|
import type {
|
|
@@ -20,6 +20,16 @@ import { errorToRFC9457Response } from "../../errors/middleware/http-error-bound
|
|
|
20
20
|
import { serverLogger as logger } from "../../utils/index.js";
|
|
21
21
|
import { isDevelopment as isDevelopmentEnv } from "../../build/config/environment.js";
|
|
22
22
|
import type { HandlerContext } from "../../types/index.js";
|
|
23
|
+
import {
|
|
24
|
+
getWorkerPool,
|
|
25
|
+
isWorkerIsolationEnabled,
|
|
26
|
+
} from "../../security/sandbox/worker-pool.js";
|
|
27
|
+
import type {
|
|
28
|
+
SerializedRequest,
|
|
29
|
+
SerializedResponse,
|
|
30
|
+
WorkerResponse,
|
|
31
|
+
} from "../../security/sandbox/worker-types.js";
|
|
32
|
+
import { getProjectEnvSnapshot } from "../../server/project-env/storage.js";
|
|
23
33
|
|
|
24
34
|
function isDevelopment(adapter: RuntimeAdapter): boolean {
|
|
25
35
|
const env = adapter.env.get("MODE") ??
|
|
@@ -115,13 +125,206 @@ function toHeadResponse(response: dntShim.Response): dntShim.Response {
|
|
|
115
125
|
return new dntShim.Response(null, { status: response.status, headers: response.headers });
|
|
116
126
|
}
|
|
117
127
|
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Worker Isolation Helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
async function serializeRequest(request: dntShim.Request): Promise<SerializedRequest> {
|
|
133
|
+
const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
|
|
134
|
+
return {
|
|
135
|
+
url: request.url,
|
|
136
|
+
method: request.method,
|
|
137
|
+
headers: [...request.headers.entries()],
|
|
138
|
+
body,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function deserializeResponse(s: SerializedResponse): dntShim.Response {
|
|
143
|
+
return new dntShim.Response(s.body as dntShim.BodyInit | null, {
|
|
144
|
+
status: s.status,
|
|
145
|
+
statusText: s.statusText,
|
|
146
|
+
headers: s.headers,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function workerResponseToResponse(
|
|
151
|
+
workerResponse: WorkerResponse,
|
|
152
|
+
pathname: string,
|
|
153
|
+
adapter: RuntimeAdapter,
|
|
154
|
+
): dntShim.Response {
|
|
155
|
+
if (workerResponse.type === "error") {
|
|
156
|
+
const { error } = workerResponse;
|
|
157
|
+
logger.error(`API route error in ${pathname} (worker):`, error.message);
|
|
158
|
+
|
|
159
|
+
// If the worker serialized RFC 9457 fields, return them directly
|
|
160
|
+
// to preserve the original status code, type, and detail.
|
|
161
|
+
if (error.status && error.type) {
|
|
162
|
+
return dntShim.Response.json(
|
|
163
|
+
{
|
|
164
|
+
type: error.type,
|
|
165
|
+
title: error.name,
|
|
166
|
+
status: error.status,
|
|
167
|
+
detail: error.detail ?? error.message,
|
|
168
|
+
instance: pathname,
|
|
169
|
+
},
|
|
170
|
+
{ status: error.status },
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const ctx = { isLocalProject: isDevelopment(adapter) } as HandlerContext;
|
|
175
|
+
const req = new dntShim.Request(`http://localhost${pathname}`);
|
|
176
|
+
const err = new Error(error.message);
|
|
177
|
+
err.name = error.name;
|
|
178
|
+
return errorToRFC9457Response(err, ctx, req);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (workerResponse.type === "result") {
|
|
182
|
+
return deserializeResponse(workerResponse.response);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// data-result type is not expected in API route execution
|
|
186
|
+
throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Isolated Execution (Worker Path)
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
function executeAppRouteIsolated(
|
|
194
|
+
modulePath: string,
|
|
195
|
+
request: dntShim.Request,
|
|
196
|
+
match: RouteMatch,
|
|
197
|
+
pathname: string,
|
|
198
|
+
adapter: RuntimeAdapter,
|
|
199
|
+
projectDir: string,
|
|
200
|
+
): Promise<dntShim.Response> {
|
|
201
|
+
const method = request.method.toUpperCase() as HTTPMethod;
|
|
202
|
+
|
|
203
|
+
return withSpan(
|
|
204
|
+
"api.executeAppRoute.isolated",
|
|
205
|
+
async () => {
|
|
206
|
+
try {
|
|
207
|
+
const pool = getWorkerPool();
|
|
208
|
+
const serialized = await serializeRequest(request);
|
|
209
|
+
|
|
210
|
+
const workerResponse = await pool.execute(
|
|
211
|
+
projectDir,
|
|
212
|
+
[projectDir],
|
|
213
|
+
{
|
|
214
|
+
type: "execute-app-route",
|
|
215
|
+
id: dntShim.crypto.randomUUID(),
|
|
216
|
+
modulePath,
|
|
217
|
+
method,
|
|
218
|
+
request: serialized,
|
|
219
|
+
params: match.params,
|
|
220
|
+
projectDir,
|
|
221
|
+
projectEnv: getProjectEnvSnapshot(),
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const response = workerResponseToResponse(workerResponse, pathname, adapter);
|
|
226
|
+
return method === "HEAD" ? toHeadResponse(response) : response;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return handleAPIError(error, pathname, adapter);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"http.method": method,
|
|
233
|
+
"http.path": pathname,
|
|
234
|
+
"api.route.pattern": match.route.pattern,
|
|
235
|
+
"api.isolated": true,
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function executePagesRouteIsolated(
|
|
241
|
+
modulePath: string,
|
|
242
|
+
request: dntShim.Request,
|
|
243
|
+
match: RouteMatch,
|
|
244
|
+
pathname: string,
|
|
245
|
+
adapter: RuntimeAdapter,
|
|
246
|
+
projectDir: string,
|
|
247
|
+
): Promise<dntShim.Response> {
|
|
248
|
+
const method = request.method as string;
|
|
249
|
+
|
|
250
|
+
return withSpan(
|
|
251
|
+
"api.executePagesRoute.isolated",
|
|
252
|
+
async () => {
|
|
253
|
+
try {
|
|
254
|
+
const pool = getWorkerPool();
|
|
255
|
+
const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
|
|
256
|
+
|
|
257
|
+
const workerResponse = await pool.execute(
|
|
258
|
+
projectDir,
|
|
259
|
+
[projectDir],
|
|
260
|
+
{
|
|
261
|
+
type: "execute-pages-route",
|
|
262
|
+
id: dntShim.crypto.randomUUID(),
|
|
263
|
+
modulePath,
|
|
264
|
+
method,
|
|
265
|
+
context: {
|
|
266
|
+
url: request.url,
|
|
267
|
+
method: request.method,
|
|
268
|
+
headers: [...request.headers.entries()],
|
|
269
|
+
body,
|
|
270
|
+
params: match.params,
|
|
271
|
+
cookies: parseCookies(request.headers.get("cookie") ?? ""),
|
|
272
|
+
},
|
|
273
|
+
projectDir,
|
|
274
|
+
projectEnv: getProjectEnvSnapshot(),
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return workerResponseToResponse(workerResponse, pathname, adapter);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return handleAPIError(error, pathname, adapter);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"http.method": method,
|
|
285
|
+
"http.path": pathname,
|
|
286
|
+
"api.route.pattern": match.route.pattern,
|
|
287
|
+
"api.isolated": true,
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Public API
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
export interface ExecuteRouteOptions {
|
|
297
|
+
/** Absolute path to the handler module on disk (for isolated execution) */
|
|
298
|
+
modulePath?: string;
|
|
299
|
+
/** Project directory (for isolated execution scope) */
|
|
300
|
+
projectDir?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
118
303
|
export function executeAppRoute(
|
|
119
304
|
handler: APIRoute,
|
|
120
305
|
request: dntShim.Request,
|
|
121
306
|
match: RouteMatch,
|
|
122
307
|
pathname: string,
|
|
123
308
|
adapter: RuntimeAdapter,
|
|
309
|
+
options?: ExecuteRouteOptions,
|
|
124
310
|
): Promise<dntShim.Response> {
|
|
311
|
+
// Isolated path: execute in per-project Worker, fall back to main process on error
|
|
312
|
+
if (
|
|
313
|
+
isWorkerIsolationEnabled() &&
|
|
314
|
+
options?.modulePath &&
|
|
315
|
+
options?.projectDir
|
|
316
|
+
) {
|
|
317
|
+
return executeAppRouteIsolated(
|
|
318
|
+
options.modulePath,
|
|
319
|
+
request,
|
|
320
|
+
match,
|
|
321
|
+
pathname,
|
|
322
|
+
adapter,
|
|
323
|
+
options.projectDir,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Default path: execute in main process (existing behavior)
|
|
125
328
|
const method = request.method.toUpperCase() as HTTPMethod;
|
|
126
329
|
|
|
127
330
|
return withSpan(
|
|
@@ -158,7 +361,25 @@ export function executePagesRoute(
|
|
|
158
361
|
pathname: string,
|
|
159
362
|
adapter: RuntimeAdapter,
|
|
160
363
|
projectDir?: string,
|
|
364
|
+
options?: ExecuteRouteOptions,
|
|
161
365
|
): Promise<dntShim.Response> {
|
|
366
|
+
// Isolated path: execute in per-project Worker, fall back to main process on error
|
|
367
|
+
if (
|
|
368
|
+
isWorkerIsolationEnabled() &&
|
|
369
|
+
options?.modulePath &&
|
|
370
|
+
(options?.projectDir ?? projectDir)
|
|
371
|
+
) {
|
|
372
|
+
return executePagesRouteIsolated(
|
|
373
|
+
options.modulePath,
|
|
374
|
+
request,
|
|
375
|
+
match,
|
|
376
|
+
pathname,
|
|
377
|
+
adapter,
|
|
378
|
+
options.projectDir ?? projectDir!,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Default path: execute in main process (existing behavior)
|
|
162
383
|
const method = request.method as keyof APIRoute;
|
|
163
384
|
|
|
164
385
|
return withSpan(
|
|
@@ -41,3 +41,14 @@ export const BUILD_HELPER_PERMISSIONS = [
|
|
|
41
41
|
"--allow-write",
|
|
42
42
|
"--allow-env",
|
|
43
43
|
] as const;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* RENDER_WORKER — Per-project Worker for isolated code execution.
|
|
47
|
+
* Read-only filesystem (transformed modules), network (data fetchers),
|
|
48
|
+
* env (API keys and config). No subprocess/ffi/sys.
|
|
49
|
+
*/
|
|
50
|
+
export const RENDER_WORKER_PERMISSIONS = [
|
|
51
|
+
"--allow-read",
|
|
52
|
+
"--allow-net",
|
|
53
|
+
"--allow-env",
|
|
54
|
+
] as const;
|