veryfront 0.1.74 → 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.
Files changed (86) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts +2 -0
  2. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  3. package/esm/cli/commands/knowledge/command.js +64 -1
  4. package/esm/deno.d.ts +2 -0
  5. package/esm/deno.js +3 -1
  6. package/esm/src/data/data-fetcher.d.ts +11 -1
  7. package/esm/src/data/data-fetcher.d.ts.map +1 -1
  8. package/esm/src/data/data-fetcher.js +5 -2
  9. package/esm/src/data/index.d.ts +1 -1
  10. package/esm/src/data/index.d.ts.map +1 -1
  11. package/esm/src/data/server-data-fetcher.d.ts +14 -1
  12. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  13. package/esm/src/data/server-data-fetcher.js +49 -3
  14. package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
  15. package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
  16. package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
  17. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  18. package/esm/src/rendering/orchestrator/pipeline.js +6 -1
  19. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
  20. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
  21. package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
  22. package/esm/src/routing/api/handler.d.ts.map +1 -1
  23. package/esm/src/routing/api/handler.js +6 -2
  24. package/esm/src/routing/api/route-executor.d.ts +8 -2
  25. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  26. package/esm/src/routing/api/route-executor.js +131 -3
  27. package/esm/src/security/deno-permissions.d.ts +6 -0
  28. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  29. package/esm/src/security/deno-permissions.js +10 -0
  30. package/esm/src/security/sandbox/project-worker.d.ts +61 -0
  31. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
  32. package/esm/src/security/sandbox/project-worker.js +318 -0
  33. package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
  34. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
  35. package/esm/src/security/sandbox/worker-permissions.js +60 -0
  36. package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
  37. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
  38. package/esm/src/security/sandbox/worker-pool.js +356 -0
  39. package/esm/src/security/sandbox/worker-types.d.ts +165 -0
  40. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
  41. package/esm/src/security/sandbox/worker-types.js +17 -0
  42. package/esm/src/server/project-env/storage.d.ts +6 -0
  43. package/esm/src/server/project-env/storage.d.ts.map +1 -1
  44. package/esm/src/server/project-env/storage.js +8 -0
  45. package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
  46. package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
  47. package/esm/src/server/runtime-handler/project-isolation.js +44 -0
  48. package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
  49. package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
  50. package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
  51. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
  52. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  53. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
  54. package/esm/src/utils/index.d.ts +10 -1
  55. package/esm/src/utils/index.d.ts.map +1 -1
  56. package/esm/src/utils/index.js +9 -1
  57. package/esm/src/utils/logger/index.d.ts +1 -1
  58. package/esm/src/utils/logger/index.d.ts.map +1 -1
  59. package/esm/src/utils/logger/index.js +1 -1
  60. package/esm/src/utils/logger/logger.d.ts +14 -0
  61. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  62. package/esm/src/utils/logger/logger.js +17 -0
  63. package/esm/src/workflow/claude-code/tool.d.ts +5 -5
  64. package/package.json +4 -1
  65. package/src/cli/commands/knowledge/command.ts +76 -1
  66. package/src/deno.js +3 -1
  67. package/src/src/data/data-fetcher.ts +18 -2
  68. package/src/src/data/index.ts +1 -1
  69. package/src/src/data/server-data-fetcher.ts +78 -3
  70. package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
  71. package/src/src/rendering/orchestrator/pipeline.ts +7 -2
  72. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
  73. package/src/src/routing/api/handler.ts +16 -3
  74. package/src/src/routing/api/route-executor.ts +222 -1
  75. package/src/src/security/deno-permissions.ts +11 -0
  76. package/src/src/security/sandbox/project-worker.ts +416 -0
  77. package/src/src/security/sandbox/worker-permissions.ts +74 -0
  78. package/src/src/security/sandbox/worker-pool.ts +451 -0
  79. package/src/src/security/sandbox/worker-types.ts +209 -0
  80. package/src/src/server/project-env/storage.ts +9 -0
  81. package/src/src/server/runtime-handler/project-isolation.ts +53 -0
  82. package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
  83. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
  84. package/src/src/utils/index.ts +11 -0
  85. package/src/src/utils/logger/index.ts +1 -0
  86. package/src/src/utils/logger/logger.ts +34 -0
@@ -1,6 +1,6 @@
1
1
  import * as dntShim from "../../../_dnt.shims.js";
2
2
  import { z } from "zod";
3
- import { createFileSystem } from "../../../src/platform/index.js";
3
+ import { createFileSystem, getEnv } from "../../../src/platform/index.js";
4
4
  import { basename, extname, join, normalize, relative } from "../../../src/platform/compat/path/index.js";
5
5
  import { withSpan } from "../../../src/observability/tracing/otlp-setup.js";
6
6
  import { cliLogger } from "../../utils/index.js";
@@ -9,6 +9,7 @@ import type { ParsedArgs } from "../../shared/types.js";
9
9
  import { downloadUploadToFile, listAllUploads, type UploadItem } from "../uploads/command.js";
10
10
  import { putRemoteFileFromLocal } from "../files/command.js";
11
11
  import { knowledgeIngestPythonSource } from "./parser-source.js";
12
+ import { createJobUserLogger, type Logger, serverLogger } from "../../../src/utils/index.js";
12
13
 
13
14
  const SUPPORTED_EXTENSIONS = new Set([
14
15
  ".pdf",
@@ -59,6 +60,8 @@ type KnowledgeSource =
59
60
 
60
61
  type DownloadResult = { uploadPath: string; localPath: string; bytes?: number };
61
62
 
63
+ const knowledgeJobLogger = serverLogger.component("knowledge-ingest");
64
+
62
65
  const KnowledgeIngestArgsSchema = z.object({
63
66
  projectSlug: z.string().optional(),
64
67
  projectDir: z.string().optional(),
@@ -130,6 +133,27 @@ function printJson(value: unknown): void {
130
133
  console.log(JSON.stringify(value, null, 2));
131
134
  }
132
135
 
136
+ function createKnowledgeIngestEventLogger(): Logger | null {
137
+ const projectId = getEnv("TENANT_PROJECT_ID");
138
+ const jobId = getEnv("JOB_ID");
139
+
140
+ if (!projectId || !jobId) {
141
+ return null;
142
+ }
143
+
144
+ return createJobUserLogger(knowledgeJobLogger, {
145
+ projectId,
146
+ jobId,
147
+ batchId: getEnv("JOB_BATCH_ID") ?? undefined,
148
+ jobTarget: getEnv("JOB_TARGET") ?? undefined,
149
+ task: "knowledge-ingest",
150
+ });
151
+ }
152
+
153
+ function buildKnowledgeSourceName(source: KnowledgeSource): string {
154
+ return basename(source.kind === "upload" ? source.uploadPath : source.localPath);
155
+ }
156
+
133
157
  function showKnowledgeUsage(): void {
134
158
  console.log(`
135
159
  Veryfront Knowledge
@@ -484,6 +508,7 @@ export async function ingestResolvedSources(
484
508
  outputDir: string;
485
509
  runParser: typeof runKnowledgeParser;
486
510
  uploadKnowledgeFile: (remotePath: string, localPath: string) => Promise<{ path: string }>;
511
+ eventLogger?: Logger | null;
487
512
  },
488
513
  ): Promise<KnowledgeIngestFileResult[]> {
489
514
  if (options.slug && sources.length !== 1) {
@@ -494,6 +519,13 @@ export async function ingestResolvedSources(
494
519
  const results: KnowledgeIngestFileResult[] = [];
495
520
 
496
521
  for (const [index, source] of sources.entries()) {
522
+ deps.eventLogger?.info("Processing knowledge source", {
523
+ phase: "file_processing",
524
+ progress_current: index + 1,
525
+ progress_total: sources.length,
526
+ source_name: buildKnowledgeSourceName(source),
527
+ });
528
+
497
529
  const parser = await deps.runParser({
498
530
  filePath: source.localPath,
499
531
  outputDir: deps.outputDir,
@@ -507,6 +539,26 @@ export async function ingestResolvedSources(
507
539
  options.knowledgePath,
508
540
  );
509
541
  const uploaded = await deps.uploadKnowledgeFile(remotePath, parser.sandbox_output_path);
542
+
543
+ deps.eventLogger?.info("Knowledge source ingested", {
544
+ phase: "file_completed",
545
+ progress_current: index + 1,
546
+ progress_total: sources.length,
547
+ source_name: buildKnowledgeSourceName(source),
548
+ remote_path: uploaded.path,
549
+ warning_count: parser.warnings.length,
550
+ });
551
+
552
+ if (parser.warnings.length > 0) {
553
+ deps.eventLogger?.warn("Knowledge source emitted warnings", {
554
+ phase: "file_warning",
555
+ progress_current: index + 1,
556
+ progress_total: sources.length,
557
+ source_name: buildKnowledgeSourceName(source),
558
+ warning_count: parser.warnings.length,
559
+ });
560
+ }
561
+
510
562
  results.push(
511
563
  createKnowledgeIngestResult({
512
564
  source: buildSourceReference(source),
@@ -545,8 +597,14 @@ export async function knowledgeCommand(args: ParsedArgs): Promise<void> {
545
597
  const outputDir = options.outputDir ?? await defaultOutputRoot();
546
598
  const shouldCleanupOutputDir = options.outputDir === undefined;
547
599
  const downloadOutputDir = resolveKnowledgeDownloadOutputDir(outputDir);
600
+ const eventLogger = createKnowledgeIngestEventLogger();
548
601
 
549
602
  try {
603
+ eventLogger?.info("Starting knowledge ingest", {
604
+ phase: "started",
605
+ mode: options.path ? "path_prefix" : "explicit_sources",
606
+ });
607
+
550
608
  const sources = await collectKnowledgeSources(options, {
551
609
  client,
552
610
  projectSlug: config.projectSlug,
@@ -558,15 +616,27 @@ export async function knowledgeCommand(args: ParsedArgs): Promise<void> {
558
616
  ),
559
617
  });
560
618
 
619
+ eventLogger?.info("Resolved knowledge sources", {
620
+ phase: "sources_resolved",
621
+ progress_total: sources.length,
622
+ });
623
+
561
624
  const results = await ingestResolvedSources(sources, options, {
562
625
  client,
563
626
  projectSlug: config.projectSlug,
564
627
  outputDir,
565
628
  runParser: runKnowledgeParser,
629
+ eventLogger,
566
630
  uploadKnowledgeFile: (remotePath, localPath) =>
567
631
  putRemoteFileFromLocal(client, config.projectSlug, remotePath, localPath),
568
632
  });
569
633
 
634
+ eventLogger?.info("Completed knowledge ingest", {
635
+ phase: "completed",
636
+ progress_current: results.length,
637
+ progress_total: results.length,
638
+ });
639
+
570
640
  if (options.json) {
571
641
  printJson(results);
572
642
  return;
@@ -578,6 +648,11 @@ export async function knowledgeCommand(args: ParsedArgs): Promise<void> {
578
648
  cliLogger.info(` ${result.summary}`);
579
649
  }
580
650
  }
651
+ } catch (error) {
652
+ eventLogger?.error("Knowledge ingest failed", {
653
+ phase: "failed",
654
+ });
655
+ throw error;
581
656
  } finally {
582
657
  if (shouldCleanupOutputDir) {
583
658
  await Promise.all([
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -34,6 +34,7 @@ export default {
34
34
  "./resource": "./src/resource/index.ts",
35
35
  "./mcp": "./src/mcp/index.ts",
36
36
  "./middleware": "./src/middleware/index.ts",
37
+ "./utils": "./src/utils/index.ts",
37
38
  "./oauth": "./src/oauth/index.ts",
38
39
  "./provider": "./src/provider/index.ts",
39
40
  "./fs": "./src/fs/index.ts",
@@ -71,6 +72,7 @@ export default {
71
72
  "veryfront/workflow/claude-code": "./src/workflow/claude-code/index.ts",
72
73
  "veryfront/workflow/claude-code/react": "./src/workflow/claude-code/react/index.ts",
73
74
  "veryfront/workflow/discovery": "./src/workflow/discovery/index.ts",
75
+ "veryfront/utils": "./src/utils/index.ts",
74
76
  "veryfront/utils/box": "./src/utils/box.ts",
75
77
  "veryfront/utils/case-utils": "./src/utils/case-utils.ts",
76
78
  "veryfront/utils/constants/server": "./src/utils/constants/server.ts",
@@ -1,11 +1,22 @@
1
1
  import { withSpan } from "../observability/tracing/otlp-setup.js";
2
2
  import { SpanNames } from "../observability/tracing/span-names.js";
3
3
  import { CacheManager } from "./data-fetching-cache.js";
4
- import { ServerDataFetcher } from "./server-data-fetcher.js";
4
+ import { ServerDataFetcher, type ServerDataFetchOptions } from "./server-data-fetcher.js";
5
5
  import { StaticDataFetcher } from "./static-data-fetcher.js";
6
6
  import { StaticPathsFetcher } from "./static-paths-fetcher.js";
7
7
  import type { DataContext, DataResult, PageWithData, StaticPathsResult } from "./types.js";
8
8
 
9
+ /**
10
+ * Options for isolated data fetching. Passed through to ServerDataFetcher
11
+ * when worker isolation is enabled.
12
+ */
13
+ export interface FetchDataOptions {
14
+ /** Absolute path to the module containing getServerData */
15
+ modulePath?: string;
16
+ /** Project directory for worker scoping */
17
+ projectDir?: string;
18
+ }
19
+
9
20
  export class DataFetcher {
10
21
  private cacheManager: CacheManager;
11
22
  private serverFetcher: ServerDataFetcher;
@@ -23,6 +34,7 @@ export class DataFetcher {
23
34
  pageModule: PageWithData,
24
35
  context: DataContext,
25
36
  mode: "development" | "production" = "development",
37
+ options?: FetchDataOptions,
26
38
  ): Promise<DataResult> {
27
39
  const preferServerData = mode === "development" || !pageModule.getStaticData;
28
40
  const useServer = preferServerData && !!pageModule.getServerData;
@@ -34,10 +46,14 @@ export class DataFetcher {
34
46
  ? "static"
35
47
  : "none";
36
48
 
49
+ const isolationOptions: ServerDataFetchOptions | undefined = options
50
+ ? { modulePath: options.modulePath, projectDir: options.projectDir }
51
+ : undefined;
52
+
37
53
  return withSpan(
38
54
  SpanNames.DATA_FETCH,
39
55
  () => {
40
- if (useServer) return this.serverFetcher.fetch(pageModule, context);
56
+ if (useServer) return this.serverFetcher.fetch(pageModule, context, isolationOptions);
41
57
  if (useStatic) return this.staticFetcher.fetch(pageModule, context);
42
58
  return Promise.resolve({ props: {} });
43
59
  },
@@ -13,5 +13,5 @@ export type {
13
13
  PageWithData,
14
14
  StaticPathsResult,
15
15
  } from "./types.js";
16
- export { DataFetcher } from "./data-fetcher.js";
16
+ export { DataFetcher, type FetchDataOptions } from "./data-fetcher.js";
17
17
  export { notFound, redirect } from "./helpers.js";
@@ -1,12 +1,29 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
1
2
  import type { DataContext, DataResult, PageWithData } from "./types.js";
2
3
  import { serverLogger } from "../utils/index.js";
3
4
  import { DATA_FETCH_TIMEOUT_MS } from "../config/defaults.js";
4
5
  import { TimeoutError, withTimeoutThrow } from "../rendering/utils/stream-utils.js";
5
6
  import { withSpan } from "../observability/tracing/otlp-setup.js";
6
7
  import { CircuitBreakerOpen, getCircuitBreaker } from "../utils/circuit-breaker.js";
8
+ import { getWorkerPool, isDataIsolationEnabled } from "../security/sandbox/worker-pool.js";
9
+ import type { WorkerResponse } from "../security/sandbox/worker-types.js";
10
+
11
+ /**
12
+ * Options for isolated data fetching through Worker pool.
13
+ */
14
+ export interface ServerDataFetchOptions {
15
+ /** Absolute path to the module containing getServerData */
16
+ modulePath?: string;
17
+ /** Project directory for worker scoping */
18
+ projectDir?: string;
19
+ }
7
20
 
8
21
  export class ServerDataFetcher {
9
- fetch(pageModule: PageWithData, context: DataContext): Promise<DataResult> {
22
+ fetch(
23
+ pageModule: PageWithData,
24
+ context: DataContext,
25
+ options?: ServerDataFetchOptions,
26
+ ): Promise<DataResult> {
10
27
  if (typeof pageModule.getServerData !== "function") {
11
28
  return Promise.resolve({ props: {} });
12
29
  }
@@ -20,6 +37,11 @@ export class ServerDataFetcher {
20
37
  successThreshold: 2,
21
38
  });
22
39
 
40
+ // Choose isolated or direct execution
41
+ const useIsolation = isDataIsolationEnabled() &&
42
+ !!options?.modulePath &&
43
+ !!options?.projectDir;
44
+
23
45
  return withSpan(
24
46
  "data.fetch_server",
25
47
  async () => {
@@ -28,7 +50,9 @@ export class ServerDataFetcher {
28
50
  try {
29
51
  const result = await circuitBreaker.execute(() =>
30
52
  withTimeoutThrow(
31
- Promise.resolve(pageModule.getServerData!(context)),
53
+ useIsolation
54
+ ? this.fetchIsolated(options!.modulePath!, options!.projectDir!, context)
55
+ : Promise.resolve(pageModule.getServerData!(context)),
32
56
  DATA_FETCH_TIMEOUT_MS,
33
57
  `getServerData for ${pathname}`,
34
58
  )
@@ -59,7 +83,11 @@ export class ServerDataFetcher {
59
83
  throw error;
60
84
  }
61
85
 
62
- this.logError("DATA_FETCH_ERROR getServerData failed", error, { pathname, durationMs });
86
+ this.logError("DATA_FETCH_ERROR getServerData failed", error, {
87
+ pathname,
88
+ durationMs,
89
+ isolated: useIsolation,
90
+ });
63
91
  throw error;
64
92
  }
65
93
  },
@@ -68,8 +96,55 @@ export class ServerDataFetcher {
68
96
  "data.pathname": pathname,
69
97
  "data.timeout_ms": DATA_FETCH_TIMEOUT_MS,
70
98
  "data.project_id": projectId,
99
+ "data.isolated": useIsolation,
100
+ },
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Execute getServerData in a per-project Worker.
106
+ */
107
+ private async fetchIsolated(
108
+ modulePath: string,
109
+ projectDir: string,
110
+ context: DataContext,
111
+ ): Promise<DataResult> {
112
+ const pool = getWorkerPool();
113
+ const body = context.request?.body ? new Uint8Array(await context.request.arrayBuffer()) : null;
114
+
115
+ const workerResponse: WorkerResponse = await pool.execute(
116
+ projectDir,
117
+ [projectDir],
118
+ {
119
+ type: "fetch-data",
120
+ id: dntShim.crypto.randomUUID(),
121
+ modulePath,
122
+ context: {
123
+ params: context.params,
124
+ query: context.query?.toString() ?? "",
125
+ request: {
126
+ url: context.request?.url ?? context.url?.toString() ?? "http://localhost",
127
+ method: context.request?.method ?? "GET",
128
+ headers: context.request ? [...context.request.headers.entries()] : [],
129
+ body,
130
+ },
131
+ url: context.url?.toString() ?? "http://localhost",
132
+ },
71
133
  },
72
134
  );
135
+
136
+ if (workerResponse.type === "error") {
137
+ const err = new Error(workerResponse.error.message);
138
+ err.name = workerResponse.error.name;
139
+ throw err;
140
+ }
141
+
142
+ if (workerResponse.type === "data-result") {
143
+ return workerResponse.result as DataResult;
144
+ }
145
+
146
+ // Unexpected response type — shouldn't happen but be defensive
147
+ throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
73
148
  }
74
149
 
75
150
  /**
@@ -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(handler, request, match, pathname, adapter, this.projectDir);
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,