veryfront 0.0.82 → 0.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +2 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +2 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/multi-tier.d.ts +0 -29
- package/esm/src/cache/multi-tier.d.ts.map +1 -1
- package/esm/src/cache/multi-tier.js +0 -26
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +139 -64
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +3 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/multi-tier.ts +0 -41
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +148 -73
- package/src/src/utils/index.ts +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start Handler - Full TUI dashboard with project discovery
|
|
3
|
+
*
|
|
4
|
+
* Default command when running `veryfront` without arguments.
|
|
5
|
+
* Provides a TUI experience with project navigation and dev server.
|
|
6
|
+
*/
|
|
7
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import { cwd, getEnv } from "../../platform/compat/process.js";
|
|
11
|
+
import { createFileSystem } from "../../platform/compat/fs.js";
|
|
12
|
+
import { isAbsolute, join, resolve } from "../../platform/compat/path/index.js";
|
|
13
|
+
import { cliLogger } from "../../utils/index.js";
|
|
14
|
+
import { exitProcess, registerTerminationSignals } from "../utils/index.js";
|
|
15
|
+
import type { ParsedArgs } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_START_PORT = 8080;
|
|
18
|
+
const DEFAULT_MCP_PORT = 9999;
|
|
19
|
+
|
|
20
|
+
interface DiscoveredProjects {
|
|
21
|
+
projects: Map<string, string>;
|
|
22
|
+
examples: Map<string, string>;
|
|
23
|
+
defaultProject: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getProjectSlug(path: string): string {
|
|
27
|
+
return path.replace(/\/+$/, "").split("/").pop() || "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function isVeryFrontProject(projectPath: string): Promise<boolean> {
|
|
31
|
+
const fs = createFileSystem();
|
|
32
|
+
const markers = ["app", "pages", "components"];
|
|
33
|
+
const checks = await Promise.all(markers.map((m) => fs.exists(join(projectPath, m))));
|
|
34
|
+
return checks.some(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function findProjectsInDirs(baseDirs: string[]): Promise<Map<string, string>> {
|
|
38
|
+
const projects = new Map<string, string>();
|
|
39
|
+
const fs = createFileSystem();
|
|
40
|
+
|
|
41
|
+
for (const baseDir of baseDirs) {
|
|
42
|
+
const absoluteBase = isAbsolute(baseDir) ? baseDir : join(cwd(), baseDir);
|
|
43
|
+
if (!(await fs.exists(absoluteBase))) continue;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
for await (const entry of fs.readDir(absoluteBase)) {
|
|
47
|
+
if (!entry.isDirectory || entry.name.startsWith(".")) continue;
|
|
48
|
+
|
|
49
|
+
const projectPath = join(absoluteBase, entry.name);
|
|
50
|
+
if (await isVeryFrontProject(projectPath)) {
|
|
51
|
+
projects.set(entry.name, resolve(projectPath));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Directory not readable - skip
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return projects;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function discoverProjects(explicitPath: string | null): Promise<DiscoveredProjects> {
|
|
63
|
+
const [projects, examples] = await Promise.all([
|
|
64
|
+
findProjectsInDirs(["data/projects", "projects"]),
|
|
65
|
+
findProjectsInDirs(["examples"]),
|
|
66
|
+
]);
|
|
67
|
+
const fs = createFileSystem();
|
|
68
|
+
let defaultProject: string | null = null;
|
|
69
|
+
|
|
70
|
+
// Add explicit project path if provided
|
|
71
|
+
if (explicitPath) {
|
|
72
|
+
const absolutePath = isAbsolute(explicitPath) ? explicitPath : join(cwd(), explicitPath);
|
|
73
|
+
if (await fs.exists(absolutePath)) {
|
|
74
|
+
const slug = getProjectSlug(absolutePath);
|
|
75
|
+
projects.set(slug, resolve(absolutePath));
|
|
76
|
+
defaultProject = slug;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fall back to current directory if no projects found
|
|
81
|
+
if (projects.size === 0 && !defaultProject) {
|
|
82
|
+
const currentDir = cwd();
|
|
83
|
+
if (await isVeryFrontProject(currentDir)) {
|
|
84
|
+
const slug = getProjectSlug(currentDir);
|
|
85
|
+
projects.set(slug, resolve(currentDir));
|
|
86
|
+
defaultProject = slug;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { projects, examples, defaultProject };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface ProxySetup {
|
|
94
|
+
interceptor: ((req: dntShim.Request) => Promise<dntShim.Request>) | undefined;
|
|
95
|
+
close: () => Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function trySetupProxy(localProjects: Map<string, string>): Promise<ProxySetup> {
|
|
99
|
+
try {
|
|
100
|
+
// Proxy is only available in local dev, not in the npm package
|
|
101
|
+
const { createProxyHandler, injectContextHeaders } = await import(
|
|
102
|
+
"../../../proxy/handler.js"
|
|
103
|
+
);
|
|
104
|
+
const { createCacheFromEnv } = await import("../../../proxy/cache/index.js");
|
|
105
|
+
|
|
106
|
+
const proxyConfig = {
|
|
107
|
+
apiBaseUrl: getEnv("VERYFRONT_API_BASE_URL") || "http://api.lvh.me:4000",
|
|
108
|
+
clientId: getEnv("OAUTH_CLIENT_ID") || "",
|
|
109
|
+
clientSecret: getEnv("OAUTH_CLIENT_SECRET") || "",
|
|
110
|
+
previewClientId: getEnv("OAUTH_PREVIEW_CLIENT_ID") || "",
|
|
111
|
+
previewClientSecret: getEnv("OAUTH_PREVIEW_CLIENT_SECRET") || "",
|
|
112
|
+
apiToken: getEnv("VERYFRONT_API_TOKEN") || "",
|
|
113
|
+
localProjects: Object.fromEntries(localProjects),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const cache = await createCacheFromEnv();
|
|
117
|
+
const handler = createProxyHandler({ config: proxyConfig, cache });
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
interceptor: async (req: dntShim.Request) =>
|
|
121
|
+
injectContextHeaders(req, await handler.processRequest(req)),
|
|
122
|
+
close: () => handler.close(),
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return { interceptor: undefined, close: async () => {} };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function handleStartCommand(args: ParsedArgs): Promise<void> {
|
|
130
|
+
const port = typeof args.port === "number" ? args.port : DEFAULT_START_PORT;
|
|
131
|
+
const mcpPort = typeof args["mcp-port"] === "number" ? args["mcp-port"] : DEFAULT_MCP_PORT;
|
|
132
|
+
const projectPath = args.project ? String(args.project) : null;
|
|
133
|
+
const headless = Boolean(args.headless || args["no-tui"]);
|
|
134
|
+
|
|
135
|
+
const { createApp, showStartup } = await import("../app/index.js");
|
|
136
|
+
const discovered = await discoverProjects(projectPath);
|
|
137
|
+
|
|
138
|
+
const app = createApp({
|
|
139
|
+
port,
|
|
140
|
+
mcpPort,
|
|
141
|
+
headless,
|
|
142
|
+
projects: discovered.projects,
|
|
143
|
+
examples: discovered.examples,
|
|
144
|
+
defaultProject: discovered.defaultProject ?? undefined,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const restoreConsole = app.interceptConsole();
|
|
148
|
+
|
|
149
|
+
if (!headless) {
|
|
150
|
+
await showStartup(["Loading configuration", "Discovering projects", "Starting server"]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const allProjects = new Map([...discovered.projects, ...discovered.examples]);
|
|
154
|
+
const proxy = await trySetupProxy(allProjects);
|
|
155
|
+
|
|
156
|
+
const { createDevServer } = await import("../../server/dev-server.js");
|
|
157
|
+
const shutdownController = new AbortController();
|
|
158
|
+
|
|
159
|
+
const devServer = await createDevServer({
|
|
160
|
+
port,
|
|
161
|
+
projectDir: cwd(),
|
|
162
|
+
hmrPort: port + 1,
|
|
163
|
+
enableHMR: true,
|
|
164
|
+
enableFastRefresh: true,
|
|
165
|
+
signal: shutdownController.signal,
|
|
166
|
+
requestInterceptor: proxy.interceptor,
|
|
167
|
+
});
|
|
168
|
+
await devServer.ready;
|
|
169
|
+
|
|
170
|
+
const { createMCPServer } = await import("../mcp/index.js");
|
|
171
|
+
const mcpServer = await createMCPServer({ httpPort: mcpPort });
|
|
172
|
+
|
|
173
|
+
app.setServerReady();
|
|
174
|
+
|
|
175
|
+
let shuttingDown = false;
|
|
176
|
+
async function shutdown(): Promise<void> {
|
|
177
|
+
if (shuttingDown) return;
|
|
178
|
+
shuttingDown = true;
|
|
179
|
+
|
|
180
|
+
restoreConsole();
|
|
181
|
+
cliLogger.info("Shutting down...");
|
|
182
|
+
|
|
183
|
+
app.stop();
|
|
184
|
+
await mcpServer.stop();
|
|
185
|
+
shutdownController.abort();
|
|
186
|
+
await devServer.stop();
|
|
187
|
+
await proxy.close();
|
|
188
|
+
exitProcess(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
registerTerminationSignals(() => void shutdown());
|
|
192
|
+
app.start();
|
|
193
|
+
|
|
194
|
+
await new Promise(() => {});
|
|
195
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Module for Dev Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes dev server functionality via MCP (Model Context Protocol)
|
|
5
|
+
* for coding agents like Claude Code and Cursor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from "./server.js";
|
|
9
|
+
export * from "./error-collector.js";
|
|
10
|
+
export * from "./log-buffer.js";
|
|
11
|
+
export * from "./tools.js";
|
|
@@ -19,6 +19,7 @@ export interface RedisRateLimitOptions {
|
|
|
19
19
|
|
|
20
20
|
export class RedisRateLimitStore implements RateLimitStore {
|
|
21
21
|
private client: RedisClient | null = null;
|
|
22
|
+
private clientPromise: Promise<RedisClient> | null = null;
|
|
22
23
|
private readonly url?: string;
|
|
23
24
|
private readonly keyPrefix: string;
|
|
24
25
|
|
|
@@ -27,9 +28,15 @@ export class RedisRateLimitStore implements RateLimitStore {
|
|
|
27
28
|
this.keyPrefix = options.keyPrefix ?? "veryfront:ratelimit:";
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
private
|
|
31
|
-
if (this.client) return this.client;
|
|
31
|
+
private ensureClient(): Promise<RedisClient> {
|
|
32
|
+
if (this.client) return Promise.resolve(this.client);
|
|
33
|
+
if (this.clientPromise) return this.clientPromise;
|
|
32
34
|
|
|
35
|
+
this.clientPromise = this.connectClient();
|
|
36
|
+
return this.clientPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async connectClient(): Promise<RedisClient> {
|
|
33
40
|
let createClient: ((options: { url?: string }) => RedisClient) | undefined;
|
|
34
41
|
|
|
35
42
|
try {
|
|
@@ -37,6 +44,7 @@ export class RedisRateLimitStore implements RateLimitStore {
|
|
|
37
44
|
const mod = await import(redisClientModule);
|
|
38
45
|
createClient = mod.createClient as (options: { url?: string }) => RedisClient;
|
|
39
46
|
} catch {
|
|
47
|
+
this.clientPromise = null;
|
|
40
48
|
throw toError(
|
|
41
49
|
createError({
|
|
42
50
|
type: "config",
|
|
@@ -46,15 +54,20 @@ export class RedisRateLimitStore implements RateLimitStore {
|
|
|
46
54
|
);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
try {
|
|
58
|
+
const client = createClient({ url: this.url });
|
|
59
|
+
|
|
60
|
+
client.on?.("error", (err: unknown) => {
|
|
61
|
+
logger.error("[redis-ratelimit] client error", err);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await client.connect();
|
|
65
|
+
this.client = client;
|
|
66
|
+
return client;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.clientPromise = null;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
private storageKey(key: string): string {
|
|
@@ -1,59 +1,38 @@
|
|
|
1
1
|
/** Redis caching for cross-pod SSR module sharing */
|
|
2
2
|
|
|
3
3
|
import { rendererLogger as logger } from "../../../../utils/index.js";
|
|
4
|
-
import {
|
|
5
|
-
getRedisClient,
|
|
6
|
-
isRedisConfigured,
|
|
7
|
-
type RedisClient,
|
|
8
|
-
} from "../../../../utils/redis-client.js";
|
|
4
|
+
import { type RedisClient } from "../../../../utils/redis-client.js";
|
|
9
5
|
import { buildRedisSSRModuleKey } from "../../../../cache/index.js";
|
|
10
6
|
import { getSSRModuleRedisTTL } from "../constants.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
import { CacheBackends, createDistributedCacheAccessor } from "../../../../cache/backend.js";
|
|
8
|
+
|
|
9
|
+
/** Lazy-loaded distributed cache backend for cross-pod sharing */
|
|
10
|
+
const getDistributedCache = createDistributedCacheAccessor(
|
|
11
|
+
() => CacheBackends.ssrModule(),
|
|
12
|
+
"SSR-MODULE-LOADER",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Legacy key builder. CacheBackend handles prefixing internally.
|
|
17
|
+
* Used only for backward compatibility if needed.
|
|
18
|
+
*/
|
|
17
19
|
export function redisKey(key: string): string {
|
|
18
20
|
return buildRedisSSRModuleKey(key);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** Initialize distributed caching for SSR modules */
|
|
22
24
|
export async function initializeSSRDistributedCache(): Promise<boolean> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (redisInitPromise) {
|
|
26
|
-
await redisInitPromise;
|
|
27
|
-
return redisEnabled;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
redisInitPromise = (async () => {
|
|
31
|
-
if (!isRedisConfigured()) {
|
|
32
|
-
logger.debug("[SSR-MODULE-LOADER] Redis not configured, using memory cache");
|
|
33
|
-
redisInitialized = true;
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
redisClient = await getRedisClient();
|
|
39
|
-
redisEnabled = true;
|
|
40
|
-
logger.debug("[SSR-MODULE-LOADER] Redis cache enabled");
|
|
41
|
-
} catch (error) {
|
|
42
|
-
logger.warn("[SSR-MODULE-LOADER] Redis unavailable, falling back to memory cache", { error });
|
|
43
|
-
redisEnabled = false;
|
|
44
|
-
} finally {
|
|
45
|
-
redisInitialized = true;
|
|
46
|
-
}
|
|
47
|
-
})();
|
|
48
|
-
|
|
49
|
-
await redisInitPromise;
|
|
50
|
-
redisInitPromise = null;
|
|
51
|
-
|
|
52
|
-
return redisEnabled;
|
|
25
|
+
const backend = await getDistributedCache();
|
|
26
|
+
return backend !== null;
|
|
53
27
|
}
|
|
54
28
|
|
|
29
|
+
/** Check if distributed caching is enabled for SSR modules */
|
|
55
30
|
export function isSSRDistributedCacheEnabled(): boolean {
|
|
56
|
-
|
|
31
|
+
// We can't synchronously check if backend is initialized without accessing the promise
|
|
32
|
+
// But we can check if we *should* be enabled based on env via CacheBackend utils
|
|
33
|
+
// For now, this returns true because it's used as a guard for get/set calls
|
|
34
|
+
// which themselves are async and handle missing backends gracefully.
|
|
35
|
+
return true;
|
|
57
36
|
}
|
|
58
37
|
|
|
59
38
|
/** @deprecated Use initializeSSRDistributedCache instead */
|
|
@@ -62,21 +41,27 @@ export const initializeSSRRedisCache = initializeSSRDistributedCache;
|
|
|
62
41
|
/** @deprecated Use isSSRDistributedCacheEnabled instead */
|
|
63
42
|
export const isSSRRedisCacheEnabled = isSSRDistributedCacheEnabled;
|
|
64
43
|
|
|
44
|
+
/** @deprecated Use isSSRDistributedCacheEnabled instead */
|
|
65
45
|
export function getRedisEnabled(): boolean {
|
|
66
|
-
return
|
|
46
|
+
return isSSRDistributedCacheEnabled();
|
|
67
47
|
}
|
|
68
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated Direct Redis client access is deprecated. Use CacheBackend abstraction.
|
|
51
|
+
* Returns null to force use of CacheBackend path in updated consumers.
|
|
52
|
+
*/
|
|
69
53
|
export function getRedisClientInstance(): RedisClient | null {
|
|
70
|
-
return
|
|
54
|
+
return null;
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
export async function getFromRedis(cacheKey: string): Promise<string | null> {
|
|
74
|
-
|
|
58
|
+
const backend = await getDistributedCache();
|
|
59
|
+
if (!backend) return null;
|
|
75
60
|
|
|
76
61
|
try {
|
|
77
|
-
return await
|
|
62
|
+
return await backend.get(cacheKey);
|
|
78
63
|
} catch (error) {
|
|
79
|
-
logger.debug("[SSR-MODULE-LOADER]
|
|
64
|
+
logger.debug("[SSR-MODULE-LOADER] Distributed cache get failed", { key: cacheKey, error });
|
|
80
65
|
return null;
|
|
81
66
|
}
|
|
82
67
|
}
|
|
@@ -87,13 +72,14 @@ export async function setInRedis(
|
|
|
87
72
|
code: string,
|
|
88
73
|
options?: { isProduction?: boolean; ttlSeconds?: number },
|
|
89
74
|
): Promise<void> {
|
|
90
|
-
|
|
75
|
+
const backend = await getDistributedCache();
|
|
76
|
+
if (!backend) return;
|
|
91
77
|
|
|
92
78
|
const ttl = options?.ttlSeconds ?? getSSRModuleRedisTTL(options?.isProduction ?? true);
|
|
93
79
|
|
|
94
80
|
try {
|
|
95
|
-
await
|
|
81
|
+
await backend.set(cacheKey, code, ttl);
|
|
96
82
|
} catch (error) {
|
|
97
|
-
logger.debug("[SSR-MODULE-LOADER]
|
|
83
|
+
logger.debug("[SSR-MODULE-LOADER] Distributed cache set failed", { key: cacheKey, error });
|
|
98
84
|
}
|
|
99
85
|
}
|
|
@@ -40,18 +40,18 @@ import { withTimeoutThrow } from "../../../rendering/utils/stream-utils.js";
|
|
|
40
40
|
import {
|
|
41
41
|
failedComponents,
|
|
42
42
|
getFromRedis,
|
|
43
|
-
getRedisClientInstance,
|
|
44
|
-
getRedisEnabled,
|
|
45
43
|
globalCrossProjectCache,
|
|
46
44
|
globalInProgress,
|
|
47
45
|
globalModuleCache,
|
|
48
46
|
globalTmpDirs,
|
|
47
|
+
isSSRDistributedCacheEnabled,
|
|
49
48
|
setInRedis,
|
|
50
49
|
transformSemaphore,
|
|
51
50
|
} from "./cache/index.js";
|
|
52
51
|
import type { ModuleCacheEntry, SSRModuleLoaderOptions } from "./types.js";
|
|
53
52
|
import { getCacheBaseDir, getHttpBundleCacheDir } from "../../../utils/cache-dir.js";
|
|
54
53
|
import { ensureHttpBundlesExist } from "../../../transforms/esm/http-cache.js";
|
|
54
|
+
import { LRUCache } from "../../../utils/lru-wrapper.js";
|
|
55
55
|
|
|
56
56
|
/** Pattern to match HTTP bundle file:// paths in transformed code */
|
|
57
57
|
const HTTP_BUNDLE_PATTERN = /file:\/\/([^"'\s]+veryfront-http-bundle\/http-([a-f0-9]+)\.mjs)/gi;
|
|
@@ -73,8 +73,12 @@ function extractHttpBundlePaths(code: string): Array<{ path: string; hash: strin
|
|
|
73
73
|
return bundles;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Track modules whose HTTP bundles have been verified, keyed by tempPath:contentHash.
|
|
78
|
+
* Bounded LRU to prevent unbounded memory growth in long-running pods.
|
|
79
|
+
* Keying by contentHash ensures verification is re-done when content changes at the same path.
|
|
80
|
+
*/
|
|
81
|
+
const verifiedHttpBundlePaths = new LRUCache<string, true>({ maxEntries: 2000 });
|
|
78
82
|
|
|
79
83
|
/**
|
|
80
84
|
* SSR Module Loader with Redis Support.
|
|
@@ -430,8 +434,9 @@ export class SSRModuleLoader {
|
|
|
430
434
|
|
|
431
435
|
const cachedEntry = globalModuleCache.get(contentCacheKey);
|
|
432
436
|
if (cachedEntry) {
|
|
433
|
-
// Verify HTTP bundles exist for in-memory cached transforms (once per path)
|
|
434
|
-
|
|
437
|
+
// Verify HTTP bundles exist for in-memory cached transforms (once per path+content)
|
|
438
|
+
const verifyKey = `${cachedEntry.tempPath}:${cachedEntry.contentHash}`;
|
|
439
|
+
if (!verifiedHttpBundlePaths.get(verifyKey)) {
|
|
435
440
|
try {
|
|
436
441
|
const cachedCode = await this.fs.readTextFile(cachedEntry.tempPath);
|
|
437
442
|
const bundlePaths = extractHttpBundlePaths(cachedCode);
|
|
@@ -450,10 +455,10 @@ export class SSRModuleLoader {
|
|
|
450
455
|
globalModuleCache.delete(filePathCacheKey);
|
|
451
456
|
// Fall through to Redis or fresh transform
|
|
452
457
|
} else {
|
|
453
|
-
verifiedHttpBundlePaths.
|
|
458
|
+
verifiedHttpBundlePaths.set(verifyKey, true);
|
|
454
459
|
}
|
|
455
460
|
} else {
|
|
456
|
-
verifiedHttpBundlePaths.
|
|
461
|
+
verifiedHttpBundlePaths.set(verifyKey, true);
|
|
457
462
|
}
|
|
458
463
|
} catch {
|
|
459
464
|
// File doesn't exist or unreadable, invalidate cache
|
|
@@ -470,9 +475,7 @@ export class SSRModuleLoader {
|
|
|
470
475
|
}
|
|
471
476
|
}
|
|
472
477
|
|
|
473
|
-
|
|
474
|
-
const redisClient = getRedisClientInstance();
|
|
475
|
-
if (redisEnabled && redisClient) {
|
|
478
|
+
if (isSSRDistributedCacheEnabled()) {
|
|
476
479
|
const redisCode = await getFromRedis(contentCacheKey);
|
|
477
480
|
if (redisCode) {
|
|
478
481
|
// Proactively ensure HTTP bundles exist before using cached transform.
|
|
@@ -501,7 +504,7 @@ export class SSRModuleLoader {
|
|
|
501
504
|
recursive: true,
|
|
502
505
|
});
|
|
503
506
|
await this.fs.writeTextFile(tempPath, redisCode);
|
|
504
|
-
verifiedHttpBundlePaths.
|
|
507
|
+
verifiedHttpBundlePaths.set(`${tempPath}:${contentHash}`, true);
|
|
505
508
|
|
|
506
509
|
const entry: ModuleCacheEntry = { tempPath, contentHash };
|
|
507
510
|
globalModuleCache.set(contentCacheKey, entry);
|
|
@@ -633,6 +636,22 @@ export class SSRModuleLoader {
|
|
|
633
636
|
// This ensures that each content version uses its own cached module
|
|
634
637
|
transformed = this.rewriteLocalImports(transformed, localImportPaths, filePath);
|
|
635
638
|
|
|
639
|
+
// Ensure HTTP bundles exist for this transform (handles nested bundle deps)
|
|
640
|
+
const bundlePaths = extractHttpBundlePaths(transformed);
|
|
641
|
+
if (bundlePaths.length > 0) {
|
|
642
|
+
const cacheDir = getHttpBundleCacheDir();
|
|
643
|
+
const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
|
|
644
|
+
if (failed.length > 0) {
|
|
645
|
+
logger.warn(
|
|
646
|
+
"[SSR-MODULE-LOADER] Some HTTP bundles could not be recovered",
|
|
647
|
+
{
|
|
648
|
+
file: filePath.slice(-40),
|
|
649
|
+
failed,
|
|
650
|
+
},
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
636
655
|
// Hash the TRANSFORMED content (after import rewrites) for cache busting
|
|
637
656
|
// This ensures Deno's module cache is invalidated when dependencies change
|
|
638
657
|
const transformedHash = await this.hashContentAsync(transformed);
|
|
@@ -641,10 +660,15 @@ export class SSRModuleLoader {
|
|
|
641
660
|
await this.fs.mkdir(tempPath.substring(0, tempPath.lastIndexOf("/")), { recursive: true });
|
|
642
661
|
await this.fs.writeTextFile(tempPath, transformed);
|
|
643
662
|
|
|
644
|
-
if (
|
|
663
|
+
if (isSSRDistributedCacheEnabled()) {
|
|
645
664
|
setInRedis(contentCacheKey, transformed, {
|
|
646
665
|
isProduction: this.isProductionContentSource(),
|
|
647
|
-
}).catch(() => {
|
|
666
|
+
}).catch((error) => {
|
|
667
|
+
logger.debug("[SSR-MODULE-LOADER] Distributed cache set failed", {
|
|
668
|
+
key: contentCacheKey,
|
|
669
|
+
error,
|
|
670
|
+
});
|
|
671
|
+
});
|
|
648
672
|
}
|
|
649
673
|
|
|
650
674
|
// Use transformedHash for cache busting in dynamic imports
|
|
@@ -221,7 +221,9 @@ export class FileCache {
|
|
|
221
221
|
const serialized = JSON.stringify(entry);
|
|
222
222
|
// Update request-scoped cache so subsequent reads in same request see the new value
|
|
223
223
|
setInRequestCache(key, serialized);
|
|
224
|
-
backend.set(key, serialized, BACKEND_TTL_SECONDS).catch(() => {
|
|
224
|
+
backend.set(key, serialized, BACKEND_TTL_SECONDS).catch((error) => {
|
|
225
|
+
logger.debug("[FileCache] Backend set failed", { key, error });
|
|
226
|
+
});
|
|
225
227
|
return;
|
|
226
228
|
}
|
|
227
229
|
|
|
@@ -308,7 +310,9 @@ export class FileCache {
|
|
|
308
310
|
|
|
309
311
|
// Fire-and-forget backend deletion
|
|
310
312
|
// Note: prefix already includes "file:" from buildFileCacheKeyPrefix, don't add it again
|
|
311
|
-
cacheBackend?.delByPattern?.(`${prefix}*`).catch(() => {
|
|
313
|
+
cacheBackend?.delByPattern?.(`${prefix}*`).catch((error) => {
|
|
314
|
+
logger.debug("[FileCache] Backend invalidation failed", { prefix, error });
|
|
315
|
+
});
|
|
312
316
|
|
|
313
317
|
return count;
|
|
314
318
|
}
|
|
@@ -346,7 +350,9 @@ export class FileCache {
|
|
|
346
350
|
|
|
347
351
|
// Fire-and-forget backend deletion
|
|
348
352
|
// Note: prefix already includes "file:" from buildFileCacheKeyPrefix, don't add it again
|
|
349
|
-
cacheBackend?.delByPattern?.(`${prefix}*:${suffix}`).catch(() => {
|
|
353
|
+
cacheBackend?.delByPattern?.(`${prefix}*:${suffix}`).catch((error) => {
|
|
354
|
+
logger.debug("[FileCache] Backend invalidation failed", { prefix, suffix, error });
|
|
355
|
+
});
|
|
350
356
|
|
|
351
357
|
return count;
|
|
352
358
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
clearModulePathCache,
|
|
4
4
|
invalidateModulePaths,
|
|
5
5
|
} from "../../transforms/mdx/esm-module-loader/index.js";
|
|
6
|
+
import { clearModuleCacheForProject } from "../../cache/module-cache.js";
|
|
6
7
|
import {
|
|
7
8
|
clearSSRModuleCache,
|
|
8
9
|
clearSSRModuleCacheForProject,
|
|
@@ -63,6 +64,9 @@ export async function invalidateProjectCaches(
|
|
|
63
64
|
});
|
|
64
65
|
if (projectId) {
|
|
65
66
|
clearSSRModuleCacheForProject(projectId);
|
|
67
|
+
// Also clear the pod-level module cache (used by RenderPipeline)
|
|
68
|
+
// This was previously missed, causing stale renders despite SSR module cache clearing
|
|
69
|
+
clearModuleCacheForProject(projectId);
|
|
66
70
|
} else {
|
|
67
71
|
clearSSRModuleCache();
|
|
68
72
|
}
|
|
@@ -360,6 +360,7 @@ async function handleListFiles(req: dntShim.Request, ctx: HandlerContext): Promi
|
|
|
360
360
|
if (!projectDir) return errorResponse("No project directory configured", 500);
|
|
361
361
|
|
|
362
362
|
const relativePath = new URL(req.url).searchParams.get("path") || "";
|
|
363
|
+
if (relativePath.includes("..")) return errorResponse("Invalid path", 400);
|
|
363
364
|
const fullPath = relativePath ? `${projectDir}/${relativePath}` : projectDir;
|
|
364
365
|
|
|
365
366
|
try {
|
|
@@ -394,6 +395,7 @@ async function handleReadFileContent(req: dntShim.Request, ctx: HandlerContext):
|
|
|
394
395
|
|
|
395
396
|
const relativePath = new URL(req.url).searchParams.get("path") || "";
|
|
396
397
|
if (!relativePath) return errorResponse("path parameter is required", 400);
|
|
398
|
+
if (relativePath.includes("..")) return errorResponse("Invalid path", 400);
|
|
397
399
|
|
|
398
400
|
try {
|
|
399
401
|
const content = await adapter.fs.readFile(`${projectDir}/${relativePath}`);
|
|
@@ -83,6 +83,12 @@ export function handleProjectsUI(req: dntShim.Request): Promise<dntShim.Response
|
|
|
83
83
|
"server.dev.projectsUI.handle",
|
|
84
84
|
async () => {
|
|
85
85
|
const relativePath = pathname.replace("/_projects/ui/", "").replace(/\.js$/, "");
|
|
86
|
+
if (relativePath.includes("..")) {
|
|
87
|
+
return new dntShim.Response("Invalid path", {
|
|
88
|
+
status: 400,
|
|
89
|
+
headers: { "Content-Type": "text/plain" },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
86
92
|
const uiDir = getUiDirectory();
|
|
87
93
|
|
|
88
94
|
const module = await readUiSource(uiDir, relativePath);
|