veryfront 0.1.73 → 0.1.74
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 +32 -5
- package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command.js +87 -21
- 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.js +1 -1
- 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/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/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/package.json +1 -1
- package/src/cli/commands/knowledge/command-help.ts +3 -1
- package/src/cli/commands/knowledge/command.ts +104 -21
- package/src/cli/commands/knowledge/parser-source.ts +110 -5
- package/src/deno.js +1 -1
- package/src/src/server/handlers/request/ssr/ssr.handler.ts +11 -2
- 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/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
|
@@ -62,7 +62,7 @@ type DownloadResult = { uploadPath: string; localPath: string; bytes?: number };
|
|
|
62
62
|
const KnowledgeIngestArgsSchema = z.object({
|
|
63
63
|
projectSlug: z.string().optional(),
|
|
64
64
|
projectDir: z.string().optional(),
|
|
65
|
-
|
|
65
|
+
sources: z.array(z.string()).default([]),
|
|
66
66
|
path: z.string().optional(),
|
|
67
67
|
all: z.boolean().default(false),
|
|
68
68
|
recursive: z.boolean().default(false),
|
|
@@ -72,6 +72,44 @@ const KnowledgeIngestArgsSchema = z.object({
|
|
|
72
72
|
slug: z.string().optional(),
|
|
73
73
|
json: z.boolean().default(false),
|
|
74
74
|
quiet: z.boolean().default(false),
|
|
75
|
+
}).superRefine((value, ctx) => {
|
|
76
|
+
const hasExplicitSources = value.sources.length > 0;
|
|
77
|
+
const hasPath = typeof value.path === "string" && value.path.length > 0;
|
|
78
|
+
|
|
79
|
+
if (hasExplicitSources && (hasPath || value.all)) {
|
|
80
|
+
ctx.addIssue({
|
|
81
|
+
code: z.ZodIssueCode.custom,
|
|
82
|
+
message: "Use either explicit source paths or --path with --all, not both.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!hasExplicitSources && !hasPath && !value.all) {
|
|
87
|
+
ctx.addIssue({
|
|
88
|
+
code: z.ZodIssueCode.custom,
|
|
89
|
+
message: "Provide one or more source paths or use --path with --all.",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasPath && !value.all) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: z.ZodIssueCode.custom,
|
|
96
|
+
message: "--path requires --all.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!hasPath && value.all) {
|
|
101
|
+
ctx.addIssue({
|
|
102
|
+
code: z.ZodIssueCode.custom,
|
|
103
|
+
message: "--all requires --path.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (value.slug && value.sources.length !== 1) {
|
|
108
|
+
ctx.addIssue({
|
|
109
|
+
code: z.ZodIssueCode.custom,
|
|
110
|
+
message: "--slug can only be used with a single explicit source.",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
75
113
|
});
|
|
76
114
|
|
|
77
115
|
export type KnowledgeIngestOptions = z.infer<typeof KnowledgeIngestArgsSchema>;
|
|
@@ -97,7 +135,7 @@ function showKnowledgeUsage(): void {
|
|
|
97
135
|
Veryfront Knowledge
|
|
98
136
|
|
|
99
137
|
Usage:
|
|
100
|
-
veryfront knowledge ingest <source
|
|
138
|
+
veryfront knowledge ingest <source...> [options]
|
|
101
139
|
veryfront knowledge ingest --path <prefix-or-dir> --all [options]
|
|
102
140
|
|
|
103
141
|
Subcommands:
|
|
@@ -111,7 +149,7 @@ export function parseKnowledgeIngestArgs(
|
|
|
111
149
|
return KnowledgeIngestArgsSchema.safeParse({
|
|
112
150
|
projectSlug: getStringArg(args, "project", "p", "project-slug"),
|
|
113
151
|
projectDir: getStringArg(args, "project-dir", "dir", "d"),
|
|
114
|
-
|
|
152
|
+
sources: args._.slice(2).filter((value): value is string => typeof value === "string"),
|
|
115
153
|
path: getStringArg(args, "path"),
|
|
116
154
|
all: getBooleanArg(args, "all"),
|
|
117
155
|
recursive: getBooleanArg(args, "recursive"),
|
|
@@ -273,6 +311,7 @@ export async function runKnowledgeParser(input: {
|
|
|
273
311
|
description?: string;
|
|
274
312
|
slug?: string;
|
|
275
313
|
sourceReference?: string;
|
|
314
|
+
env?: Record<string, string>;
|
|
276
315
|
}): Promise<KnowledgeParserResult> {
|
|
277
316
|
const tempDir = await dntShim.Deno.makeTempDir({ prefix: "veryfront-knowledge-parser-" });
|
|
278
317
|
const inputJsonPath = `${tempDir}/input.json`;
|
|
@@ -296,6 +335,7 @@ export async function runKnowledgeParser(input: {
|
|
|
296
335
|
try {
|
|
297
336
|
result = await new dntShim.Deno.Command("python3", {
|
|
298
337
|
args: [scriptPath, "--input-json", inputJsonPath, "--output-json", outputJsonPath],
|
|
338
|
+
...(input.env ? { env: input.env } : {}),
|
|
299
339
|
stdout: "piped",
|
|
300
340
|
stderr: "piped",
|
|
301
341
|
}).output();
|
|
@@ -321,7 +361,7 @@ export async function runKnowledgeParser(input: {
|
|
|
321
361
|
}
|
|
322
362
|
|
|
323
363
|
export async function collectKnowledgeSources(
|
|
324
|
-
options: Pick<KnowledgeIngestOptions, "
|
|
364
|
+
options: Pick<KnowledgeIngestOptions, "sources" | "path" | "all" | "recursive">,
|
|
325
365
|
deps: {
|
|
326
366
|
client: ApiClient;
|
|
327
367
|
projectSlug: string;
|
|
@@ -330,29 +370,68 @@ export async function collectKnowledgeSources(
|
|
|
330
370
|
): Promise<KnowledgeSource[]> {
|
|
331
371
|
const fs = createFileSystem();
|
|
332
372
|
|
|
333
|
-
if (options.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
373
|
+
if (options.sources.length > 0) {
|
|
374
|
+
const explicitSources: Array<
|
|
375
|
+
| { kind: "local"; sources: KnowledgeSource[] }
|
|
376
|
+
| { kind: "upload"; input: string; uploadPath: string }
|
|
377
|
+
> = [];
|
|
378
|
+
const uploadTargets: string[] = [];
|
|
379
|
+
|
|
380
|
+
for (const input of options.sources) {
|
|
381
|
+
if (!isProjectUploadReference(input) && await fs.exists(input)) {
|
|
382
|
+
const localFiles = await collectLocalFiles(input, options.recursive);
|
|
383
|
+
if (!localFiles.length) throw new Error(`No supported files found at ${input}`);
|
|
384
|
+
explicitSources.push({
|
|
385
|
+
kind: "local",
|
|
386
|
+
sources: localFiles.map((localPath) => ({ kind: "local", input, localPath })),
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isLikelyLocalPath(input)) {
|
|
392
|
+
throw new Error(`Local file not found: ${input}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const uploadPath = normalizeProjectUploadPath(input);
|
|
396
|
+
explicitSources.push({ kind: "upload", input, uploadPath });
|
|
397
|
+
uploadTargets.push(uploadPath);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const downloads = uploadTargets.length > 0 ? await deps.downloadUploads(uploadTargets) : [];
|
|
401
|
+
const downloadsByPath = new Map<string, DownloadResult[]>();
|
|
402
|
+
|
|
403
|
+
for (const download of downloads) {
|
|
404
|
+
const existing = downloadsByPath.get(download.uploadPath) ?? [];
|
|
405
|
+
existing.push(download);
|
|
406
|
+
downloadsByPath.set(download.uploadPath, existing);
|
|
338
407
|
}
|
|
339
408
|
|
|
340
|
-
|
|
341
|
-
|
|
409
|
+
const resolvedSources: KnowledgeSource[] = [];
|
|
410
|
+
for (const source of explicitSources) {
|
|
411
|
+
if (source.kind === "local") {
|
|
412
|
+
resolvedSources.push(...source.sources);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const matchingDownloads = downloadsByPath.get(source.uploadPath);
|
|
417
|
+
const download = matchingDownloads?.shift();
|
|
418
|
+
if (!download) {
|
|
419
|
+
throw new Error(`Upload not found: ${formatKnowledgeUploadSource(source.uploadPath)}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
resolvedSources.push({
|
|
423
|
+
kind: "upload",
|
|
424
|
+
input: source.input,
|
|
425
|
+
uploadPath: download.uploadPath,
|
|
426
|
+
localPath: download.localPath,
|
|
427
|
+
});
|
|
342
428
|
}
|
|
343
429
|
|
|
344
|
-
|
|
345
|
-
const downloads = await deps.downloadUploads([uploadPath]);
|
|
346
|
-
return downloads.map((download) => ({
|
|
347
|
-
kind: "upload",
|
|
348
|
-
input: options.source!,
|
|
349
|
-
uploadPath: download.uploadPath,
|
|
350
|
-
localPath: download.localPath,
|
|
351
|
-
}));
|
|
430
|
+
return resolvedSources;
|
|
352
431
|
}
|
|
353
432
|
|
|
354
433
|
if (!options.path || !options.all) {
|
|
355
|
-
throw new Error("Provide
|
|
434
|
+
throw new Error("Provide one or more source paths or use --path with --all.");
|
|
356
435
|
}
|
|
357
436
|
|
|
358
437
|
if (!isProjectUploadReference(options.path) && await fs.exists(options.path)) {
|
|
@@ -407,7 +486,11 @@ export async function ingestResolvedSources(
|
|
|
407
486
|
uploadKnowledgeFile: (remotePath: string, localPath: string) => Promise<{ path: string }>;
|
|
408
487
|
},
|
|
409
488
|
): Promise<KnowledgeIngestFileResult[]> {
|
|
410
|
-
|
|
489
|
+
if (options.slug && sources.length !== 1) {
|
|
490
|
+
throw new Error("--slug can only be used with a single explicit source.");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const slugs = options.slug ? [options.slug] : ensureUniqueSlugs(sources);
|
|
411
494
|
const results: KnowledgeIngestFileResult[] = [];
|
|
412
495
|
|
|
413
496
|
for (const [index, source] of sources.entries()) {
|
|
@@ -3,6 +3,7 @@ import argparse
|
|
|
3
3
|
import csv
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
|
+
import subprocess
|
|
6
7
|
from datetime import date
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Optional
|
|
@@ -71,6 +72,107 @@ def build_frontmatter(source: str, source_type: str, description: str) -> str:
|
|
|
71
72
|
])
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
def metadata_int(metadata: dict[str, Any], *keys: str) -> Optional[int]:
|
|
76
|
+
for key in keys:
|
|
77
|
+
value = metadata.get(key)
|
|
78
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
79
|
+
return value
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def metadata_string_list(metadata: dict[str, Any], *keys: str) -> Optional[list[str]]:
|
|
84
|
+
for key in keys:
|
|
85
|
+
value = metadata.get(key)
|
|
86
|
+
if isinstance(value, list) and all(isinstance(item, str) for item in value):
|
|
87
|
+
return value
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_kreuzberg_stats(source_type: str, content: str, metadata: dict[str, Any]):
|
|
92
|
+
stats: dict[str, Any] = {
|
|
93
|
+
"characters": len(content),
|
|
94
|
+
"lines": len(content.splitlines()) if content else 0,
|
|
95
|
+
"engine": "kreuzberg",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if isinstance(metadata.get("mime_type"), str):
|
|
99
|
+
stats["mime_type"] = metadata["mime_type"]
|
|
100
|
+
|
|
101
|
+
if source_type == "pdf":
|
|
102
|
+
stats["pages"] = metadata_int(metadata, "page_count") or 0
|
|
103
|
+
stats["tables"] = metadata_int(metadata, "table_count") or 0
|
|
104
|
+
elif source_type in {"xlsx", "xls"}:
|
|
105
|
+
stats["sheets"] = metadata_int(metadata, "sheet_count") or 0
|
|
106
|
+
stats["rows"] = metadata_int(metadata, "row_count") or 0
|
|
107
|
+
stats["sheet_names"] = metadata_string_list(metadata, "sheet_names") or []
|
|
108
|
+
elif source_type == "docx":
|
|
109
|
+
stats["paragraphs"] = metadata_int(metadata, "paragraph_count") or 0
|
|
110
|
+
stats["tables"] = metadata_int(metadata, "table_count") or 0
|
|
111
|
+
elif source_type == "pptx":
|
|
112
|
+
stats["slides"] = metadata_int(metadata, "slide_count", "page_count") or 0
|
|
113
|
+
stats["tables"] = metadata_int(metadata, "table_count") or 0
|
|
114
|
+
elif source_type == "html":
|
|
115
|
+
stats["tables"] = metadata_int(metadata, "table_count") or 0
|
|
116
|
+
|
|
117
|
+
return stats
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_with_kreuzberg(path: str, source_type: str):
|
|
121
|
+
warnings: list[str] = []
|
|
122
|
+
completed = subprocess.run(
|
|
123
|
+
[
|
|
124
|
+
"kreuzberg",
|
|
125
|
+
"extract",
|
|
126
|
+
path,
|
|
127
|
+
"--format",
|
|
128
|
+
"json",
|
|
129
|
+
"--output-format",
|
|
130
|
+
"markdown",
|
|
131
|
+
],
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
check=False,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if completed.returncode != 0:
|
|
138
|
+
detail = completed.stderr.strip() or completed.stdout.strip() or f"exit code {completed.returncode}"
|
|
139
|
+
raise RuntimeError(f"kreuzberg extract failed: {detail}")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
payload = json.loads(completed.stdout)
|
|
143
|
+
except json.JSONDecodeError as error:
|
|
144
|
+
raise RuntimeError(f"kreuzberg extract returned invalid JSON: {error}") from error
|
|
145
|
+
|
|
146
|
+
content = payload.get("content", "")
|
|
147
|
+
if not isinstance(content, str):
|
|
148
|
+
raise RuntimeError("kreuzberg extract did not return string content")
|
|
149
|
+
|
|
150
|
+
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
|
|
151
|
+
normalized_content = clean_text(content)
|
|
152
|
+
stats = build_kreuzberg_stats(source_type, normalized_content, metadata)
|
|
153
|
+
|
|
154
|
+
return normalized_content or "_No extractable text found in document._", stats, warnings
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def prefer_kreuzberg(source_type: str, fallback_parser):
|
|
158
|
+
def parser(path: str):
|
|
159
|
+
try:
|
|
160
|
+
return parse_with_kreuzberg(path, source_type)
|
|
161
|
+
except FileNotFoundError as error:
|
|
162
|
+
if getattr(error, "filename", "") == "kreuzberg":
|
|
163
|
+
return fallback_parser(path)
|
|
164
|
+
raise
|
|
165
|
+
except RuntimeError as error:
|
|
166
|
+
content, stats, warnings = fallback_parser(path)
|
|
167
|
+
warnings.append(
|
|
168
|
+
"kreuzberg extraction failed; fell back to the built-in parser: "
|
|
169
|
+
+ str(error)
|
|
170
|
+
)
|
|
171
|
+
return content, stats, warnings
|
|
172
|
+
|
|
173
|
+
return parser
|
|
174
|
+
|
|
175
|
+
|
|
74
176
|
def parse_csv_like(path: str, delimiter: str = ","):
|
|
75
177
|
warnings: list[str] = []
|
|
76
178
|
with open(path, newline="", encoding="utf-8-sig") as file:
|
|
@@ -305,18 +407,19 @@ def parse_json(path: str):
|
|
|
305
407
|
def select_parser(path: Path):
|
|
306
408
|
ext = path.suffix.lower()
|
|
307
409
|
if ext == ".pdf":
|
|
308
|
-
return "pdf", parse_pdf
|
|
410
|
+
return "pdf", prefer_kreuzberg("pdf", parse_pdf)
|
|
309
411
|
if ext in {".csv", ".tsv"}:
|
|
310
412
|
delimiter = "\t" if ext == ".tsv" else ","
|
|
311
413
|
return ext.lstrip("."), lambda file_path: parse_csv_like(file_path, delimiter)
|
|
312
414
|
if ext in {".xlsx", ".xls"}:
|
|
313
|
-
|
|
415
|
+
source_type = ext.lstrip(".")
|
|
416
|
+
return source_type, prefer_kreuzberg(source_type, parse_excel)
|
|
314
417
|
if ext == ".docx":
|
|
315
|
-
return "docx", parse_docx
|
|
418
|
+
return "docx", prefer_kreuzberg("docx", parse_docx)
|
|
316
419
|
if ext == ".pptx":
|
|
317
|
-
return "pptx", parse_pptx
|
|
420
|
+
return "pptx", prefer_kreuzberg("pptx", parse_pptx)
|
|
318
421
|
if ext in {".html", ".htm"}:
|
|
319
|
-
return "html", parse_html
|
|
422
|
+
return "html", prefer_kreuzberg("html", parse_html)
|
|
320
423
|
if ext in {".txt", ".md", ".mdx"}:
|
|
321
424
|
return ext.lstrip("."), parse_text
|
|
322
425
|
if ext == ".json":
|
|
@@ -325,6 +428,8 @@ def select_parser(path: Path):
|
|
|
325
428
|
|
|
326
429
|
|
|
327
430
|
def build_summary(source_type: str, stats: dict[str, Any]) -> str:
|
|
431
|
+
if stats.get("engine") == "kreuzberg":
|
|
432
|
+
return f"Converted {source_type.upper()} to markdown ({stats.get('characters', 0)} chars)."
|
|
328
433
|
if source_type in {"csv", "tsv"}:
|
|
329
434
|
return f"Parsed {stats.get('rows', 0)} rows across {stats.get('columns', 0)} columns."
|
|
330
435
|
if source_type in {"xlsx", "xls"}:
|
package/src/deno.js
CHANGED
|
@@ -26,7 +26,11 @@ import { serverLogger } from "../../../../utils/index.js";
|
|
|
26
26
|
import { endRequest, startRequest } from "../../../../utils/index.js";
|
|
27
27
|
import { tryNotFoundFallback } from "./not-found-fallback.js";
|
|
28
28
|
import { tryErrorPageFallback } from "./error-page-fallback.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
type SSRRenderResult,
|
|
31
|
+
SSRService,
|
|
32
|
+
type SSRServiceLike,
|
|
33
|
+
} from "../../../services/rendering/ssr.service.js";
|
|
30
34
|
import { ErrorPages } from "../../../utils/error-html.js";
|
|
31
35
|
import { buildSSRResponse } from "./ssr-response-builder.js";
|
|
32
36
|
|
|
@@ -62,7 +66,12 @@ export class SSRHandler extends BaseHandler {
|
|
|
62
66
|
patterns: [{ pattern: /^(?!\/_).*/, method: ["GET", "HEAD"] }],
|
|
63
67
|
};
|
|
64
68
|
|
|
65
|
-
private ssrService
|
|
69
|
+
private ssrService: SSRServiceLike;
|
|
70
|
+
|
|
71
|
+
constructor(ssrService?: SSRServiceLike) {
|
|
72
|
+
super();
|
|
73
|
+
this.ssrService = ssrService ?? new SSRService();
|
|
74
|
+
}
|
|
66
75
|
|
|
67
76
|
handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
|
|
68
77
|
const url = new URL(req.url);
|
|
@@ -15,7 +15,11 @@ import { isExtendedFSAdapter } from "../../platform/adapters/fs/wrapper.js";
|
|
|
15
15
|
import { getConfig } from "../../config/loader.js";
|
|
16
16
|
import type { VeryfrontConfig } from "../../config/index.js";
|
|
17
17
|
import { timeAsync } from "./request-lifecycle.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
defaultDiscoveryCache,
|
|
20
|
+
findLocalProjectPath,
|
|
21
|
+
type ProjectDiscoveryCache,
|
|
22
|
+
} from "./local-project-discovery.js";
|
|
19
23
|
import type { ParsedDomain } from "../utils/domain-parser.js";
|
|
20
24
|
|
|
21
25
|
const baseLogger = getBaseLogger("SERVER");
|
|
@@ -60,6 +64,8 @@ interface AdapterResolutionOptions {
|
|
|
60
64
|
headerProjectPath: string | undefined;
|
|
61
65
|
/** Whether running in proxy mode */
|
|
62
66
|
isProxyMode: boolean;
|
|
67
|
+
/** Optional injectable cache (defaults to module-level singleton) */
|
|
68
|
+
cache?: ProjectDiscoveryCache;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
@@ -71,6 +77,8 @@ interface AdapterResolutionOptions {
|
|
|
71
77
|
export async function resolveAdapter(
|
|
72
78
|
opts: AdapterResolutionOptions,
|
|
73
79
|
): Promise<AdapterResolutionResult> {
|
|
80
|
+
const cache = opts.cache ?? defaultDiscoveryCache;
|
|
81
|
+
|
|
74
82
|
let effectiveProjectDir = opts.projectDir;
|
|
75
83
|
let effectiveAdapter = opts.adapter;
|
|
76
84
|
let effectiveConfig = opts.config;
|
|
@@ -81,7 +89,7 @@ export async function resolveAdapter(
|
|
|
81
89
|
const trustedHeaderProjectPath = opts.isProxyMode ? opts.headerProjectPath : undefined;
|
|
82
90
|
const shouldCheckLocalPath = opts.projectSlug && (!opts.isProxyMode || trustedHeaderProjectPath);
|
|
83
91
|
const localProjectPath = shouldCheckLocalPath
|
|
84
|
-
? await findLocalProjectPath(opts.projectSlug!, opts.adapter, trustedHeaderProjectPath)
|
|
92
|
+
? await findLocalProjectPath(opts.projectSlug!, opts.adapter, trustedHeaderProjectPath, cache)
|
|
85
93
|
: undefined;
|
|
86
94
|
|
|
87
95
|
const isLocalProject = !!localProjectPath;
|
|
@@ -95,16 +103,16 @@ export async function resolveAdapter(
|
|
|
95
103
|
});
|
|
96
104
|
|
|
97
105
|
// Get or create local adapter
|
|
98
|
-
if (!
|
|
106
|
+
if (!cache.adapters.has(effectiveProjectDir)) {
|
|
99
107
|
const baseAdapter = await runtime.get();
|
|
100
|
-
|
|
108
|
+
cache.adapters.set(effectiveProjectDir, baseAdapter);
|
|
101
109
|
logger.debug("Created local adapter for project", {
|
|
102
110
|
projectSlug: opts.projectSlug,
|
|
103
111
|
projectDir: effectiveProjectDir,
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
effectiveAdapter =
|
|
115
|
+
effectiveAdapter = cache.adapters.get(effectiveProjectDir)!;
|
|
108
116
|
|
|
109
117
|
// Load project-specific config
|
|
110
118
|
try {
|
|
@@ -21,6 +21,7 @@ import { getErrorMessage } from "../../errors/veryfront-error.js";
|
|
|
21
21
|
import { UNKNOWN_ERROR } from "../../errors/error-registry.js";
|
|
22
22
|
import { errorToRFC9457Response } from "../../errors/middleware/http-error-boundary.js";
|
|
23
23
|
import { RouteRegistry } from "../../routing/registry/index.js";
|
|
24
|
+
import type { Handler } from "../../types/index.js";
|
|
24
25
|
import { SecurityConfigLoader } from "../../security/http/config.js";
|
|
25
26
|
|
|
26
27
|
// Re-export is at the bottom of the file
|
|
@@ -114,6 +115,136 @@ const baseLogger = getBaseLogger("SERVER");
|
|
|
114
115
|
|
|
115
116
|
const logger = baseLogger.component("runtime-handler");
|
|
116
117
|
|
|
118
|
+
/** Handler names in registration order. */
|
|
119
|
+
export const HANDLER_NAMES = [
|
|
120
|
+
"AuthHandler",
|
|
121
|
+
"CsrfHandler",
|
|
122
|
+
"HMRHandler",
|
|
123
|
+
"CorsHandler",
|
|
124
|
+
"HealthHandler",
|
|
125
|
+
"MetricsHandler",
|
|
126
|
+
"MemoryDebugHandler",
|
|
127
|
+
"ClientLogHandler",
|
|
128
|
+
"DevEndpointsHandler",
|
|
129
|
+
"StylesCSSHandler",
|
|
130
|
+
"DebugContextHandler",
|
|
131
|
+
"OpenAPIHandler",
|
|
132
|
+
"OpenAPIDocsHandler",
|
|
133
|
+
"InternalAgentsListHandler",
|
|
134
|
+
"AgentStreamHandler",
|
|
135
|
+
"AgentRunResumeHandler",
|
|
136
|
+
"AgentRunCancelHandler",
|
|
137
|
+
"ChannelInvokeHandler",
|
|
138
|
+
"DevDashboardHandler",
|
|
139
|
+
"ProjectsHandler",
|
|
140
|
+
"StudioBridgeModulesHandler",
|
|
141
|
+
"CSSHandler",
|
|
142
|
+
"DevFileHandler",
|
|
143
|
+
"SnippetHandler",
|
|
144
|
+
"StaticHandler",
|
|
145
|
+
"LibModulesHandler",
|
|
146
|
+
"RSCHandler",
|
|
147
|
+
"ModuleHandler",
|
|
148
|
+
"ApiHandlerWrapper",
|
|
149
|
+
"MarkdownPreviewHandler",
|
|
150
|
+
"SSRHandler",
|
|
151
|
+
"NotFoundHandler",
|
|
152
|
+
] as const;
|
|
153
|
+
|
|
154
|
+
/** Union of all registered handler names. */
|
|
155
|
+
export type HandlerName = (typeof HANDLER_NAMES)[number];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Dependencies for handler registry creation.
|
|
159
|
+
* All fields are optional — when omitted, the real handler implementation is used.
|
|
160
|
+
* This allows tests to inject mock handlers for specific slots.
|
|
161
|
+
*/
|
|
162
|
+
export interface HandlerDependencies {
|
|
163
|
+
/** Override any handler by its typed name. */
|
|
164
|
+
overrides?: Partial<Record<HandlerName, Handler>>;
|
|
165
|
+
/** When true, log handler registration details. */
|
|
166
|
+
debug?: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Factory for each handler. Only called when no override is provided (lazy instantiation). */
|
|
170
|
+
const handlerFactories: Record<
|
|
171
|
+
HandlerName,
|
|
172
|
+
(projectDir: string, adapter: RuntimeAdapter) => Handler
|
|
173
|
+
> = {
|
|
174
|
+
AuthHandler: () => new AuthHandler(),
|
|
175
|
+
CsrfHandler: () => new CsrfHandler(),
|
|
176
|
+
HMRHandler: () => new HMRHandler(),
|
|
177
|
+
CorsHandler: () => new CorsHandler(),
|
|
178
|
+
HealthHandler: () => new HealthHandler(),
|
|
179
|
+
MetricsHandler: () => new MetricsHandler(),
|
|
180
|
+
MemoryDebugHandler: () => new MemoryDebugHandler(),
|
|
181
|
+
ClientLogHandler: () => new ClientLogHandler(),
|
|
182
|
+
DevEndpointsHandler: () => new DevEndpointsHandler(),
|
|
183
|
+
StylesCSSHandler: () => new StylesCSSHandler(),
|
|
184
|
+
DebugContextHandler: () => new DebugContextHandler(),
|
|
185
|
+
OpenAPIHandler: () => new OpenAPIHandler(),
|
|
186
|
+
OpenAPIDocsHandler: () => new OpenAPIDocsHandler(),
|
|
187
|
+
InternalAgentsListHandler: () => new InternalAgentsListHandler(),
|
|
188
|
+
AgentStreamHandler: () => new AgentStreamHandler(),
|
|
189
|
+
AgentRunResumeHandler: () => new AgentRunResumeHandler(),
|
|
190
|
+
AgentRunCancelHandler: () => new AgentRunCancelHandler(),
|
|
191
|
+
ChannelInvokeHandler: () => new ChannelInvokeHandler(),
|
|
192
|
+
DevDashboardHandler: () => new DevDashboardHandler(),
|
|
193
|
+
ProjectsHandler: () => new ProjectsHandler(),
|
|
194
|
+
StudioBridgeModulesHandler: () => new StudioBridgeModulesHandler(),
|
|
195
|
+
CSSHandler: () => new CSSHandler(),
|
|
196
|
+
DevFileHandler: () => new DevFileHandler(),
|
|
197
|
+
SnippetHandler: () => new SnippetHandler(),
|
|
198
|
+
StaticHandler: () => new StaticHandler(),
|
|
199
|
+
LibModulesHandler: () => new LibModulesHandler(),
|
|
200
|
+
RSCHandler: () => new RSCHandler(),
|
|
201
|
+
ModuleHandler: () => new ModuleHandler(),
|
|
202
|
+
ApiHandlerWrapper: (projectDir, adapter) => new ApiHandlerWrapper(projectDir, adapter),
|
|
203
|
+
MarkdownPreviewHandler: () => new MarkdownPreviewHandler(),
|
|
204
|
+
SSRHandler: () => new SSRHandler(),
|
|
205
|
+
NotFoundHandler: () => new NotFoundHandler(),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Creates a RouteRegistry populated with the standard handler chain.
|
|
210
|
+
*
|
|
211
|
+
* Handlers are instantiated lazily — overridden slots skip construction
|
|
212
|
+
* of the default handler entirely.
|
|
213
|
+
*
|
|
214
|
+
* @param projectDir - Root project directory
|
|
215
|
+
* @param adapter - Runtime adapter for environment access
|
|
216
|
+
* @param deps - Optional dependency overrides for testing
|
|
217
|
+
* @returns Object containing the registry and the api handler (for initialization)
|
|
218
|
+
*/
|
|
219
|
+
export function createHandlerRegistry(
|
|
220
|
+
projectDir: string,
|
|
221
|
+
adapter: RuntimeAdapter,
|
|
222
|
+
deps: HandlerDependencies = {},
|
|
223
|
+
): { registry: RouteRegistry; apiHandler: ApiHandlerWrapper } {
|
|
224
|
+
const registry = new RouteRegistry({
|
|
225
|
+
debug: deps.debug,
|
|
226
|
+
enableMetrics: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const overrides = deps.overrides ?? {};
|
|
230
|
+
|
|
231
|
+
// Create the ApiHandlerWrapper first — it's special because callers need
|
|
232
|
+
// the returned instance for initialization regardless of overrides.
|
|
233
|
+
const apiHandler = overrides.ApiHandlerWrapper
|
|
234
|
+
? (overrides.ApiHandlerWrapper as ApiHandlerWrapper)
|
|
235
|
+
: new ApiHandlerWrapper(projectDir, adapter);
|
|
236
|
+
|
|
237
|
+
const handlers = HANDLER_NAMES.map((name) => {
|
|
238
|
+
if (name === "ApiHandlerWrapper") return apiHandler;
|
|
239
|
+
if (overrides[name]) return overrides[name]!;
|
|
240
|
+
return handlerFactories[name](projectDir, adapter);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
registry.registerAll(handlers);
|
|
244
|
+
|
|
245
|
+
return { registry, apiHandler };
|
|
246
|
+
}
|
|
247
|
+
|
|
117
248
|
export interface RuntimeHandlerOptions {
|
|
118
249
|
projectDir: string;
|
|
119
250
|
/** When true, expose additional debug logging. */
|
|
@@ -188,48 +319,10 @@ export function createVeryfrontHandler(
|
|
|
188
319
|
}
|
|
189
320
|
})();
|
|
190
321
|
|
|
191
|
-
const registry =
|
|
322
|
+
const { registry, apiHandler } = createHandlerRegistry(projectDir, adapter, {
|
|
192
323
|
debug: opts.debug,
|
|
193
|
-
enableMetrics: true,
|
|
194
324
|
});
|
|
195
325
|
|
|
196
|
-
const apiHandler = new ApiHandlerWrapper(projectDir, adapter);
|
|
197
|
-
|
|
198
|
-
registry.registerAll([
|
|
199
|
-
new AuthHandler(),
|
|
200
|
-
new CsrfHandler(),
|
|
201
|
-
new HMRHandler(),
|
|
202
|
-
new CorsHandler(),
|
|
203
|
-
new HealthHandler(),
|
|
204
|
-
new MetricsHandler(),
|
|
205
|
-
new MemoryDebugHandler(),
|
|
206
|
-
new ClientLogHandler(),
|
|
207
|
-
new DevEndpointsHandler(),
|
|
208
|
-
new StylesCSSHandler(),
|
|
209
|
-
new DebugContextHandler(),
|
|
210
|
-
new OpenAPIHandler(),
|
|
211
|
-
new OpenAPIDocsHandler(),
|
|
212
|
-
new InternalAgentsListHandler(),
|
|
213
|
-
new AgentStreamHandler(),
|
|
214
|
-
new AgentRunResumeHandler(),
|
|
215
|
-
new AgentRunCancelHandler(),
|
|
216
|
-
new ChannelInvokeHandler(),
|
|
217
|
-
new DevDashboardHandler(),
|
|
218
|
-
new ProjectsHandler(),
|
|
219
|
-
new StudioBridgeModulesHandler(),
|
|
220
|
-
new CSSHandler(),
|
|
221
|
-
new DevFileHandler(),
|
|
222
|
-
new SnippetHandler(),
|
|
223
|
-
new StaticHandler(),
|
|
224
|
-
new LibModulesHandler(),
|
|
225
|
-
new RSCHandler(),
|
|
226
|
-
new ModuleHandler(),
|
|
227
|
-
apiHandler,
|
|
228
|
-
new MarkdownPreviewHandler(),
|
|
229
|
-
new SSRHandler(),
|
|
230
|
-
new NotFoundHandler(),
|
|
231
|
-
]);
|
|
232
|
-
|
|
233
326
|
const isProxyMode = opts.config?.fs?.veryfront?.proxyMode === true;
|
|
234
327
|
|
|
235
328
|
const readyPromise = isProxyMode ? Promise.resolve() : apiHandler.initialize().catch((error) => {
|