veryfront 0.1.26 → 0.1.28
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 +3 -11
- package/esm/cli/app/shell.d.ts.map +1 -1
- package/esm/cli/app/shell.js +9 -5
- package/esm/cli/commands/demo/demo.js +1 -1
- package/esm/cli/commands/init/catalog.d.ts.map +1 -1
- package/esm/cli/commands/init/catalog.js +13 -5
- package/esm/cli/commands/init/command-help.js +4 -4
- package/esm/cli/commands/init/types.d.ts +1 -1
- package/esm/cli/commands/init/types.d.ts.map +1 -1
- package/esm/cli/commands/serve/command.d.ts.map +1 -1
- package/esm/cli/commands/serve/command.js +0 -4
- package/esm/cli/commands/start/command.d.ts.map +1 -1
- package/esm/cli/commands/start/command.js +16 -9
- package/esm/cli/help/tips.js +6 -6
- package/esm/cli/mcp/remote-file-tools.js +1 -1
- package/esm/cli/mcp/tools/catalog-tools.d.ts +3 -3
- package/esm/cli/mcp/tools/catalog-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/catalog-tools.js +21 -13
- package/esm/cli/mcp/tools/project-tools.js +1 -1
- package/esm/cli/templates/index.js +11 -11
- package/esm/cli/templates/manifest.d.ts +22 -15
- package/esm/cli/templates/manifest.js +24 -17
- package/esm/cli/templates/types.d.ts +1 -1
- package/esm/cli/templates/types.d.ts.map +1 -1
- package/esm/cli/utils/index.d.ts.map +1 -1
- package/esm/cli/utils/index.js +13 -1
- 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 +2 -0
- package/esm/src/html/styles-builder/project-css-cache.d.ts +8 -1
- package/esm/src/html/styles-builder/project-css-cache.d.ts.map +1 -1
- package/esm/src/html/styles-builder/project-css-cache.js +13 -2
- package/esm/src/html/styles-builder/tailwind-compiler.d.ts +2 -0
- package/esm/src/html/styles-builder/tailwind-compiler.d.ts.map +1 -1
- package/esm/src/html/styles-builder/tailwind-compiler.js +52 -19
- package/esm/src/modules/react-loader/css-import-collector.d.ts +29 -0
- package/esm/src/modules/react-loader/css-import-collector.d.ts.map +1 -0
- package/esm/src/modules/react-loader/css-import-collector.js +41 -0
- 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 +6 -0
- package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.js +5 -0
- package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/factory.js +5 -1
- package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +1 -0
- package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +19 -5
- package/esm/src/platform/compat/process.d.ts.map +1 -1
- package/esm/src/platform/compat/process.js +20 -3
- package/esm/src/proxy/main.js +31 -12
- package/esm/src/proxy/token-manager.d.ts +2 -0
- package/esm/src/proxy/token-manager.d.ts.map +1 -1
- package/esm/src/proxy/token-manager.js +47 -8
- package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +23 -0
- package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -0
- package/esm/src/rendering/orchestrator/css-candidate-manifest.js +132 -0
- package/esm/src/rendering/orchestrator/html.d.ts +11 -1
- package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/html.js +103 -18
- package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +14 -2
- package/esm/src/server/bootstrap.d.ts +2 -0
- package/esm/src/server/bootstrap.d.ts.map +1 -1
- package/esm/src/server/bootstrap.js +10 -0
- package/esm/src/server/handlers/preview/markdown-html-generator.d.ts.map +1 -1
- package/esm/src/server/handlers/preview/markdown-html-generator.js +11 -5
- package/esm/src/server/production-server.js +10 -2
- package/esm/src/studio/bridge-template.d.ts +2 -0
- package/esm/src/studio/bridge-template.d.ts.map +1 -1
- package/esm/src/studio/bridge-template.js +3390 -52
- package/esm/src/transforms/css-modules/naming.d.ts +33 -0
- package/esm/src/transforms/css-modules/naming.d.ts.map +1 -0
- package/esm/src/transforms/css-modules/naming.js +128 -0
- package/esm/src/transforms/esm/import-parser.d.ts +1 -0
- package/esm/src/transforms/esm/import-parser.d.ts.map +1 -1
- package/esm/src/transforms/esm/import-parser.js +16 -5
- package/esm/src/transforms/pipeline/index.d.ts.map +1 -1
- package/esm/src/transforms/pipeline/index.js +3 -1
- package/esm/src/transforms/pipeline/stages/index.d.ts +1 -0
- package/esm/src/transforms/pipeline/stages/index.d.ts.map +1 -1
- package/esm/src/transforms/pipeline/stages/index.js +1 -0
- package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts +18 -0
- package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts.map +1 -0
- package/esm/src/transforms/pipeline/stages/ssr-css-strip.js +168 -0
- package/package.json +1 -1
- package/src/cli/app/shell.ts +9 -5
- package/src/cli/commands/demo/demo.ts +1 -1
- package/src/cli/commands/init/catalog.ts +13 -5
- package/src/cli/commands/init/command-help.ts +4 -4
- package/src/cli/commands/init/types.ts +5 -5
- package/src/cli/commands/serve/command.ts +0 -5
- package/src/cli/commands/start/command.ts +15 -10
- package/src/cli/help/tips.ts +6 -6
- package/src/cli/mcp/remote-file-tools.ts +1 -1
- package/src/cli/mcp/tools/catalog-tools.ts +21 -13
- package/src/cli/mcp/tools/project-tools.ts +1 -1
- package/src/cli/templates/index.ts +11 -11
- package/src/cli/templates/manifest.js +24 -17
- package/src/cli/templates/types.ts +5 -5
- package/src/cli/utils/index.ts +12 -1
- package/src/deno.js +1 -1
- package/src/src/html/html-shell-generator.ts +2 -0
- package/src/src/html/styles-builder/project-css-cache.ts +24 -1
- package/src/src/html/styles-builder/tailwind-compiler.ts +67 -26
- package/src/src/modules/react-loader/css-import-collector.ts +50 -0
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +7 -0
- package/src/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.ts +6 -0
- package/src/src/platform/adapters/fs/factory.ts +5 -1
- package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +21 -5
- package/src/src/platform/compat/process.ts +28 -4
- package/src/src/proxy/main.ts +32 -12
- package/src/src/proxy/token-manager.ts +54 -8
- package/src/src/rendering/orchestrator/css-candidate-manifest.ts +176 -0
- package/src/src/rendering/orchestrator/html.ts +128 -16
- package/src/src/rendering/orchestrator/pipeline.ts +183 -165
- package/src/src/server/bootstrap.ts +16 -0
- package/src/src/server/handlers/preview/markdown-html-generator.ts +12 -5
- package/src/src/server/production-server.ts +12 -2
- package/src/src/studio/bridge-template.ts +3392 -52
- package/src/src/transforms/css-modules/naming.ts +152 -0
- package/src/src/transforms/esm/import-parser.ts +15 -5
- package/src/src/transforms/pipeline/index.ts +3 -0
- package/src/src/transforms/pipeline/stages/index.ts +1 -0
- package/src/src/transforms/pipeline/stages/ssr-css-strip.ts +201 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveStylesheet,
|
|
19
19
|
} from "./tailwind-compiler-utils.js";
|
|
20
20
|
import { cacheCSSAsync, DEFAULT_STYLESHEET } from "./css-hash-cache.js";
|
|
21
|
+
import { TAILWIND_VERSION } from "../../utils/constants/cdn.js";
|
|
21
22
|
|
|
22
23
|
const projectCssCacheLog = logger.component("project-css-cache");
|
|
23
24
|
const tailwindLog = logger.component("tailwind");
|
|
@@ -40,9 +41,17 @@ export interface ProjectCSSRequestContext {
|
|
|
40
41
|
projectSlug: string;
|
|
41
42
|
stylesheet: string;
|
|
42
43
|
candidatesHash: string;
|
|
44
|
+
profileHash: string;
|
|
45
|
+
environment: string;
|
|
43
46
|
cacheKey: string;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export interface ProjectCSSProfile {
|
|
50
|
+
minify?: boolean;
|
|
51
|
+
environment?: string;
|
|
52
|
+
buildMode?: "development" | "production";
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
// ============================================================================
|
|
47
56
|
// Constants
|
|
48
57
|
// ============================================================================
|
|
@@ -116,15 +125,29 @@ export function createProjectCSSRequestContext(
|
|
|
116
125
|
projectSlug: string,
|
|
117
126
|
stylesheet: string | undefined,
|
|
118
127
|
candidates: Set<string>,
|
|
128
|
+
profile?: ProjectCSSProfile,
|
|
119
129
|
): ProjectCSSRequestContext {
|
|
120
130
|
const resolvedStylesheet = resolveStylesheet(stylesheet, DEFAULT_STYLESHEET);
|
|
121
131
|
const stylesheetHash = hashString(resolvedStylesheet);
|
|
122
132
|
const candidatesHash = hashCandidates(candidates);
|
|
133
|
+
const environment = profile?.environment ?? "preview";
|
|
134
|
+
const profileHash = hashString(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
cacheSchema: "v2",
|
|
137
|
+
tailwindVersion: TAILWIND_VERSION,
|
|
138
|
+
minify: profile?.minify ?? false,
|
|
139
|
+
buildMode: profile?.buildMode ?? "production",
|
|
140
|
+
environment,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
123
144
|
return {
|
|
124
145
|
projectSlug,
|
|
125
146
|
stylesheet: resolvedStylesheet,
|
|
126
147
|
candidatesHash,
|
|
127
|
-
|
|
148
|
+
profileHash,
|
|
149
|
+
environment,
|
|
150
|
+
cacheKey: `${projectSlug}:${environment}:${stylesheetHash}:${candidatesHash}:${profileHash}`,
|
|
128
151
|
};
|
|
129
152
|
}
|
|
130
153
|
|
|
@@ -43,6 +43,11 @@ export {
|
|
|
43
43
|
} from "./project-css-cache.js";
|
|
44
44
|
|
|
45
45
|
const logger = serverLogger.component("tailwind");
|
|
46
|
+
const inFlightProjectCSS = new Map<
|
|
47
|
+
string,
|
|
48
|
+
Promise<{ css: string; hash: string; fromCache: boolean }>
|
|
49
|
+
>();
|
|
50
|
+
const inFlightRegeneration = new Map<string, Promise<string | undefined>>();
|
|
46
51
|
|
|
47
52
|
export interface TailwindResult {
|
|
48
53
|
css: string;
|
|
@@ -51,6 +56,8 @@ export interface TailwindResult {
|
|
|
51
56
|
|
|
52
57
|
export interface GenerateOptions {
|
|
53
58
|
minify?: boolean;
|
|
59
|
+
environment?: string;
|
|
60
|
+
buildMode?: "development" | "production";
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
export interface CSSErrorInfo {
|
|
@@ -69,7 +76,11 @@ export async function getProjectCSS(
|
|
|
69
76
|
candidates: Set<string>,
|
|
70
77
|
options?: GenerateOptions,
|
|
71
78
|
): Promise<{ css: string; hash: string; fromCache: boolean }> {
|
|
72
|
-
const context = createProjectCSSRequestContext(projectSlug, stylesheet, candidates
|
|
79
|
+
const context = createProjectCSSRequestContext(projectSlug, stylesheet, candidates, {
|
|
80
|
+
minify: options?.minify,
|
|
81
|
+
environment: options?.environment,
|
|
82
|
+
buildMode: options?.buildMode,
|
|
83
|
+
});
|
|
73
84
|
|
|
74
85
|
const localHit = await tryGetProjectCSSFromLocalFallback(context, candidates);
|
|
75
86
|
if (localHit) return localHit;
|
|
@@ -81,36 +92,55 @@ export async function getProjectCSS(
|
|
|
81
92
|
const distributedHit = await tryGetProjectCSSFromDistributedCache(context, candidates);
|
|
82
93
|
if (distributedHit) return distributedHit;
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (result.error) {
|
|
88
|
-
const formatted = formatCSSError(result.error);
|
|
89
|
-
logger.error("Project CSS generation failed", {
|
|
95
|
+
const inFlight = inFlightProjectCSS.get(context.cacheKey);
|
|
96
|
+
if (inFlight) {
|
|
97
|
+
logger.debug("Project CSS compile single-flight hit", {
|
|
90
98
|
projectSlug: context.projectSlug,
|
|
91
|
-
|
|
92
|
-
suggestion: formatted.suggestion,
|
|
99
|
+
cacheKeySuffix: context.cacheKey.slice(-24),
|
|
93
100
|
});
|
|
94
|
-
|
|
95
|
-
`[tailwind] ${formatted.title}: ${formatted.message} Suggestion: ${formatted.suggestion}`,
|
|
96
|
-
);
|
|
101
|
+
return inFlight;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
context,
|
|
102
|
-
{ css: result.css, hash, candidatesHash: context.candidatesHash },
|
|
103
|
-
candidates,
|
|
104
|
-
);
|
|
104
|
+
const generationPromise = (async () => {
|
|
105
|
+
// Generate fresh CSS
|
|
106
|
+
const result = await generateTailwindCSS(context.stylesheet, candidates, options);
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
if (result.error) {
|
|
109
|
+
const formatted = formatCSSError(result.error);
|
|
110
|
+
logger.error("Project CSS generation failed", {
|
|
111
|
+
projectSlug: context.projectSlug,
|
|
112
|
+
error: formatted.message,
|
|
113
|
+
suggestion: formatted.suggestion,
|
|
114
|
+
});
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[tailwind] ${formatted.title}: ${formatted.message} Suggestion: ${formatted.suggestion}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hash = hashCSS(result.css);
|
|
121
|
+
await storeProjectCSS(
|
|
122
|
+
context,
|
|
123
|
+
{ css: result.css, hash, candidatesHash: context.candidatesHash },
|
|
124
|
+
candidates,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
logger.debug("Project CSS generated", {
|
|
128
|
+
projectSlug: context.projectSlug,
|
|
129
|
+
hash,
|
|
130
|
+
cssLength: result.css.length,
|
|
131
|
+
candidateCount: candidates.size,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return { css: result.css, hash, fromCache: false };
|
|
135
|
+
})();
|
|
136
|
+
|
|
137
|
+
inFlightProjectCSS.set(context.cacheKey, generationPromise);
|
|
112
138
|
|
|
113
|
-
|
|
139
|
+
try {
|
|
140
|
+
return await generationPromise;
|
|
141
|
+
} finally {
|
|
142
|
+
inFlightProjectCSS.delete(context.cacheKey);
|
|
143
|
+
}
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
// ============================================================================
|
|
@@ -128,7 +158,10 @@ export async function getProjectCSS(
|
|
|
128
158
|
* @returns The regenerated CSS if inputs are cached and hash matches, undefined otherwise
|
|
129
159
|
*/
|
|
130
160
|
export async function regenerateCSSByHash(expectedHash: string): Promise<string | undefined> {
|
|
131
|
-
|
|
161
|
+
const inFlight = inFlightRegeneration.get(expectedHash);
|
|
162
|
+
if (inFlight) return await inFlight;
|
|
163
|
+
|
|
164
|
+
const regenerationPromise = withSpan(
|
|
132
165
|
SpanNames.HTML_REGENERATE_CSS_BY_HASH,
|
|
133
166
|
async () => {
|
|
134
167
|
const inputs = await resolveRegenerationInputs(expectedHash);
|
|
@@ -175,6 +208,14 @@ export async function regenerateCSSByHash(expectedHash: string): Promise<string
|
|
|
175
208
|
},
|
|
176
209
|
{ "css.hash": expectedHash },
|
|
177
210
|
);
|
|
211
|
+
|
|
212
|
+
inFlightRegeneration.set(expectedHash, regenerationPromise);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
return await regenerationPromise;
|
|
216
|
+
} finally {
|
|
217
|
+
inFlightRegeneration.delete(expectedHash);
|
|
218
|
+
}
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
// ============================================================================
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Import Collector - Request-scoped CSS import tracking for SSR
|
|
3
|
+
*
|
|
4
|
+
* Collects CSS import paths discovered during module loading using
|
|
5
|
+
* AsyncLocalStorage for proper isolation between concurrent requests.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { result, cssImports } = await runWithCSSCollector(() => loadModules(...));
|
|
9
|
+
* // cssImports contains absolute paths to CSS files discovered during loading
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
|
|
14
|
+
interface CSSCollectorStore {
|
|
15
|
+
imports: Set<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cssStorage = new AsyncLocalStorage<CSSCollectorStore>();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run a function with CSS import collection enabled.
|
|
22
|
+
* Returns the function result and all collected CSS import paths.
|
|
23
|
+
*/
|
|
24
|
+
export async function runWithCSSCollector<T>(
|
|
25
|
+
fn: () => T | Promise<T>,
|
|
26
|
+
): Promise<{ result: T; cssImports: string[] }> {
|
|
27
|
+
const store: CSSCollectorStore = { imports: new Set() };
|
|
28
|
+
const result = await cssStorage.run(store, fn);
|
|
29
|
+
return { result, cssImports: [...store.imports] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a CSS import path discovered during module loading.
|
|
34
|
+
* No-op if called outside of a runWithCSSCollector context.
|
|
35
|
+
*/
|
|
36
|
+
export function registerCSSImport(absolutePath: string): void {
|
|
37
|
+
const store = cssStorage.getStore();
|
|
38
|
+
if (!store) return;
|
|
39
|
+
store.imports.add(absolutePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get all CSS imports collected so far in the current context.
|
|
44
|
+
* Returns empty array if called outside of a runWithCSSCollector context.
|
|
45
|
+
*/
|
|
46
|
+
export function getCSSImports(): string[] {
|
|
47
|
+
const store = cssStorage.getStore();
|
|
48
|
+
if (!store) return [];
|
|
49
|
+
return [...store.imports];
|
|
50
|
+
}
|
|
@@ -52,6 +52,7 @@ import { SSRCircuitBreaker } from "./ssr-circuit-breaker.js";
|
|
|
52
52
|
import { SSRDependencyValidator } from "./ssr-dependency-validator.js";
|
|
53
53
|
import { preflightLocalImports } from "./preflight-imports.js";
|
|
54
54
|
import { resolveVfModuleImports } from "./vf-module-resolver.js";
|
|
55
|
+
import { registerCSSImport } from "../css-import-collector.js";
|
|
55
56
|
|
|
56
57
|
const logger = rendererLogger.component("ssr-module-loader");
|
|
57
58
|
|
|
@@ -480,6 +481,12 @@ export class SSRModuleLoader {
|
|
|
480
481
|
this.options.adapter,
|
|
481
482
|
);
|
|
482
483
|
|
|
484
|
+
// Register CSS imports for later inclusion in HTML output.
|
|
485
|
+
// CSS files are not JS modules — skip them in the dependency graph.
|
|
486
|
+
for (const cssImport of parseResult.cssImports) {
|
|
487
|
+
registerCSSImport(cssImport.absolutePath);
|
|
488
|
+
}
|
|
489
|
+
|
|
483
490
|
if (parseResult.missing.length > 0) {
|
|
484
491
|
this.depValidator.missingDependencies.push(...parseResult.missing);
|
|
485
492
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { CrossProjectImport, MissingImport } from "../../../transforms/esm/import-parser.js";
|
|
11
11
|
import { parseLocalImports } from "../../../transforms/esm/import-parser.js";
|
|
12
|
+
import { registerCSSImport } from "../css-import-collector.js";
|
|
12
13
|
import { createFileSystem } from "../../../platform/compat/fs.js";
|
|
13
14
|
import { createError, toError } from "../../../errors/veryfront-error.js";
|
|
14
15
|
import { rendererLogger } from "../../../utils/index.js";
|
|
@@ -92,6 +93,11 @@ export class SSRDependencyValidator {
|
|
|
92
93
|
this.adapter,
|
|
93
94
|
);
|
|
94
95
|
|
|
96
|
+
// Register CSS imports from cached modules for HTML inclusion
|
|
97
|
+
for (const cssImport of parseResult.cssImports) {
|
|
98
|
+
registerCSSImport(cssImport.absolutePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
if (parseResult.missing.length > 0) {
|
|
96
102
|
this.missingDependencies.push(...parseResult.missing);
|
|
97
103
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../../../rendering/snippet-renderer.js";
|
|
20
20
|
import { clearRendererCacheForProject } from "../../../rendering/renderer.js";
|
|
21
21
|
import { invalidateProjectCSS } from "../../../html/styles-builder/tailwind-compiler.js";
|
|
22
|
+
import { invalidateProjectCandidateManifests } from "../../../rendering/orchestrator/css-candidate-manifest.js";
|
|
22
23
|
|
|
23
24
|
export function createFSAdapter(config: FSAdapterConfig): Promise<FSAdapter> {
|
|
24
25
|
const type = config.type ?? "local";
|
|
@@ -51,7 +52,10 @@ export function createFSAdapter(config: FSAdapterConfig): Promise<FSAdapter> {
|
|
|
51
52
|
clearRouterDetectionCacheForProject,
|
|
52
53
|
clearSnippetCacheForProject,
|
|
53
54
|
clearRendererCacheForProject,
|
|
54
|
-
clearProjectCSSCache:
|
|
55
|
+
clearProjectCSSCache: (projectSlug: string) => {
|
|
56
|
+
invalidateProjectCSS(projectSlug);
|
|
57
|
+
invalidateProjectCandidateManifests(projectSlug);
|
|
58
|
+
},
|
|
55
59
|
...config.invalidationCallbacks,
|
|
56
60
|
},
|
|
57
61
|
};
|
|
@@ -54,6 +54,7 @@ export class WebSocketManager {
|
|
|
54
54
|
|
|
55
55
|
private wsConnectionId: string | null = null;
|
|
56
56
|
private wsConsecutiveFailures = 0;
|
|
57
|
+
private disposed = false;
|
|
57
58
|
private pokeMetrics = {
|
|
58
59
|
received: 0,
|
|
59
60
|
invalidationsTriggered: 0,
|
|
@@ -72,6 +73,8 @@ export class WebSocketManager {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
connect(projectId: string): void {
|
|
76
|
+
if (this.disposed) return;
|
|
77
|
+
|
|
75
78
|
this.cleanupTimers();
|
|
76
79
|
|
|
77
80
|
if (this.wsConsecutiveFailures >= WS_RECONNECT_MAX_FAILURES) {
|
|
@@ -118,21 +121,28 @@ export class WebSocketManager {
|
|
|
118
121
|
};
|
|
119
122
|
|
|
120
123
|
this.ws.onclose = () => {
|
|
124
|
+
this.wsConnectionId = null;
|
|
125
|
+
this.cleanupTimers();
|
|
126
|
+
|
|
127
|
+
if (this.disposed) return;
|
|
128
|
+
|
|
121
129
|
this.wsConsecutiveFailures++;
|
|
122
130
|
const delay = this.getReconnectDelay();
|
|
123
131
|
logger.debug("WebSocket closed, reconnecting", {
|
|
124
132
|
delayMs: delay,
|
|
125
|
-
connectionId: this.wsConnectionId,
|
|
126
133
|
totalPokesReceived: this.pokeMetrics.received,
|
|
127
134
|
consecutiveFailures: this.wsConsecutiveFailures,
|
|
128
135
|
});
|
|
129
|
-
this.wsConnectionId = null;
|
|
130
|
-
this.cleanupTimers();
|
|
131
136
|
this.wsReconnectTimer = dntShim.setTimeout(() => this.connect(projectId), delay);
|
|
132
137
|
};
|
|
133
138
|
|
|
134
|
-
this.ws.onerror = (
|
|
135
|
-
logger.warn("WebSocket error", {
|
|
139
|
+
this.ws.onerror = (event) => {
|
|
140
|
+
logger.warn("WebSocket error", {
|
|
141
|
+
type: event.type,
|
|
142
|
+
url: (event.target as WebSocket)?.url?.replace(/token=[^&]+/, "token=***"),
|
|
143
|
+
readyState: (event.target as WebSocket)?.readyState,
|
|
144
|
+
consecutiveFailures: this.wsConsecutiveFailures,
|
|
145
|
+
});
|
|
136
146
|
};
|
|
137
147
|
} catch (error) {
|
|
138
148
|
this.wsConsecutiveFailures++;
|
|
@@ -152,6 +162,7 @@ export class WebSocketManager {
|
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
dispose(): void {
|
|
165
|
+
this.disposed = true;
|
|
155
166
|
this.cleanupTimers();
|
|
156
167
|
|
|
157
168
|
if (this.invalidationTimer) {
|
|
@@ -166,6 +177,11 @@ export class WebSocketManager {
|
|
|
166
177
|
|
|
167
178
|
if (!this.ws) return;
|
|
168
179
|
|
|
180
|
+
// Detach handlers before closing to prevent onclose from scheduling a reconnect
|
|
181
|
+
this.ws.onclose = null;
|
|
182
|
+
this.ws.onerror = null;
|
|
183
|
+
this.ws.onmessage = null;
|
|
184
|
+
|
|
169
185
|
try {
|
|
170
186
|
this.ws.close();
|
|
171
187
|
} catch (error) {
|
|
@@ -361,7 +361,10 @@ export function getOsType(): string {
|
|
|
361
361
|
/**
|
|
362
362
|
* Register a signal handler (SIGINT, SIGTERM) for graceful shutdown
|
|
363
363
|
*/
|
|
364
|
-
export function onSignal(
|
|
364
|
+
export function onSignal(
|
|
365
|
+
signal: "SIGINT" | "SIGTERM",
|
|
366
|
+
handler: () => void,
|
|
367
|
+
): void {
|
|
365
368
|
if (IS_DENO) {
|
|
366
369
|
dntShim.Deno.addSignalListener(signal, handler);
|
|
367
370
|
return;
|
|
@@ -397,14 +400,35 @@ export function onGlobalError(
|
|
|
397
400
|
|
|
398
401
|
if (!hasNodeProcess) return;
|
|
399
402
|
|
|
403
|
+
const handleNodeGlobalError = (
|
|
404
|
+
error: Error,
|
|
405
|
+
type: "uncaughtException" | "unhandledRejection",
|
|
406
|
+
): void => {
|
|
407
|
+
let shouldPreventExit = false;
|
|
408
|
+
try {
|
|
409
|
+
shouldPreventExit = onError(error, type) === true;
|
|
410
|
+
} catch (handlerError) {
|
|
411
|
+
const handlerException = handlerError instanceof Error
|
|
412
|
+
? handlerError
|
|
413
|
+
: new Error(String(handlerError));
|
|
414
|
+
console.error("Global error handler threw while processing", type, handlerException);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (shouldPreventExit) return;
|
|
418
|
+
|
|
419
|
+
// Node/Bun suppress default fatal behavior when a listener is registered.
|
|
420
|
+
// If the callback did not explicitly handle the error, exit to preserve
|
|
421
|
+
// expected fatal semantics for uncaught exceptions and unhandled rejections.
|
|
422
|
+
nodeProcess!.exit(1);
|
|
423
|
+
};
|
|
424
|
+
|
|
400
425
|
nodeProcess!.on("uncaughtException", (error: Error) => {
|
|
401
|
-
|
|
402
|
-
// Note: In Node.js, uncaughtException doesn't exit by default if handler is registered
|
|
426
|
+
handleNodeGlobalError(error, "uncaughtException");
|
|
403
427
|
});
|
|
404
428
|
|
|
405
429
|
nodeProcess!.on("unhandledRejection", (reason: unknown) => {
|
|
406
430
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
407
|
-
|
|
431
|
+
handleNodeGlobalError(error, "unhandledRejection");
|
|
408
432
|
});
|
|
409
433
|
}
|
|
410
434
|
|
package/src/src/proxy/main.ts
CHANGED
|
@@ -581,19 +581,40 @@ function router(req: dntShim.Request): Promise<dntShim.Response> {
|
|
|
581
581
|
return forwardToServer(req);
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
// Create server before signal registration so early SIGTERM/SIGINT can close it safely.
|
|
585
|
+
const server = createHttpServer();
|
|
586
|
+
|
|
584
587
|
// Graceful shutdown
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
588
|
+
let shuttingDown = false;
|
|
589
|
+
async function shutdown(signal: "SIGINT" | "SIGTERM"): Promise<void> {
|
|
590
|
+
if (shuttingDown) return;
|
|
591
|
+
shuttingDown = true;
|
|
592
|
+
|
|
593
|
+
proxyLogger.info(`Received ${signal}, shutting down`);
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await server.close();
|
|
597
|
+
rendererRouter?.close();
|
|
598
|
+
serverResolver.close();
|
|
599
|
+
await proxyHandler.close();
|
|
600
|
+
await shutdownOTLP();
|
|
601
|
+
proxyLogger.info("Closed connections");
|
|
602
|
+
} catch (error) {
|
|
603
|
+
proxyLogger.error("Error while shutting down proxy", error);
|
|
604
|
+
} finally {
|
|
605
|
+
exit(0);
|
|
606
|
+
}
|
|
593
607
|
}
|
|
594
608
|
|
|
595
|
-
|
|
596
|
-
|
|
609
|
+
const handleSignal = (signal: "SIGINT" | "SIGTERM"): void => {
|
|
610
|
+
void shutdown(signal).catch((error) => {
|
|
611
|
+
proxyLogger.error("Unhandled shutdown error", { signal }, error);
|
|
612
|
+
exit(1);
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
onSignal("SIGINT", () => handleSignal("SIGINT"));
|
|
617
|
+
onSignal("SIGTERM", () => handleSignal("SIGTERM"));
|
|
597
618
|
|
|
598
619
|
// Wait for sticky-session router to resolve initial target list
|
|
599
620
|
await rendererRouter?.ready();
|
|
@@ -607,6 +628,5 @@ proxyLogger.debug("Starting proxy server (split mode)", {
|
|
|
607
628
|
apiBaseUrl: config.apiBaseUrl,
|
|
608
629
|
});
|
|
609
630
|
|
|
610
|
-
//
|
|
611
|
-
const server = createHttpServer();
|
|
631
|
+
// Start the HTTP server
|
|
612
632
|
await server.serve(router, { port: PORT, hostname: HOST });
|
|
@@ -5,6 +5,15 @@ import { ProxySpanNames, withSpan } from "./tracing.js";
|
|
|
5
5
|
|
|
6
6
|
export type TokenScope = "preview" | "production";
|
|
7
7
|
|
|
8
|
+
interface NegativeCacheEntry {
|
|
9
|
+
status: number;
|
|
10
|
+
message: string;
|
|
11
|
+
cachedAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const NEGATIVE_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
const NEGATIVE_CACHE_MAX_SIZE = 1000;
|
|
16
|
+
|
|
8
17
|
export interface OAuthConfig {
|
|
9
18
|
apiBaseUrl: string;
|
|
10
19
|
apiClientId: string;
|
|
@@ -21,6 +30,7 @@ export interface TokenManagerOptions {
|
|
|
21
30
|
export class TokenManager {
|
|
22
31
|
private cache: TokenCache;
|
|
23
32
|
private pendingRequests = new Map<string, Promise<string>>();
|
|
33
|
+
private negativeCache = new Map<string, NegativeCacheEntry>();
|
|
24
34
|
private refreshBuffer: number;
|
|
25
35
|
|
|
26
36
|
constructor(
|
|
@@ -42,6 +52,14 @@ export class TokenManager {
|
|
|
42
52
|
return withSpan(
|
|
43
53
|
ProxySpanNames.PROXY_TOKEN_FETCH,
|
|
44
54
|
async () => {
|
|
55
|
+
const negEntry = this.negativeCache.get(cacheKey);
|
|
56
|
+
if (negEntry) {
|
|
57
|
+
if (Date.now() - negEntry.cachedAt < NEGATIVE_CACHE_TTL) {
|
|
58
|
+
throw new Error(negEntry.message);
|
|
59
|
+
}
|
|
60
|
+
this.negativeCache.delete(cacheKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
const cached = await this.cache.get(cacheKey);
|
|
46
64
|
if (cached && this.isTokenValid(cached)) return cached.token;
|
|
47
65
|
|
|
@@ -67,10 +85,13 @@ export class TokenManager {
|
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
async invalidateToken(scope: TokenScope, projectSlug?: string): Promise<void> {
|
|
70
|
-
|
|
88
|
+
const cacheKey = this.getCacheKey(scope, projectSlug);
|
|
89
|
+
this.negativeCache.delete(cacheKey);
|
|
90
|
+
await this.cache.delete(cacheKey);
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
async clearCache(): Promise<void> {
|
|
94
|
+
this.negativeCache.clear();
|
|
74
95
|
await this.cache.clear();
|
|
75
96
|
}
|
|
76
97
|
|
|
@@ -101,13 +122,32 @@ export class TokenManager {
|
|
|
101
122
|
? this.config.previewApiClientSecret
|
|
102
123
|
: this.config.apiClientSecret;
|
|
103
124
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
let response: TokenResponse;
|
|
126
|
+
try {
|
|
127
|
+
response = await fetchOAuthToken({
|
|
128
|
+
apiBaseUrl: this.config.apiBaseUrl,
|
|
129
|
+
apiClientId,
|
|
130
|
+
apiClientSecret,
|
|
131
|
+
projectSlug,
|
|
132
|
+
customDomain,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const status = this.parseStatusFromError(error);
|
|
136
|
+
if (status === 400 || status === 404) {
|
|
137
|
+
const projectKey = projectSlug || customDomain;
|
|
138
|
+
const cacheKey = this.getCacheKey(scope, projectKey);
|
|
139
|
+
if (this.negativeCache.size >= NEGATIVE_CACHE_MAX_SIZE) {
|
|
140
|
+
const oldest = this.negativeCache.keys().next().value;
|
|
141
|
+
if (oldest !== undefined) this.negativeCache.delete(oldest);
|
|
142
|
+
}
|
|
143
|
+
this.negativeCache.set(cacheKey, {
|
|
144
|
+
status,
|
|
145
|
+
message: error instanceof Error ? error.message : String(error),
|
|
146
|
+
cachedAt: Date.now(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
111
151
|
|
|
112
152
|
const projectKey = projectSlug || customDomain;
|
|
113
153
|
|
|
@@ -121,6 +161,12 @@ export class TokenManager {
|
|
|
121
161
|
return response.access_token;
|
|
122
162
|
}
|
|
123
163
|
|
|
164
|
+
private parseStatusFromError(error: unknown): number | null {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
const match = message.match(/failed: (\d+)/);
|
|
167
|
+
return match ? Number(match[1]) : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
124
170
|
private calculateExpiresAt(response: TokenResponse): number {
|
|
125
171
|
if (response.expires_in) return Date.now() + response.expires_in * 1000;
|
|
126
172
|
|