veryfront 0.1.84 → 0.1.86

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/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -12,6 +12,9 @@ export declare class VeryfrontFSAdapter implements FSAdapter {
12
12
  private initialized;
13
13
  /** Resolves when file list initialization is complete (for coordinating reads) */
14
14
  private fileListReadyResolve;
15
+ /** Single-flight background rewarm when the file list cache disappears */
16
+ private fileListWarmupPromise;
17
+ private fileListWarmupKey;
15
18
  private projectData?;
16
19
  private apiBaseUrl;
17
20
  private apiToken;
@@ -29,6 +32,7 @@ export declare class VeryfrontFSAdapter implements FSAdapter {
29
32
  constructor(config: FSAdapterConfig);
30
33
  initialize(): Promise<void>;
31
34
  private isPersistentCacheInvalidated;
35
+ private scheduleFileListWarmup;
32
36
  getPokeMetrics(): {
33
37
  received: number;
34
38
  invalidationsTriggered: number;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/adapter.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,9 @@ export class VeryfrontFSAdapter {
27
27
  initialized = false;
28
28
  /** Resolves when file list initialization is complete (for coordinating reads) */
29
29
  fileListReadyResolve = null;
30
+ /** Single-flight background rewarm when the file list cache disappears */
31
+ fileListWarmupPromise = null;
32
+ fileListWarmupKey = null;
30
33
  projectData;
31
34
  apiBaseUrl;
32
35
  apiToken;
@@ -94,6 +97,9 @@ export class VeryfrontFSAdapter {
94
97
  hasResult: !!result,
95
98
  resultSize: result?.length ?? 0,
96
99
  });
100
+ if (!result?.length) {
101
+ this.scheduleFileListWarmup("getFileList miss", cacheKey);
102
+ }
97
103
  return result;
98
104
  },
99
105
  hasCachedFileList: async () => {
@@ -109,6 +115,9 @@ export class VeryfrontFSAdapter {
109
115
  hasResult,
110
116
  resultSize: result?.length ?? 0,
111
117
  });
118
+ if (!hasResult) {
119
+ this.scheduleFileListWarmup("hasCachedFileList miss", cacheKey);
120
+ }
112
121
  return hasResult;
113
122
  },
114
123
  isPersistentCacheInvalidated: (prefix) => this.isPersistentCacheInvalidated(prefix),
@@ -132,6 +141,9 @@ export class VeryfrontFSAdapter {
132
141
  resultSize: result?.length ?? 0,
133
142
  hasContent: result?.filter((f) => f.content)?.length ?? 0,
134
143
  });
144
+ if (!result?.length) {
145
+ this.scheduleFileListWarmup("getFileListCache miss", cacheKey);
146
+ }
135
147
  return result;
136
148
  });
137
149
  this.dirOps = new DirectoryOperations(this.client, this.cache, this.normalizer, contentContextGetter);
@@ -280,6 +292,65 @@ export class VeryfrontFSAdapter {
280
292
  isPersistentCacheInvalidated(prefix) {
281
293
  return isPrefixBeingInvalidated(prefix);
282
294
  }
295
+ scheduleFileListWarmup(reason, cacheKey) {
296
+ if (!this.initialized || !this.contentContext)
297
+ return;
298
+ const effectiveCacheKey = cacheKey ?? buildFileListCacheKey(this.contentContext);
299
+ if (this.fileListWarmupPromise && this.fileListWarmupKey === effectiveCacheKey) {
300
+ logger.debug("File list warmup already in progress", {
301
+ reason,
302
+ cacheKey: effectiveCacheKey,
303
+ });
304
+ return;
305
+ }
306
+ const warmupContext = this.contentContext;
307
+ let warmupPromise = null;
308
+ warmupPromise = (async () => {
309
+ try {
310
+ const existing = await this.cache.getAsync(effectiveCacheKey);
311
+ if (existing?.length) {
312
+ logger.debug("Skipping file list warmup because cache is already populated", {
313
+ reason,
314
+ cacheKey: effectiveCacheKey,
315
+ fileCount: existing.length,
316
+ });
317
+ return;
318
+ }
319
+ logger.debug("Starting file list warmup", {
320
+ reason,
321
+ cacheKey: effectiveCacheKey,
322
+ sourceType: warmupContext.sourceType,
323
+ branch: warmupContext.branch,
324
+ environmentName: warmupContext.environmentName,
325
+ releaseId: warmupContext.releaseId,
326
+ });
327
+ const files = await fetchFileListForContext(this.client, warmupContext);
328
+ await this.cache.setAsync(effectiveCacheKey, files);
329
+ logger.debug("File list warmup complete", {
330
+ reason,
331
+ cacheKey: effectiveCacheKey,
332
+ totalFiles: files.length,
333
+ filesWithContent: files.filter((file) => file.content).length,
334
+ });
335
+ }
336
+ catch (error) {
337
+ logger.warn("File list warmup failed", {
338
+ reason,
339
+ cacheKey: effectiveCacheKey,
340
+ error: error instanceof Error ? error.message : String(error),
341
+ });
342
+ }
343
+ finally {
344
+ if (warmupPromise && this.fileListWarmupPromise === warmupPromise) {
345
+ this.fileListWarmupPromise = null;
346
+ this.fileListWarmupKey = null;
347
+ }
348
+ }
349
+ })();
350
+ this.fileListWarmupPromise = warmupPromise;
351
+ this.fileListWarmupKey = effectiveCacheKey;
352
+ this.readOps.setFileListReadyPromise(warmupPromise);
353
+ }
283
354
  getPokeMetrics() {
284
355
  return this.wsManager.getPokeMetrics();
285
356
  }
@@ -317,6 +388,8 @@ export class VeryfrontFSAdapter {
317
388
  this.statOps.clearIndex();
318
389
  this.dirOps.clearTree();
319
390
  this.initialized = false;
391
+ this.fileListWarmupPromise = null;
392
+ this.fileListWarmupKey = null;
320
393
  logger.debug("Disposed");
321
394
  }
322
395
  getCacheStats() {
@@ -342,6 +415,7 @@ export class VeryfrontFSAdapter {
342
415
  hasFiles: !!files,
343
416
  fileCount: files?.length ?? 0,
344
417
  });
418
+ this.scheduleFileListWarmup("getAllSourceFiles miss", cacheKey);
345
419
  return [];
346
420
  }
347
421
  const fileSummary = summarizeFileList(files);
@@ -429,6 +503,8 @@ export class VeryfrontFSAdapter {
429
503
  if (contextChanged) {
430
504
  this.statOps.clearIndex();
431
505
  this.dirOps.clearTree();
506
+ this.fileListWarmupPromise = null;
507
+ this.fileListWarmupKey = null;
432
508
  logger.debug("Cleared index and dirTree due to context change", {
433
509
  oldContext,
434
510
  newContext: context,
@@ -11,10 +11,21 @@ interface RouteCandidateOptions {
11
11
  files: SourceFileLike[];
12
12
  developmentMode: boolean;
13
13
  }
14
+ interface ProjectCandidateOptions {
15
+ projectScope: string;
16
+ projectVersion: string;
17
+ projectDir: string;
18
+ files: SourceFileLike[];
19
+ developmentMode: boolean;
20
+ }
14
21
  /**
15
22
  * Resolve route-scoped Tailwind candidates from a precomputed per-project manifest.
16
23
  */
17
24
  export declare function getRouteCandidates(options: RouteCandidateOptions): Set<string>;
25
+ /**
26
+ * Resolve full-project Tailwind candidates from a precomputed per-project manifest.
27
+ */
28
+ export declare function getProjectCandidates(options: ProjectCandidateOptions): Set<string>;
18
29
  /**
19
30
  * Invalidate cached candidate manifests for one project scope (or all scopes).
20
31
  */
@@ -1 +1 @@
1
- {"version":3,"file":"css-candidate-manifest.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/orchestrator/css-candidate-manifest.ts"],"names":[],"mappings":"AAIA,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAQD,UAAU,qBAAqB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAmFD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,GAAG,GAAG,CAAC,MAAM,CAAC,CA+C9E;AAED;;GAEG;AACH,wBAAgB,mCAAmC,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAc/E"}
1
+ {"version":3,"file":"css-candidate-manifest.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/orchestrator/css-candidate-manifest.ts"],"names":[],"mappings":"AAIA,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAQD,UAAU,qBAAqB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,UAAU,uBAAuB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CAC1B;AA0GD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,GAAG,GAAG,CAAC,MAAM,CAAC,CAkC9E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,uBAAuB,GAAG,GAAG,CAAC,MAAM,CAAC,CAGlF;AAED;;GAEG;AACH,wBAAgB,mCAAmC,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAc/E"}
@@ -58,6 +58,21 @@ function buildCandidateManifest(files, projectDir) {
58
58
  }
59
59
  return { fileCandidates, allCandidates, builtAt: Date.now() };
60
60
  }
61
+ function getOrBuildManifest(options) {
62
+ const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
63
+ const existingManifest = manifestCache.get(manifestKey);
64
+ const manifest = shouldRebuildManifest(existingManifest, options.developmentMode)
65
+ ? buildCandidateManifest(options.files, options.projectDir)
66
+ : existingManifest;
67
+ if (manifest !== existingManifest) {
68
+ manifestCache.set(manifestKey, manifest);
69
+ for (const key of routeCandidateCache.keys()) {
70
+ if (key.startsWith(`${manifestKey}:`))
71
+ routeCandidateCache.delete(key);
72
+ }
73
+ }
74
+ return manifest;
75
+ }
61
76
  function addCandidatesForPath(target, manifest, path, projectDir) {
62
77
  const absolutePath = normalizePath(path);
63
78
  const relativePath = toRelativeProjectPath(path, projectDir);
@@ -73,18 +88,7 @@ function addCandidatesForPath(target, manifest, path, projectDir) {
73
88
  */
74
89
  export function getRouteCandidates(options) {
75
90
  const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
76
- const existingManifest = manifestCache.get(manifestKey);
77
- const manifest = shouldRebuildManifest(existingManifest, options.developmentMode)
78
- ? buildCandidateManifest(options.files, options.projectDir)
79
- : existingManifest;
80
- if (manifest !== existingManifest) {
81
- manifestCache.set(manifestKey, manifest);
82
- // Clear route subsets when project-level file manifest is rebuilt.
83
- for (const key of routeCandidateCache.keys()) {
84
- if (key.startsWith(`${manifestKey}:`))
85
- routeCandidateCache.delete(key);
86
- }
87
- }
91
+ const manifest = getOrBuildManifest(options);
88
92
  const routeCacheKey = `${manifestKey}:${options.routeKey}`;
89
93
  const cachedRoute = routeCandidateCache.get(routeCacheKey);
90
94
  if (cachedRoute)
@@ -112,6 +116,13 @@ export function getRouteCandidates(options) {
112
116
  });
113
117
  return new Set(routeCandidates);
114
118
  }
119
+ /**
120
+ * Resolve full-project Tailwind candidates from a precomputed per-project manifest.
121
+ */
122
+ export function getProjectCandidates(options) {
123
+ const manifest = getOrBuildManifest(options);
124
+ return new Set(manifest.allCandidates);
125
+ }
115
126
  /**
116
127
  * Invalidate cached candidate manifests for one project scope (or all scopes).
117
128
  */
@@ -1 +1 @@
1
- {"version":3,"file":"styles-candidate-scanner.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/dev/styles-candidate-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAWlD;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAqCxF"}
1
+ {"version":3,"file":"styles-candidate-scanner.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/dev/styles-candidate-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA8BlD;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CA0CxF"}
@@ -11,12 +11,26 @@ import { extractCandidates } from "../../../html/styles-builder/tailwind-compile
11
11
  import { serverLogger } from "../../../utils/index.js";
12
12
  import { createFileSystem } from "../../../platform/compat/fs.js";
13
13
  import { join } from "../../../platform/compat/path/index.js";
14
+ import { getProjectCandidates } from "../../../rendering/orchestrator/css-candidate-manifest.js";
14
15
  import { FRAMEWORK_CANDIDATES } from "./framework-candidates.generated.js";
15
16
  const logger = serverLogger.component("styles-candidate-scanner");
16
17
  const SOURCE_EXTENSIONS = [".tsx", ".jsx", ".mdx", ".ts", ".js"];
17
18
  const SKIP_DIRS = new Set(["node_modules", ".cache", ".git", "dist", "build", ".vscode"]);
18
19
  /** De-duplicated set of framework candidates, computed once at import time. */
19
20
  const frameworkCandidates = new Set(FRAMEWORK_CANDIDATES);
21
+ function resolveProjectVersion(ctx, contentContext) {
22
+ if (contentContext?.releaseId)
23
+ return `release:${contentContext.releaseId}`;
24
+ if (contentContext?.branch)
25
+ return `branch:${contentContext.branch}`;
26
+ if (contentContext?.environmentName)
27
+ return `environment:${contentContext.environmentName}`;
28
+ if (ctx.releaseId)
29
+ return `release:${ctx.releaseId}`;
30
+ if (ctx.parsedDomain?.branch)
31
+ return `branch:${ctx.parsedDomain.branch}`;
32
+ return "live";
33
+ }
20
34
  /**
21
35
  * Extract Tailwind CSS candidate class names from all project source files.
22
36
  *
@@ -38,14 +52,17 @@ export async function extractProjectCandidates(ctx) {
38
52
  }
39
53
  const candidates = new Set(frameworkCandidates);
40
54
  const files = await fsAdapter.getAllSourceFiles();
41
- for (const file of files) {
42
- if (!file.content)
43
- continue;
44
- if (!SOURCE_EXTENSIONS.some((ext) => file.path.endsWith(ext)))
45
- continue;
46
- for (const cls of extractCandidates(file.content)) {
47
- candidates.add(cls);
48
- }
55
+ const contentContext = typeof fsAdapter.getContentContext === "function"
56
+ ? fsAdapter.getContentContext()
57
+ : null;
58
+ for (const cls of getProjectCandidates({
59
+ projectScope: ctx.projectSlug ?? contentContext?.projectSlug ?? ctx.projectDir,
60
+ projectVersion: resolveProjectVersion(ctx, contentContext),
61
+ projectDir: ctx.projectDir,
62
+ files,
63
+ developmentMode: contentContext?.sourceType === "branch",
64
+ })) {
65
+ candidates.add(cls);
49
66
  }
50
67
  return candidates;
51
68
  }
@@ -11,5 +11,6 @@ export declare class StylesCSSHandler extends BaseHandler {
11
11
  metadata: HandlerMetadata;
12
12
  handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult>;
13
13
  private loadStylesheet;
14
+ private generateStylesheet;
14
15
  }
15
16
  //# sourceMappingURL=styles-css.handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles-css.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/dev/styles-css.handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAanG,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,QAAQ,EAAE,eAAe,CAKvB;IAEI,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAkGjE,cAAc;CAiB7B"}
1
+ {"version":3,"file":"styles-css.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/dev/styles-css.handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAkBnG,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,QAAQ,EAAE,eAAe,CAKvB;IAEI,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAkGjE,cAAc;IAkB5B,OAAO,CAAC,kBAAkB;CAe3B"}
@@ -1,7 +1,7 @@
1
1
  import { BaseHandler } from "../response/base.js";
2
2
  import { HTTP_OK, PRIORITY_HIGH_DEV } from "../../../utils/constants/index.js";
3
3
  import { joinPath } from "../../../utils/path-utils.js";
4
- import { formatCSSError, generateTailwindCSS, } from "../../../html/styles-builder/tailwind-compiler.js";
4
+ import { formatCSSError, generateTailwindCSS, getProjectCSS, } from "../../../html/styles-builder/tailwind-compiler.js";
5
5
  import { DEFAULT_STYLESHEET } from "../../../html/styles-builder/css-hash-cache.js";
6
6
  import { serverLogger } from "../../../utils/index.js";
7
7
  import { extractProjectCandidates } from "./styles-candidate-scanner.js";
@@ -39,10 +39,8 @@ export class StylesCSSHandler extends BaseHandler {
39
39
  });
40
40
  candidates = new Set();
41
41
  }
42
- const result = await generateTailwindCSS(rawCss, candidates, {
43
- projectSlug: ctx.projectSlug,
44
- });
45
- if (result.error) {
42
+ const result = await this.generateStylesheet(ctx, rawCss, candidates);
43
+ if ("error" in result && result.error) {
46
44
  const formatted = formatCSSError(result.error);
47
45
  logger.error("Tailwind error", {
48
46
  error: formatted.message,
@@ -82,6 +80,8 @@ body::before {
82
80
  logger.debug("CSS generated", {
83
81
  candidates: candidates.size,
84
82
  cssLength: result.css.length,
83
+ fromCache: "fromCache" in result ? result.fromCache : false,
84
+ cssHash: "hash" in result ? result.hash : undefined,
85
85
  });
86
86
  return this.respond(responseBuilder.withContentType("text/css; charset=utf-8", result.css, HTTP_OK));
87
87
  });
@@ -114,4 +114,14 @@ body::before {
114
114
  return DEFAULT_STYLESHEET;
115
115
  }
116
116
  }
117
+ generateStylesheet(ctx, rawCss, candidates) {
118
+ if (!ctx.projectSlug) {
119
+ return generateTailwindCSS(rawCss, candidates);
120
+ }
121
+ return getProjectCSS(ctx.projectSlug, rawCss, candidates, {
122
+ minify: true,
123
+ environment: "preview",
124
+ buildMode: "production",
125
+ });
126
+ }
117
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -49,6 +49,9 @@ export class VeryfrontFSAdapter implements FSAdapter {
49
49
 
50
50
  /** Resolves when file list initialization is complete (for coordinating reads) */
51
51
  private fileListReadyResolve: (() => void) | null = null;
52
+ /** Single-flight background rewarm when the file list cache disappears */
53
+ private fileListWarmupPromise: Promise<void> | null = null;
54
+ private fileListWarmupKey: string | null = null;
52
55
 
53
56
  private projectData?: Project;
54
57
  private apiBaseUrl: string;
@@ -140,6 +143,10 @@ export class VeryfrontFSAdapter implements FSAdapter {
140
143
  resultSize: result?.length ?? 0,
141
144
  });
142
145
 
146
+ if (!result?.length) {
147
+ this.scheduleFileListWarmup("getFileList miss", cacheKey);
148
+ }
149
+
143
150
  return result;
144
151
  },
145
152
  hasCachedFileList: async () => {
@@ -158,6 +165,10 @@ export class VeryfrontFSAdapter implements FSAdapter {
158
165
  resultSize: result?.length ?? 0,
159
166
  });
160
167
 
168
+ if (!hasResult) {
169
+ this.scheduleFileListWarmup("hasCachedFileList miss", cacheKey);
170
+ }
171
+
161
172
  return hasResult;
162
173
  },
163
174
  isPersistentCacheInvalidated: (prefix: string) => this.isPersistentCacheInvalidated(prefix),
@@ -202,6 +213,10 @@ export class VeryfrontFSAdapter implements FSAdapter {
202
213
  hasContent: result?.filter((f) => f.content)?.length ?? 0,
203
214
  });
204
215
 
216
+ if (!result?.length) {
217
+ this.scheduleFileListWarmup("getFileListCache miss", cacheKey);
218
+ }
219
+
205
220
  return result;
206
221
  },
207
222
  );
@@ -389,6 +404,73 @@ export class VeryfrontFSAdapter implements FSAdapter {
389
404
  return isPrefixBeingInvalidated(prefix);
390
405
  }
391
406
 
407
+ private scheduleFileListWarmup(reason: string, cacheKey?: string): void {
408
+ if (!this.initialized || !this.contentContext) return;
409
+
410
+ const effectiveCacheKey = cacheKey ?? buildFileListCacheKey(this.contentContext);
411
+
412
+ if (this.fileListWarmupPromise && this.fileListWarmupKey === effectiveCacheKey) {
413
+ logger.debug("File list warmup already in progress", {
414
+ reason,
415
+ cacheKey: effectiveCacheKey,
416
+ });
417
+ return;
418
+ }
419
+
420
+ const warmupContext = this.contentContext;
421
+ let warmupPromise: Promise<void> | null = null;
422
+ warmupPromise = (async () => {
423
+ try {
424
+ const existing = await this.cache.getAsync<Array<{ path: string; content?: string }>>(
425
+ effectiveCacheKey,
426
+ );
427
+
428
+ if (existing?.length) {
429
+ logger.debug("Skipping file list warmup because cache is already populated", {
430
+ reason,
431
+ cacheKey: effectiveCacheKey,
432
+ fileCount: existing.length,
433
+ });
434
+ return;
435
+ }
436
+
437
+ logger.debug("Starting file list warmup", {
438
+ reason,
439
+ cacheKey: effectiveCacheKey,
440
+ sourceType: warmupContext.sourceType,
441
+ branch: warmupContext.branch,
442
+ environmentName: warmupContext.environmentName,
443
+ releaseId: warmupContext.releaseId,
444
+ });
445
+
446
+ const files = await fetchFileListForContext(this.client, warmupContext);
447
+ await this.cache.setAsync(effectiveCacheKey, files);
448
+
449
+ logger.debug("File list warmup complete", {
450
+ reason,
451
+ cacheKey: effectiveCacheKey,
452
+ totalFiles: files.length,
453
+ filesWithContent: files.filter((file) => file.content).length,
454
+ });
455
+ } catch (error) {
456
+ logger.warn("File list warmup failed", {
457
+ reason,
458
+ cacheKey: effectiveCacheKey,
459
+ error: error instanceof Error ? error.message : String(error),
460
+ });
461
+ } finally {
462
+ if (warmupPromise && this.fileListWarmupPromise === warmupPromise) {
463
+ this.fileListWarmupPromise = null;
464
+ this.fileListWarmupKey = null;
465
+ }
466
+ }
467
+ })();
468
+
469
+ this.fileListWarmupPromise = warmupPromise;
470
+ this.fileListWarmupKey = effectiveCacheKey;
471
+ this.readOps.setFileListReadyPromise(warmupPromise);
472
+ }
473
+
392
474
  getPokeMetrics(): {
393
475
  received: number;
394
476
  invalidationsTriggered: number;
@@ -442,6 +524,8 @@ export class VeryfrontFSAdapter implements FSAdapter {
442
524
  this.statOps.clearIndex();
443
525
  this.dirOps.clearTree();
444
526
  this.initialized = false;
527
+ this.fileListWarmupPromise = null;
528
+ this.fileListWarmupKey = null;
445
529
 
446
530
  logger.debug("Disposed");
447
531
  }
@@ -473,6 +557,7 @@ export class VeryfrontFSAdapter implements FSAdapter {
473
557
  hasFiles: !!files,
474
558
  fileCount: files?.length ?? 0,
475
559
  });
560
+ this.scheduleFileListWarmup("getAllSourceFiles miss", cacheKey);
476
561
  return [];
477
562
  }
478
563
 
@@ -584,6 +669,8 @@ export class VeryfrontFSAdapter implements FSAdapter {
584
669
  if (contextChanged) {
585
670
  this.statOps.clearIndex();
586
671
  this.dirOps.clearTree();
672
+ this.fileListWarmupPromise = null;
673
+ this.fileListWarmupKey = null;
587
674
  logger.debug("Cleared index and dirTree due to context change", {
588
675
  oldContext,
589
676
  newContext: context,
@@ -23,6 +23,14 @@ interface RouteCandidateOptions {
23
23
  developmentMode: boolean;
24
24
  }
25
25
 
26
+ interface ProjectCandidateOptions {
27
+ projectScope: string;
28
+ projectVersion: string;
29
+ projectDir: string;
30
+ files: SourceFileLike[];
31
+ developmentMode: boolean;
32
+ }
33
+
26
34
  const logger = rendererLogger.component("css-candidate-manifest");
27
35
  const SOURCE_EXTENSIONS = [".tsx", ".jsx", ".mdx", ".ts", ".js"];
28
36
  const DEV_MANIFEST_TTL_MS = 2_000;
@@ -90,6 +98,29 @@ function buildCandidateManifest(files: SourceFileLike[], projectDir: string): Ca
90
98
  return { fileCandidates, allCandidates, builtAt: Date.now() };
91
99
  }
92
100
 
101
+ function getOrBuildManifest(
102
+ options: Pick<
103
+ ProjectCandidateOptions,
104
+ "projectScope" | "projectVersion" | "projectDir" | "files" | "developmentMode"
105
+ >,
106
+ ): CandidateManifest {
107
+ const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
108
+ const existingManifest = manifestCache.get(manifestKey);
109
+ const manifest = shouldRebuildManifest(existingManifest, options.developmentMode)
110
+ ? buildCandidateManifest(options.files, options.projectDir)
111
+ : existingManifest!;
112
+
113
+ if (manifest !== existingManifest) {
114
+ manifestCache.set(manifestKey, manifest);
115
+
116
+ for (const key of routeCandidateCache.keys()) {
117
+ if (key.startsWith(`${manifestKey}:`)) routeCandidateCache.delete(key);
118
+ }
119
+ }
120
+
121
+ return manifest;
122
+ }
123
+
93
124
  function addCandidatesForPath(
94
125
  target: Set<string>,
95
126
  manifest: CandidateManifest,
@@ -109,20 +140,7 @@ function addCandidatesForPath(
109
140
  */
110
141
  export function getRouteCandidates(options: RouteCandidateOptions): Set<string> {
111
142
  const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
112
- const existingManifest = manifestCache.get(manifestKey);
113
- const manifest = shouldRebuildManifest(existingManifest, options.developmentMode)
114
- ? buildCandidateManifest(options.files, options.projectDir)
115
- : existingManifest!;
116
-
117
- if (manifest !== existingManifest) {
118
- manifestCache.set(manifestKey, manifest);
119
-
120
- // Clear route subsets when project-level file manifest is rebuilt.
121
- for (const key of routeCandidateCache.keys()) {
122
- if (key.startsWith(`${manifestKey}:`)) routeCandidateCache.delete(key);
123
- }
124
- }
125
-
143
+ const manifest = getOrBuildManifest(options);
126
144
  const routeCacheKey = `${manifestKey}:${options.routeKey}`;
127
145
  const cachedRoute = routeCandidateCache.get(routeCacheKey);
128
146
  if (cachedRoute) return new Set(cachedRoute);
@@ -156,6 +174,14 @@ export function getRouteCandidates(options: RouteCandidateOptions): Set<string>
156
174
  return new Set(routeCandidates);
157
175
  }
158
176
 
177
+ /**
178
+ * Resolve full-project Tailwind candidates from a precomputed per-project manifest.
179
+ */
180
+ export function getProjectCandidates(options: ProjectCandidateOptions): Set<string> {
181
+ const manifest = getOrBuildManifest(options);
182
+ return new Set(manifest.allCandidates);
183
+ }
184
+
159
185
  /**
160
186
  * Invalidate cached candidate manifests for one project scope (or all scopes).
161
187
  */
@@ -12,6 +12,8 @@ import { extractCandidates } from "../../../html/styles-builder/tailwind-compile
12
12
  import { serverLogger } from "../../../utils/index.js";
13
13
  import { createFileSystem } from "../../../platform/compat/fs.js";
14
14
  import { join } from "../../../platform/compat/path/index.js";
15
+ import { getProjectCandidates } from "../../../rendering/orchestrator/css-candidate-manifest.js";
16
+ import type { ResolvedContentContext } from "../../../platform/adapters/fs/veryfront/types.js";
15
17
  import type { HandlerContext } from "../types.js";
16
18
  import { FRAMEWORK_CANDIDATES } from "./framework-candidates.generated.js";
17
19
 
@@ -23,6 +25,25 @@ const SKIP_DIRS = new Set(["node_modules", ".cache", ".git", "dist", "build", ".
23
25
  /** De-duplicated set of framework candidates, computed once at import time. */
24
26
  const frameworkCandidates = new Set<string>(FRAMEWORK_CANDIDATES);
25
27
 
28
+ interface SourceFileProvider {
29
+ getAllSourceFiles?: () =>
30
+ | Array<{ path: string; content?: string }>
31
+ | Promise<Array<{ path: string; content?: string }>>;
32
+ getContentContext?: () => ResolvedContentContext | null;
33
+ }
34
+
35
+ function resolveProjectVersion(
36
+ ctx: HandlerContext,
37
+ contentContext: ResolvedContentContext | null,
38
+ ): string {
39
+ if (contentContext?.releaseId) return `release:${contentContext.releaseId}`;
40
+ if (contentContext?.branch) return `branch:${contentContext.branch}`;
41
+ if (contentContext?.environmentName) return `environment:${contentContext.environmentName}`;
42
+ if (ctx.releaseId) return `release:${ctx.releaseId}`;
43
+ if (ctx.parsedDomain?.branch) return `branch:${ctx.parsedDomain.branch}`;
44
+ return "live";
45
+ }
46
+
26
47
  /**
27
48
  * Extract Tailwind CSS candidate class names from all project source files.
28
49
  *
@@ -42,9 +63,8 @@ export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set
42
63
 
43
64
  // Call method directly on wrappedFs to preserve 'this' context
44
65
  const fsAdapter = wrappedFs.getUnderlyingAdapter() as {
45
- getAllSourceFiles?: () =>
46
- | Array<{ path: string; content?: string }>
47
- | Promise<Array<{ path: string; content?: string }>>;
66
+ getAllSourceFiles?: SourceFileProvider["getAllSourceFiles"];
67
+ getContentContext?: SourceFileProvider["getContentContext"];
48
68
  };
49
69
 
50
70
  if (typeof fsAdapter.getAllSourceFiles !== "function") {
@@ -56,14 +76,20 @@ export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set
56
76
 
57
77
  const candidates = new Set<string>(frameworkCandidates);
58
78
  const files = await fsAdapter.getAllSourceFiles();
59
-
60
- for (const file of files) {
61
- if (!file.content) continue;
62
- if (!SOURCE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) continue;
63
-
64
- for (const cls of extractCandidates(file.content)) {
65
- candidates.add(cls);
66
- }
79
+ const contentContext = typeof fsAdapter.getContentContext === "function"
80
+ ? fsAdapter.getContentContext()
81
+ : null;
82
+
83
+ for (
84
+ const cls of getProjectCandidates({
85
+ projectScope: ctx.projectSlug ?? contentContext?.projectSlug ?? ctx.projectDir,
86
+ projectVersion: resolveProjectVersion(ctx, contentContext),
87
+ projectDir: ctx.projectDir,
88
+ files,
89
+ developmentMode: contentContext?.sourceType === "branch",
90
+ })
91
+ ) {
92
+ candidates.add(cls);
67
93
  }
68
94
 
69
95
  return candidates;
@@ -14,6 +14,7 @@ import { joinPath } from "../../../utils/path-utils.js";
14
14
  import {
15
15
  formatCSSError,
16
16
  generateTailwindCSS,
17
+ getProjectCSS,
17
18
  } from "../../../html/styles-builder/tailwind-compiler.js";
18
19
  import { DEFAULT_STYLESHEET } from "../../../html/styles-builder/css-hash-cache.js";
19
20
  import { serverLogger } from "../../../utils/index.js";
@@ -21,6 +22,10 @@ import { extractProjectCandidates } from "./styles-candidate-scanner.js";
21
22
 
22
23
  const logger = serverLogger.component("styles-css-handler");
23
24
 
25
+ type GeneratedStylesResult =
26
+ | Awaited<ReturnType<typeof generateTailwindCSS>>
27
+ | Awaited<ReturnType<typeof getProjectCSS>>;
28
+
24
29
  export class StylesCSSHandler extends BaseHandler {
25
30
  metadata: HandlerMetadata = {
26
31
  name: "StylesCSSHandler",
@@ -54,11 +59,9 @@ export class StylesCSSHandler extends BaseHandler {
54
59
  });
55
60
  candidates = new Set<string>();
56
61
  }
57
- const result = await generateTailwindCSS(rawCss, candidates, {
58
- projectSlug: ctx.projectSlug,
59
- });
62
+ const result = await this.generateStylesheet(ctx, rawCss, candidates);
60
63
 
61
- if (result.error) {
64
+ if ("error" in result && result.error) {
62
65
  const formatted = formatCSSError(result.error);
63
66
  logger.error("Tailwind error", {
64
67
  error: formatted.message,
@@ -104,6 +107,8 @@ body::before {
104
107
  logger.debug("CSS generated", {
105
108
  candidates: candidates.size,
106
109
  cssLength: result.css.length,
110
+ fromCache: "fromCache" in result ? result.fromCache : false,
111
+ cssHash: "hash" in result ? result.hash : undefined,
107
112
  });
108
113
 
109
114
  return this.respond(
@@ -144,4 +149,20 @@ body::before {
144
149
  return DEFAULT_STYLESHEET;
145
150
  }
146
151
  }
152
+
153
+ private generateStylesheet(
154
+ ctx: HandlerContext,
155
+ rawCss: string,
156
+ candidates: Set<string>,
157
+ ): Promise<GeneratedStylesResult> {
158
+ if (!ctx.projectSlug) {
159
+ return generateTailwindCSS(rawCss, candidates);
160
+ }
161
+
162
+ return getProjectCSS(ctx.projectSlug, rawCss, candidates, {
163
+ minify: true,
164
+ environment: "preview",
165
+ buildMode: "production",
166
+ });
167
+ }
147
168
  }