veryfront 0.1.72 → 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/html/html-shell-generator.d.ts.map +1 -1
- package/esm/src/html/html-shell-generator.js +6 -0
- package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +116 -105
- package/esm/src/server/dev-server/error-overlay/error-formatter.d.ts +4 -0
- package/esm/src/server/dev-server/error-overlay/error-formatter.d.ts.map +1 -1
- package/esm/src/server/dev-server/error-overlay/error-formatter.js +15 -0
- package/esm/src/server/dev-server/error-overlay/html-template.d.ts +1 -1
- package/esm/src/server/dev-server/error-overlay/html-template.d.ts.map +1 -1
- package/esm/src/server/dev-server/error-overlay/html-template.js +131 -8
- package/esm/src/server/dev-server/error-overlay/index.d.ts +1 -1
- package/esm/src/server/dev-server/error-overlay/index.d.ts.map +1 -1
- package/esm/src/server/dev-server/error-overlay/index.js +1 -1
- package/esm/src/server/dev-server/error-overlay/overlay-renderer.d.ts +1 -1
- package/esm/src/server/dev-server/error-overlay/overlay-renderer.d.ts.map +1 -1
- package/esm/src/server/dev-server/error-overlay/overlay-renderer.js +2 -2
- package/esm/src/server/dev-server/request-handler.d.ts.map +1 -1
- package/esm/src/server/dev-server/request-handler.js +6 -2
- 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 +18 -3
- 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/utils/error-html.d.ts.map +1 -1
- package/esm/src/server/utils/error-html.js +26 -6
- 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/html/html-shell-generator.ts +9 -0
- package/src/src/rendering/orchestrator/pipeline.ts +186 -172
- package/src/src/server/dev-server/error-overlay/error-formatter.ts +21 -0
- package/src/src/server/dev-server/error-overlay/html-template.ts +139 -8
- package/src/src/server/dev-server/error-overlay/index.ts +1 -0
- package/src/src/server/dev-server/error-overlay/overlay-renderer.ts +2 -1
- package/src/src/server/dev-server/request-handler.ts +6 -2
- 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 +43 -5
- package/src/src/server/shared/renderer/adapter.ts +107 -8
- package/src/src/server/shared/renderer/index.ts +7 -1
- package/src/src/server/utils/error-html.ts +29 -6
|
@@ -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) => {
|
|
@@ -17,24 +17,56 @@ const baseLogger = getBaseLogger("SERVER");
|
|
|
17
17
|
|
|
18
18
|
const logger = baseLogger.component("runtime-handler");
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Injectable cache container for project discovery state.
|
|
22
|
+
*
|
|
23
|
+
* Wraps both the project-path cache (slug → absolute path) and the
|
|
24
|
+
* adapter cache (project dir → RuntimeAdapter) so that callers — especially
|
|
25
|
+
* tests — can supply an isolated instance instead of sharing global state.
|
|
26
|
+
*/
|
|
27
|
+
export class ProjectDiscoveryCache {
|
|
28
|
+
/** Cache of discovered local project paths by slug */
|
|
29
|
+
readonly projects: LRUCache<string, string>;
|
|
30
|
+
/** Cache of local adapters by project directory */
|
|
31
|
+
readonly adapters: LRUCache<string, RuntimeAdapter>;
|
|
32
|
+
|
|
33
|
+
constructor(opts?: { maxProjects?: number; maxAdapters?: number }) {
|
|
34
|
+
this.projects = new LRUCache<string, string>({
|
|
35
|
+
maxEntries: opts?.maxProjects ?? 100,
|
|
36
|
+
});
|
|
37
|
+
this.adapters = new LRUCache<string, RuntimeAdapter>({
|
|
38
|
+
maxEntries: opts?.maxAdapters ?? 50,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
/** Clear both caches */
|
|
43
|
+
clear(): void {
|
|
44
|
+
this.projects.clear();
|
|
45
|
+
this.adapters.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
27
48
|
|
|
28
|
-
/**
|
|
29
|
-
export const
|
|
49
|
+
/** Default module-level cache instance (backward-compatible singleton) */
|
|
50
|
+
export const defaultDiscoveryCache = new ProjectDiscoveryCache();
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
52
|
+
// Register the default caches for monitoring
|
|
53
|
+
registerLRUCache("local-project-cache", defaultDiscoveryCache.projects);
|
|
54
|
+
registerLRUCache("local-adapter-cache", defaultDiscoveryCache.adapters);
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
/**
|
|
57
|
+
* @deprecated Use `defaultDiscoveryCache.adapters` instead.
|
|
58
|
+
* Kept for backward compatibility with existing consumers.
|
|
59
|
+
*/
|
|
60
|
+
export const localAdapterCache = defaultDiscoveryCache.adapters;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated Use `defaultDiscoveryCache.projects` instead.
|
|
64
|
+
* Kept for backward compatibility with existing consumers.
|
|
65
|
+
*/
|
|
66
|
+
export const localProjectCache = defaultDiscoveryCache.projects;
|
|
67
|
+
|
|
68
|
+
/** Standard directories to search for local projects */
|
|
69
|
+
export const standardProjectDirs = ["data/projects", "projects"];
|
|
38
70
|
|
|
39
71
|
function isNotFoundError(error: unknown): boolean {
|
|
40
72
|
if (!(error instanceof Error)) return false;
|
|
@@ -78,12 +110,14 @@ async function isValidLocalProjectPath(path: string, adapter: RuntimeAdapter): P
|
|
|
78
110
|
* @param slug - The project slug to find
|
|
79
111
|
* @param adapter - The runtime adapter to use for filesystem operations
|
|
80
112
|
* @param headerPath - Optional path from x-project-path header (takes precedence)
|
|
113
|
+
* @param cache - Optional cache instance (defaults to module-level singleton)
|
|
81
114
|
* @returns The absolute path to the project, or undefined if not found
|
|
82
115
|
*/
|
|
83
116
|
export async function findLocalProjectPath(
|
|
84
117
|
slug: string,
|
|
85
118
|
adapter: RuntimeAdapter,
|
|
86
119
|
headerPath?: string,
|
|
120
|
+
cache: ProjectDiscoveryCache = defaultDiscoveryCache,
|
|
87
121
|
): Promise<string | undefined> {
|
|
88
122
|
if (headerPath) {
|
|
89
123
|
try {
|
|
@@ -92,7 +126,7 @@ export async function findLocalProjectPath(
|
|
|
92
126
|
const absolutePath = normalizedPath.startsWith("/")
|
|
93
127
|
? normalizedPath
|
|
94
128
|
: `${cwd()}/${normalizedPath}`;
|
|
95
|
-
|
|
129
|
+
cache.projects.set(slug, absolutePath);
|
|
96
130
|
return absolutePath;
|
|
97
131
|
}
|
|
98
132
|
logger.warn("Ignoring invalid x-project-path override", {
|
|
@@ -108,7 +142,7 @@ export async function findLocalProjectPath(
|
|
|
108
142
|
}
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
const cached =
|
|
145
|
+
const cached = cache.projects.get(slug);
|
|
112
146
|
if (cached) return cached;
|
|
113
147
|
|
|
114
148
|
for (const dir of standardProjectDirs) {
|
|
@@ -118,7 +152,7 @@ export async function findLocalProjectPath(
|
|
|
118
152
|
if (!await isValidLocalProjectPath(projectPath, adapter)) continue;
|
|
119
153
|
|
|
120
154
|
const absolutePath = projectPath.startsWith("/") ? projectPath : `${cwd()}/${projectPath}`;
|
|
121
|
-
|
|
155
|
+
cache.projects.set(slug, absolutePath);
|
|
122
156
|
logger.debug("Discovered local project", { slug, path: absolutePath });
|
|
123
157
|
return absolutePath;
|
|
124
158
|
} catch (error) {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
startRenderSession,
|
|
16
16
|
} from "../../../transforms/mdx/esm-module-loader/module-fetcher/index.js";
|
|
17
17
|
import { getErrorCollector } from "../../../observability/error-collector.js";
|
|
18
|
-
import { ErrorOverlay } from "../../dev-server/error-overlay/index.js";
|
|
18
|
+
import { ErrorOverlay, parseErrorLocation } from "../../dev-server/error-overlay/index.js";
|
|
19
19
|
import { ErrorPages } from "../../utils/error-html.js";
|
|
20
20
|
import {
|
|
21
21
|
HTTP_INTERNAL_SERVER_ERROR,
|
|
@@ -27,6 +27,32 @@ import type { CacheRepository } from "../../../repositories/types.js";
|
|
|
27
27
|
|
|
28
28
|
const logger = serverLogger.component("ssr-service");
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Provides a renderer for a given handler context.
|
|
32
|
+
* Extracted to allow dependency injection in tests.
|
|
33
|
+
*/
|
|
34
|
+
export interface RendererProvider {
|
|
35
|
+
getRenderer(ctx: HandlerContext): Promise<RendererAdapter>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Minimal interface for SSRService consumers (e.g., SSRHandler).
|
|
40
|
+
* Allows dependency injection and mocking in tests.
|
|
41
|
+
*/
|
|
42
|
+
export interface SSRServiceLike {
|
|
43
|
+
checkMemoryPressure(): MemoryStatus;
|
|
44
|
+
renderPage(ctx: HandlerContext, options: SSRRenderOptions): Promise<SSRRenderResult>;
|
|
45
|
+
createMemoryPressureResult(slug: string): SSRRenderResult;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default RendererProvider that delegates to the real getRendererForProject.
|
|
50
|
+
*/
|
|
51
|
+
const defaultRendererProvider: RendererProvider = {
|
|
52
|
+
getRenderer: (ctx: HandlerContext) =>
|
|
53
|
+
timeAsync("renderer-init", () => getRendererForProject(ctx)),
|
|
54
|
+
};
|
|
55
|
+
|
|
30
56
|
export interface SSRRenderResult {
|
|
31
57
|
status: number;
|
|
32
58
|
html?: string;
|
|
@@ -59,11 +85,16 @@ export interface MemoryStatus {
|
|
|
59
85
|
heapUsedPercent: number;
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
export class SSRService {
|
|
88
|
+
export class SSRService implements SSRServiceLike {
|
|
63
89
|
private readonly cacheRepo?: CacheRepository<string>;
|
|
90
|
+
private readonly rendererProvider: RendererProvider;
|
|
64
91
|
|
|
65
|
-
constructor(options?: {
|
|
92
|
+
constructor(options?: {
|
|
93
|
+
cacheRepo?: CacheRepository<string>;
|
|
94
|
+
rendererProvider?: RendererProvider;
|
|
95
|
+
}) {
|
|
66
96
|
this.cacheRepo = options?.cacheRepo;
|
|
97
|
+
this.rendererProvider = options?.rendererProvider ?? defaultRendererProvider;
|
|
67
98
|
}
|
|
68
99
|
|
|
69
100
|
checkMemoryPressure(): MemoryStatus {
|
|
@@ -78,7 +109,7 @@ export class SSRService {
|
|
|
78
109
|
}
|
|
79
110
|
|
|
80
111
|
async getRenderer(ctx: HandlerContext): Promise<RendererAdapter> {
|
|
81
|
-
return
|
|
112
|
+
return this.rendererProvider.getRenderer(ctx);
|
|
82
113
|
}
|
|
83
114
|
|
|
84
115
|
async renderPage(ctx: HandlerContext, options: SSRRenderOptions): Promise<SSRRenderResult> {
|
|
@@ -240,9 +271,16 @@ export class SSRService {
|
|
|
240
271
|
slug,
|
|
241
272
|
});
|
|
242
273
|
|
|
274
|
+
const sourceFile = (errorObj as Error & { sourceFile?: string }).sourceFile;
|
|
275
|
+
const location = sourceFile ? parseErrorLocation(errorObj, sourceFile) : {};
|
|
243
276
|
return {
|
|
244
277
|
status: HTTP_INTERNAL_SERVER_ERROR,
|
|
245
|
-
html: ErrorOverlay.createHTML({
|
|
278
|
+
html: ErrorOverlay.createHTML({
|
|
279
|
+
error: errorObj,
|
|
280
|
+
type: "runtime",
|
|
281
|
+
...(sourceFile ? { file: sourceFile } : {}),
|
|
282
|
+
...location,
|
|
283
|
+
}, ctx.projectSlug),
|
|
246
284
|
isStreaming: false,
|
|
247
285
|
cacheStrategy: "no-cache",
|
|
248
286
|
error: errorObj,
|
|
@@ -64,11 +64,95 @@ export interface RendererAdapter {
|
|
|
64
64
|
destroy(): Promise<void>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Abstraction over renderer initialization, allowing tests to inject
|
|
69
|
+
* a mock renderer without pulling in the full rendering subsystem.
|
|
70
|
+
*/
|
|
71
|
+
export interface RendererInitializer {
|
|
72
|
+
initialize(options: RendererOptions): Promise<Renderer>;
|
|
73
|
+
isInitialized(): boolean;
|
|
74
|
+
get(): Renderer;
|
|
75
|
+
destroy(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default initializer that delegates to the real shared renderer
|
|
80
|
+
* singleton from `#veryfront/rendering/renderer.ts`.
|
|
81
|
+
*/
|
|
82
|
+
const defaultInitializer: RendererInitializer = {
|
|
83
|
+
initialize: initializeRenderer,
|
|
84
|
+
isInitialized: isRendererInitialized,
|
|
85
|
+
get: getRenderer,
|
|
86
|
+
destroy: destroySharedRenderer,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let activeInitializer: RendererInitializer = defaultInitializer;
|
|
90
|
+
let rendererInitState: { initializer: RendererInitializer; promise: Promise<Renderer> } | null =
|
|
91
|
+
null;
|
|
92
|
+
|
|
93
|
+
function scheduleInitializerDestroy(
|
|
94
|
+
initializer: RendererInitializer,
|
|
95
|
+
pendingPromise?: Promise<unknown>,
|
|
96
|
+
): void {
|
|
97
|
+
const destroy = async () => {
|
|
98
|
+
try {
|
|
99
|
+
await initializer.destroy();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.warn("Failed to destroy renderer initializer", {
|
|
102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (pendingPromise) {
|
|
108
|
+
void pendingPromise
|
|
109
|
+
.catch(() => undefined)
|
|
110
|
+
.then(destroy);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!initializer.isInitialized()) return;
|
|
115
|
+
void destroy();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Replace the renderer initializer used by the adapter layer.
|
|
120
|
+
* Pass `undefined` to restore the default (real) initializer.
|
|
121
|
+
*
|
|
122
|
+
* Returns a disposer that restores the previous initializer — use in
|
|
123
|
+
* `afterEach` or with `using` to prevent test pollution:
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* afterEach(() => setRendererInitializer(undefined));
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @internal Test-only — not part of the public API.
|
|
130
|
+
*/
|
|
131
|
+
export function setRendererInitializer(
|
|
132
|
+
initializer?: RendererInitializer,
|
|
133
|
+
): void {
|
|
134
|
+
const nextInitializer = initializer ?? defaultInitializer;
|
|
135
|
+
const previous = activeInitializer;
|
|
136
|
+
const previousPendingPromise = rendererInitState?.initializer === previous
|
|
137
|
+
? rendererInitState.promise
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
activeInitializer = nextInitializer;
|
|
141
|
+
|
|
142
|
+
if (rendererInitState?.initializer !== activeInitializer) {
|
|
143
|
+
rendererInitState = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (previous !== activeInitializer) {
|
|
147
|
+
scheduleInitializerDestroy(previous, previousPendingPromise);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
68
150
|
|
|
69
151
|
async function getOrInitRenderer(): Promise<Renderer> {
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
152
|
+
if (activeInitializer.isInitialized()) return activeInitializer.get();
|
|
153
|
+
if (rendererInitState?.initializer === activeInitializer) {
|
|
154
|
+
return rendererInitState.promise;
|
|
155
|
+
}
|
|
72
156
|
|
|
73
157
|
const isProxyMode = getEnvBoolean("PROXY_MODE", false, {
|
|
74
158
|
trueValues: ["1"],
|
|
@@ -99,12 +183,19 @@ async function getOrInitRenderer(): Promise<Renderer> {
|
|
|
99
183
|
cacheType: useApiCache ? "api-distributed" : "memory",
|
|
100
184
|
});
|
|
101
185
|
|
|
102
|
-
|
|
186
|
+
const initializer = activeInitializer;
|
|
187
|
+
const initPromise = initializer.initialize(options);
|
|
188
|
+
rendererInitState = {
|
|
189
|
+
initializer,
|
|
190
|
+
promise: initPromise,
|
|
191
|
+
};
|
|
103
192
|
|
|
104
193
|
try {
|
|
105
|
-
return await
|
|
194
|
+
return await initPromise;
|
|
106
195
|
} finally {
|
|
107
|
-
|
|
196
|
+
if (rendererInitState?.promise === initPromise) {
|
|
197
|
+
rendererInitState = null;
|
|
198
|
+
}
|
|
108
199
|
}
|
|
109
200
|
}
|
|
110
201
|
|
|
@@ -308,6 +399,14 @@ export async function getRendererForProject(ctx: HandlerContext): Promise<Render
|
|
|
308
399
|
}
|
|
309
400
|
|
|
310
401
|
export async function destroyRendererAdapter(): Promise<void> {
|
|
311
|
-
|
|
312
|
-
|
|
402
|
+
const pendingPromise = rendererInitState?.initializer === activeInitializer
|
|
403
|
+
? rendererInitState.promise
|
|
404
|
+
: undefined;
|
|
405
|
+
rendererInitState = null;
|
|
406
|
+
|
|
407
|
+
if (pendingPromise) {
|
|
408
|
+
await pendingPromise.catch(() => undefined);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await activeInitializer.destroy();
|
|
313
412
|
}
|
|
@@ -4,5 +4,11 @@
|
|
|
4
4
|
* @module server/shared/renderer
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
destroyRendererAdapter,
|
|
9
|
+
getRendererForProject,
|
|
10
|
+
type RendererAdapter,
|
|
11
|
+
type RendererInitializer,
|
|
12
|
+
setRendererInitializer,
|
|
13
|
+
} from "./adapter.js";
|
|
8
14
|
export { shouldRejectDueToMemory } from "./memory/pressure.js";
|