veryfront 0.1.89 → 0.1.90

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.
Files changed (58) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts +3 -0
  2. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  3. package/esm/cli/commands/knowledge/command.js +27 -2
  4. package/esm/deno.js +1 -1
  5. package/esm/src/html/styles-builder/candidate-extractor.d.ts +5 -1
  6. package/esm/src/html/styles-builder/candidate-extractor.d.ts.map +1 -1
  7. package/esm/src/html/styles-builder/candidate-extractor.js +6 -1
  8. package/esm/src/html/styles-builder/content-version.d.ts +9 -0
  9. package/esm/src/html/styles-builder/content-version.d.ts.map +1 -0
  10. package/esm/src/html/styles-builder/content-version.js +15 -0
  11. package/esm/src/html/styles-builder/css-pregeneration.d.ts +11 -0
  12. package/esm/src/html/styles-builder/css-pregeneration.d.ts.map +1 -1
  13. package/esm/src/html/styles-builder/css-pregeneration.js +15 -10
  14. package/esm/src/html/styles-builder/prepared-project-css-cache.d.ts +31 -0
  15. package/esm/src/html/styles-builder/prepared-project-css-cache.d.ts.map +1 -0
  16. package/esm/src/html/styles-builder/prepared-project-css-cache.js +165 -0
  17. package/esm/src/html/styles-builder/style-scope-profile.d.ts +14 -0
  18. package/esm/src/html/styles-builder/style-scope-profile.d.ts.map +1 -0
  19. package/esm/src/html/styles-builder/style-scope-profile.js +121 -0
  20. package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
  21. package/esm/src/platform/adapters/fs/factory.js +2 -21
  22. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  23. package/esm/src/platform/adapters/fs/veryfront/adapter.js +44 -19
  24. package/esm/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.d.ts +3 -0
  25. package/esm/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.d.ts.map +1 -0
  26. package/esm/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.js +25 -0
  27. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts.map +1 -1
  28. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.js +2 -15
  29. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +4 -0
  30. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  31. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +2 -0
  32. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +3 -0
  33. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -1
  34. package/esm/src/rendering/orchestrator/css-candidate-manifest.js +10 -5
  35. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  36. package/esm/src/rendering/orchestrator/html.js +8 -0
  37. package/esm/src/server/handlers/dev/styles-candidate-scanner.d.ts.map +1 -1
  38. package/esm/src/server/handlers/dev/styles-candidate-scanner.js +14 -16
  39. package/esm/src/server/handlers/dev/styles-css.handler.d.ts +2 -0
  40. package/esm/src/server/handlers/dev/styles-css.handler.d.ts.map +1 -1
  41. package/esm/src/server/handlers/dev/styles-css.handler.js +55 -8
  42. package/package.json +1 -1
  43. package/src/cli/commands/knowledge/command.ts +30 -2
  44. package/src/deno.js +1 -1
  45. package/src/src/html/styles-builder/candidate-extractor.ts +13 -0
  46. package/src/src/html/styles-builder/content-version.ts +20 -0
  47. package/src/src/html/styles-builder/css-pregeneration.ts +49 -12
  48. package/src/src/html/styles-builder/prepared-project-css-cache.ts +228 -0
  49. package/src/src/html/styles-builder/style-scope-profile.ts +164 -0
  50. package/src/src/platform/adapters/fs/factory.ts +2 -27
  51. package/src/src/platform/adapters/fs/veryfront/adapter.ts +49 -20
  52. package/src/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.ts +35 -0
  53. package/src/src/platform/adapters/fs/veryfront/proxy-manager.ts +4 -21
  54. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +3 -0
  55. package/src/src/rendering/orchestrator/css-candidate-manifest.ts +28 -6
  56. package/src/src/rendering/orchestrator/html.ts +11 -0
  57. package/src/src/server/handlers/dev/styles-candidate-scanner.ts +18 -15
  58. package/src/src/server/handlers/dev/styles-css.handler.ts +88 -16
@@ -245,6 +245,7 @@ export class VeryfrontFSAdapter implements FSAdapter {
245
245
  },
246
246
  clearFileListIndex: () => this.readOps.clearFileListIndex(),
247
247
  setFileListCache: (cacheKey, files) => this.cache.setAsync(cacheKey, files),
248
+ pregenerateStyles: (files) => this.triggerCSSPregeneration(files),
248
249
  });
249
250
 
250
251
  logger.debug("Created", {
@@ -349,12 +350,10 @@ export class VeryfrontFSAdapter implements FSAdapter {
349
350
  sourceFilesWithContent: fileSummary.sourceFilesWithContent,
350
351
  });
351
352
 
352
- // Trigger CSS pre-generation for non-branch environments (fire-and-forget)
353
- // This runs in parallel with the rest of initialization
354
- if (
355
- this.contentContext.sourceType !== "branch" &&
356
- fileSummary.sourceFilesWithContent > 0
357
- ) {
353
+ // Trigger CSS pre-generation after the initial file snapshot is ready.
354
+ // This keeps stylesheet generation off the first styles request for both
355
+ // preview branches and published content.
356
+ if (fileSummary.sourceFilesWithContent > 0) {
358
357
  this.triggerCSSPregeneration(files).catch(() => {
359
358
  // Error already logged in triggerCSSPregeneration
360
359
  });
@@ -445,6 +444,13 @@ export class VeryfrontFSAdapter implements FSAdapter {
445
444
 
446
445
  const files = await fetchFileListForContext(this.client, warmupContext);
447
446
  await this.cache.setAsync(effectiveCacheKey, files);
447
+ const fileSummary = summarizeFileList(files);
448
+
449
+ if (fileSummary.sourceFilesWithContent > 0) {
450
+ this.triggerCSSPregeneration(files).catch(() => {
451
+ // Error already logged in triggerCSSPregeneration
452
+ });
453
+ }
448
454
 
449
455
  logger.debug("File list warmup complete", {
450
456
  reason,
@@ -716,34 +722,57 @@ export class VeryfrontFSAdapter implements FSAdapter {
716
722
  const { pregenerateCSSFromFiles, findStylesheetFromFiles } = await import(
717
723
  "../../../../html/styles-builder/css-pregeneration.js"
718
724
  );
725
+ const { resolveStyleContentVersion } = await import(
726
+ "../../../../html/styles-builder/content-version.js"
727
+ );
728
+ const { createStyleScopeProfile } = await import(
729
+ "../../../../html/styles-builder/style-scope-profile.js"
730
+ );
719
731
 
720
732
  let stylesheetPath: string | undefined;
733
+ let styleProfile = createStyleScopeProfile();
721
734
  const projectDir = this.normalizer.getProjectDir();
722
735
 
723
- if (projectDir) {
724
- try {
725
- const { runtime } = await import("../../registry.js");
726
- const { getConfig } = await import("../../../../config/index.js");
727
- const adapter = await runtime.get();
728
- const cacheKey = this.client.getProjectId() || this.projectSlug;
729
- const config = await getConfig(projectDir, adapter, { cacheKey });
730
- stylesheetPath = config?.tailwind?.stylesheet;
731
- } catch (error) {
732
- logger.debug("Failed to load config for CSS pre-generation", {
733
- projectSlug: this.projectSlug,
734
- error: error instanceof Error ? error.message : String(error),
735
- });
736
- }
736
+ if (!projectDir) {
737
+ logger.debug("Skipping CSS pre-generation without projectDir", {
738
+ projectSlug: this.projectSlug,
739
+ });
740
+ return;
741
+ }
742
+
743
+ try {
744
+ const { runtime } = await import("../../registry.js");
745
+ const { getConfig } = await import("../../../../config/index.js");
746
+ const adapter = await runtime.get();
747
+ const cacheKey = this.client.getProjectId() || this.projectSlug;
748
+ const config = await getConfig(projectDir, adapter, { cacheKey });
749
+ stylesheetPath = config?.tailwind?.stylesheet;
750
+ styleProfile = createStyleScopeProfile(config);
751
+ } catch (error) {
752
+ logger.debug("Failed to load config for CSS pre-generation", {
753
+ projectSlug: this.projectSlug,
754
+ error: error instanceof Error ? error.message : String(error),
755
+ });
737
756
  }
738
757
 
739
758
  const stylesheet = findStylesheetFromFiles(files, stylesheetPath);
759
+ const projectVersion = resolveStyleContentVersion(this.contentContext, {
760
+ branch: this.contentContext?.branch ?? null,
761
+ releaseId: this.contentContext?.releaseId ?? null,
762
+ environmentName: this.contentContext?.environmentName ?? null,
763
+ });
740
764
 
741
765
  await pregenerateCSSFromFiles({
742
766
  projectSlug: this.projectSlug,
767
+ projectVersion,
768
+ projectDir,
743
769
  files,
770
+ styleProfile,
744
771
  stylesheet,
745
772
  stylesheetPath,
746
773
  minify: true,
774
+ environment: "preview",
775
+ buildMode: "production",
747
776
  });
748
777
  } catch (error) {
749
778
  logger.warn("CSS pre-generation failed", {
@@ -0,0 +1,35 @@
1
+ import {
2
+ clearSSRModuleCache,
3
+ clearSSRModuleCacheForProject,
4
+ } from "../../../../modules/react-loader/ssr-module-loader/cache/index.js";
5
+ import { clearRouterDetectionCacheForProject } from "../../../../rendering/router-detection.js";
6
+ import {
7
+ clearModulePathCache,
8
+ invalidateModulePaths,
9
+ } from "../../../../transforms/mdx/esm-module-loader/cache/index.js";
10
+ import { clearSnippetCacheForProject } from "../../../../rendering/snippet-renderer.js";
11
+ import { clearRendererCacheForProject } from "../../../../rendering/renderer.js";
12
+ import { invalidateProjectCSS } from "../../../../html/styles-builder/tailwind-compiler.js";
13
+ import { invalidatePreparedProjectCSS } from "../../../../html/styles-builder/prepared-project-css-cache.js";
14
+ import { invalidateProjectCandidateManifests } from "../../../../rendering/orchestrator/css-candidate-manifest.js";
15
+ import type { InvalidationCallbacks } from "./types.js";
16
+
17
+ export function createDefaultInvalidationCallbacks(
18
+ callbacks?: InvalidationCallbacks,
19
+ ): InvalidationCallbacks {
20
+ return {
21
+ clearSSRModuleCache,
22
+ clearModulePathCache,
23
+ invalidateModulePaths,
24
+ clearSSRModuleCacheForProject,
25
+ clearRouterDetectionCacheForProject,
26
+ clearSnippetCacheForProject,
27
+ clearRendererCacheForProject,
28
+ clearProjectCSSCache: (projectSlug: string) => {
29
+ invalidateProjectCSS(projectSlug);
30
+ invalidatePreparedProjectCSS(projectSlug);
31
+ invalidateProjectCandidateManifests(projectSlug);
32
+ },
33
+ ...callbacks,
34
+ };
35
+ }
@@ -4,18 +4,8 @@ import { CACHE_INVARIANT_VIOLATION, INVALID_ARGUMENT } from "../../../../errors/
4
4
  import { buildProxyManagerCacheKey } from "../../../../cache/index.js";
5
5
  import { VeryfrontFSAdapter } from "./index.js";
6
6
  import type { CacheStats, FSAdapterConfig, ResolvedContentContext } from "./types.js";
7
- import {
8
- clearSSRModuleCache,
9
- clearSSRModuleCacheForProject,
10
- } from "../../../../modules/react-loader/ssr-module-loader/cache/index.js";
11
- import { clearRouterDetectionCacheForProject } from "../../../../rendering/router-detection.js";
12
- import {
13
- clearModulePathCache,
14
- invalidateModulePaths,
15
- } from "../../../../transforms/mdx/esm-module-loader/cache/index.js";
16
- import { clearSnippetCacheForProject } from "../../../../rendering/snippet-renderer.js";
17
- import { clearRendererCacheForProject } from "../../../../rendering/renderer.js";
18
7
  import { GetAdapterParamsSchema } from "./schemas/index.js";
8
+ import { createDefaultInvalidationCallbacks } from "./default-invalidation-callbacks.js";
19
9
 
20
10
  const logger = baseLogger.component("proxy-fs-adapter-manager");
21
11
 
@@ -306,16 +296,9 @@ export class ProxyFSAdapterManager {
306
296
  projectId,
307
297
  apiToken: effectiveToken,
308
298
  },
309
- invalidationCallbacks: {
310
- clearSSRModuleCache,
311
- clearModulePathCache,
312
- invalidateModulePaths,
313
- clearSSRModuleCacheForProject,
314
- clearRouterDetectionCacheForProject,
315
- clearSnippetCacheForProject,
316
- clearRendererCacheForProject,
317
- ...this.baseConfig.invalidationCallbacks,
318
- },
299
+ invalidationCallbacks: createDefaultInvalidationCallbacks(
300
+ this.baseConfig.invalidationCallbacks,
301
+ ),
319
302
  };
320
303
 
321
304
  const adapter = new VeryfrontFSAdapter(config);
@@ -43,6 +43,7 @@ interface WebSocketDeps {
43
43
  cacheKey: string,
44
44
  files: Array<{ path: string; content?: string }>,
45
45
  ) => Promise<void>;
46
+ pregenerateStyles?: (files: Array<{ path: string; content?: string }>) => Promise<void>;
46
47
  }
47
48
 
48
49
  export class WebSocketManager {
@@ -629,6 +630,7 @@ export class WebSocketManager {
629
630
  const cacheKey = buildFileListCacheKey(contentContext);
630
631
  await this.deps.setFileListCache(cacheKey, files);
631
632
  this.deps.clearFileListIndex();
633
+ await this.deps.pregenerateStyles?.(files);
632
634
 
633
635
  logger.debug("Fresh files cached (memory + Redis)", {
634
636
  cacheKey,
@@ -771,6 +773,7 @@ export class WebSocketManager {
771
773
  const cacheKey = buildFileListCacheKey(contentContext);
772
774
  await this.deps.setFileListCache(cacheKey, files);
773
775
  this.deps.clearFileListIndex();
776
+ await this.deps.pregenerateStyles?.(files);
774
777
 
775
778
  logger.debug("FRESH FILES FETCHED", {
776
779
  cacheKey,
@@ -1,4 +1,8 @@
1
1
  import { extractCandidates } from "../../html/styles-builder/tailwind-compiler.js";
2
+ import {
3
+ filterFilesForStyleScope,
4
+ type StyleScopeProfile,
5
+ } from "../../html/styles-builder/style-scope-profile.js";
2
6
  import { getRouteModulePaths } from "../../modules/manifest/route-module-manifest.js";
3
7
  import { rendererLogger } from "../../utils/index.js";
4
8
 
@@ -17,6 +21,7 @@ interface RouteCandidateOptions {
17
21
  projectScope: string;
18
22
  projectVersion: string;
19
23
  projectDir: string;
24
+ styleProfile?: StyleScopeProfile;
20
25
  routeKey: string;
21
26
  routeFilePaths: string[];
22
27
  files: SourceFileLike[];
@@ -27,6 +32,7 @@ interface ProjectCandidateOptions {
27
32
  projectScope: string;
28
33
  projectVersion: string;
29
34
  projectDir: string;
35
+ styleProfile?: StyleScopeProfile;
30
36
  files: SourceFileLike[];
31
37
  developmentMode: boolean;
32
38
  }
@@ -51,8 +57,12 @@ function toRelativeProjectPath(path: string, projectDir: string): string {
51
57
  return normalized.replace(/^\/+/, "");
52
58
  }
53
59
 
54
- function buildManifestCacheKey(projectScope: string, projectVersion: string): string {
55
- return `${projectScope}:${projectVersion}`;
60
+ function buildManifestCacheKey(
61
+ projectScope: string,
62
+ projectVersion: string,
63
+ styleProfileHash?: string,
64
+ ): string {
65
+ return `${projectScope}:${projectVersion}:${styleProfileHash ?? "default"}`;
56
66
  }
57
67
 
58
68
  function shouldRebuildManifest(
@@ -101,13 +111,20 @@ function buildCandidateManifest(files: SourceFileLike[], projectDir: string): Ca
101
111
  function getOrBuildManifest(
102
112
  options: Pick<
103
113
  ProjectCandidateOptions,
104
- "projectScope" | "projectVersion" | "projectDir" | "files" | "developmentMode"
114
+ "projectScope" | "projectVersion" | "projectDir" | "files" | "developmentMode" | "styleProfile"
105
115
  >,
106
116
  ): CandidateManifest {
107
- const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
117
+ const manifestKey = buildManifestCacheKey(
118
+ options.projectScope,
119
+ options.projectVersion,
120
+ options.styleProfile?.hash,
121
+ );
108
122
  const existingManifest = manifestCache.get(manifestKey);
123
+ const scopedFiles = options.styleProfile
124
+ ? filterFilesForStyleScope(options.files, options.styleProfile, options.projectDir)
125
+ : options.files;
109
126
  const manifest = shouldRebuildManifest(existingManifest, options.developmentMode)
110
- ? buildCandidateManifest(options.files, options.projectDir)
127
+ ? buildCandidateManifest(scopedFiles, options.projectDir)
111
128
  : existingManifest!;
112
129
 
113
130
  if (manifest !== existingManifest) {
@@ -139,7 +156,11 @@ function addCandidatesForPath(
139
156
  * Resolve route-scoped Tailwind candidates from a precomputed per-project manifest.
140
157
  */
141
158
  export function getRouteCandidates(options: RouteCandidateOptions): Set<string> {
142
- const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
159
+ const manifestKey = buildManifestCacheKey(
160
+ options.projectScope,
161
+ options.projectVersion,
162
+ options.styleProfile?.hash,
163
+ );
143
164
  const manifest = getOrBuildManifest(options);
144
165
  const routeCacheKey = `${manifestKey}:${options.routeKey}`;
145
166
  const cachedRoute = routeCandidateCache.get(routeCacheKey);
@@ -167,6 +188,7 @@ export function getRouteCandidates(options: RouteCandidateOptions): Set<string>
167
188
  logger.debug("Resolved route candidates", {
168
189
  projectScope: options.projectScope,
169
190
  projectVersion: options.projectVersion,
191
+ styleProfileHash: options.styleProfile?.hash,
170
192
  route: options.routeKey,
171
193
  count: routeCandidates.size,
172
194
  });
@@ -29,6 +29,9 @@ import {
29
29
  rewriteCssModuleContent,
30
30
  } from "../../transforms/css-modules/naming.js";
31
31
  import { getRouteCandidates } from "./css-candidate-manifest.js";
32
+ import { resolveStyleContentVersion } from "../../html/styles-builder/content-version.js";
33
+ import { createStyleScopeProfile } from "../../html/styles-builder/style-scope-profile.js";
34
+ import type { ResolvedContentContext } from "../../platform/adapters/fs/veryfront/types.js";
32
35
 
33
36
  const logger = rendererLogger.component("html-generator");
34
37
 
@@ -453,9 +456,16 @@ export class HTMLGenerator {
453
456
  if (typeof wrappedFs.getUnderlyingAdapter !== "function") return undefined;
454
457
 
455
458
  const fsAdapter = wrappedFs.getUnderlyingAdapter() as {
459
+ getContentContext?: () => ResolvedContentContext | null;
456
460
  getProjectData?: () => { updated_at?: string } | undefined;
457
461
  };
458
462
 
463
+ const contentContext = typeof fsAdapter.getContentContext === "function"
464
+ ? fsAdapter.getContentContext()
465
+ : null;
466
+
467
+ if (contentContext) return resolveStyleContentVersion(contentContext);
468
+
459
469
  return fsAdapter.getProjectData?.()?.updated_at;
460
470
  }
461
471
 
@@ -505,6 +515,7 @@ export class HTMLGenerator {
505
515
  projectScope,
506
516
  projectVersion,
507
517
  projectDir: this.config.projectDir,
518
+ styleProfile: createStyleScopeProfile(this.config.config),
508
519
  routeKey,
509
520
  routeFilePaths,
510
521
  files,
@@ -9,6 +9,12 @@
9
9
  */
10
10
 
11
11
  import { extractCandidates } from "../../../html/styles-builder/tailwind-compiler.js";
12
+ import { resolveStyleContentVersion } from "../../../html/styles-builder/content-version.js";
13
+ import {
14
+ createStyleScopeProfile,
15
+ shouldIncludeStylePath,
16
+ shouldTraverseStyleDirectory,
17
+ } from "../../../html/styles-builder/style-scope-profile.js";
12
18
  import { serverLogger } from "../../../utils/index.js";
13
19
  import { createFileSystem } from "../../../platform/compat/fs.js";
14
20
  import { join } from "../../../platform/compat/path/index.js";
@@ -20,7 +26,6 @@ import { FRAMEWORK_CANDIDATES } from "./framework-candidates.generated.js";
20
26
  const logger = serverLogger.component("styles-candidate-scanner");
21
27
 
22
28
  const SOURCE_EXTENSIONS = [".tsx", ".jsx", ".mdx", ".ts", ".js"];
23
- const SKIP_DIRS = new Set(["node_modules", ".cache", ".git", "dist", "build", ".vscode"]);
24
29
 
25
30
  /** De-duplicated set of framework candidates, computed once at import time. */
26
31
  const frameworkCandidates = new Set<string>(FRAMEWORK_CANDIDATES);
@@ -32,18 +37,6 @@ interface SourceFileProvider {
32
37
  getContentContext?: () => ResolvedContentContext | null;
33
38
  }
34
39
 
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
-
47
40
  /**
48
41
  * Extract Tailwind CSS candidate class names from all project source files.
49
42
  *
@@ -52,6 +45,7 @@ function resolveProjectVersion(
52
45
  * method is available (local dev mode).
53
46
  */
54
47
  export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set<string>> {
48
+ const styleProfile = createStyleScopeProfile(ctx.config);
55
49
  const wrappedFs = ctx.adapter.fs as { getUnderlyingAdapter?: () => unknown };
56
50
 
57
51
  if (typeof wrappedFs.getUnderlyingAdapter !== "function") {
@@ -83,8 +77,13 @@ export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set
83
77
  for (
84
78
  const cls of getProjectCandidates({
85
79
  projectScope: ctx.projectSlug ?? contentContext?.projectSlug ?? ctx.projectDir,
86
- projectVersion: resolveProjectVersion(ctx, contentContext),
80
+ projectVersion: resolveStyleContentVersion(contentContext, {
81
+ releaseId: ctx.releaseId,
82
+ branch: ctx.parsedDomain?.branch,
83
+ environmentName: ctx.environmentName,
84
+ }),
87
85
  projectDir: ctx.projectDir,
86
+ styleProfile,
88
87
  files,
89
88
  developmentMode: contentContext?.sourceType === "branch",
90
89
  })
@@ -100,6 +99,7 @@ export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set
100
99
  * Used in local development mode where projects are read directly from disk.
101
100
  */
102
101
  async function scanLocalFiles(projectDir: string, ctx: HandlerContext): Promise<Set<string>> {
102
+ const styleProfile = createStyleScopeProfile(ctx.config);
103
103
  const candidates = new Set<string>(frameworkCandidates);
104
104
  const fs = createFileSystem();
105
105
 
@@ -116,11 +116,14 @@ async function scanLocalFiles(projectDir: string, ctx: HandlerContext): Promise<
116
116
  const fullPath = join(dir, entry.name);
117
117
 
118
118
  if (entry.isDirectory) {
119
- if (!SKIP_DIRS.has(entry.name)) await scanDir(fullPath);
119
+ if (shouldTraverseStyleDirectory(styleProfile, fullPath, projectDir)) {
120
+ await scanDir(fullPath);
121
+ }
120
122
  continue;
121
123
  }
122
124
 
123
125
  if (!entry.isFile) continue;
126
+ if (!shouldIncludeStylePath(styleProfile, fullPath, projectDir)) continue;
124
127
  if (!SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) continue;
125
128
 
126
129
  try {
@@ -11,20 +11,22 @@ import { BaseHandler } from "../response/base.js";
11
11
  import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
12
12
  import { HTTP_OK, PRIORITY_HIGH_DEV } from "../../../utils/constants/index.js";
13
13
  import { joinPath } from "../../../utils/path-utils.js";
14
- import {
15
- formatCSSError,
16
- generateTailwindCSS,
17
- getProjectCSS,
18
- } from "../../../html/styles-builder/tailwind-compiler.js";
14
+ import { formatCSSError, getProjectCSS } from "../../../html/styles-builder/tailwind-compiler.js";
19
15
  import { DEFAULT_STYLESHEET } from "../../../html/styles-builder/css-hash-cache.js";
16
+ import { resolveStyleContentVersion } from "../../../html/styles-builder/content-version.js";
17
+ import {
18
+ createPreparedProjectCSSContext,
19
+ storePreparedProjectCSS,
20
+ tryGetPreparedProjectCSS,
21
+ } from "../../../html/styles-builder/prepared-project-css-cache.js";
22
+ import { createStyleScopeProfile } from "../../../html/styles-builder/style-scope-profile.js";
20
23
  import { serverLogger } from "../../../utils/index.js";
24
+ import type { ResolvedContentContext } from "../../../platform/adapters/fs/veryfront/types.js";
21
25
  import { extractProjectCandidates } from "./styles-candidate-scanner.js";
22
26
 
23
27
  const logger = serverLogger.component("styles-css-handler");
24
28
 
25
- type GeneratedStylesResult =
26
- | Awaited<ReturnType<typeof generateTailwindCSS>>
27
- | Awaited<ReturnType<typeof getProjectCSS>>;
29
+ type GeneratedStylesResult = Awaited<ReturnType<typeof getProjectCSS>>;
28
30
 
29
31
  export class StylesCSSHandler extends BaseHandler {
30
32
  metadata: HandlerMetadata = {
@@ -40,6 +42,9 @@ export class StylesCSSHandler extends BaseHandler {
40
42
  try {
41
43
  return await this.withProxyContext(ctx, async () => {
42
44
  const responseBuilder = this.createResponseBuilder(ctx).withCache("no-cache");
45
+ const projectScope = ctx.projectSlug ?? ctx.projectDir;
46
+ const styleProfile = createStyleScopeProfile(ctx.config);
47
+ const contentContext = this.getContentContext(ctx);
43
48
  let rawCss: string;
44
49
  try {
45
50
  rawCss = await this.loadStylesheet(ctx);
@@ -49,6 +54,29 @@ export class StylesCSSHandler extends BaseHandler {
49
54
  });
50
55
  rawCss = DEFAULT_STYLESHEET;
51
56
  }
57
+ const preparedContext = this.createPreparedCSSContext(
58
+ projectScope,
59
+ rawCss,
60
+ styleProfile.hash,
61
+ contentContext,
62
+ ctx,
63
+ );
64
+
65
+ if (preparedContext) {
66
+ const prepared = await tryGetPreparedProjectCSS(preparedContext);
67
+ if (prepared) {
68
+ logger.debug("Prepared CSS cache hit", {
69
+ projectScope,
70
+ projectVersion: preparedContext.projectVersion,
71
+ styleProfileHash: styleProfile.hash,
72
+ cssHash: prepared.hash,
73
+ });
74
+
75
+ return this.respond(
76
+ responseBuilder.withContentType("text/css; charset=utf-8", prepared.css, HTTP_OK),
77
+ );
78
+ }
79
+ }
52
80
 
53
81
  let candidates: Set<string>;
54
82
  try {
@@ -59,10 +87,11 @@ export class StylesCSSHandler extends BaseHandler {
59
87
  });
60
88
  candidates = new Set<string>();
61
89
  }
62
- const result = await this.generateStylesheet(ctx, rawCss, candidates);
63
-
64
- if ("error" in result && result.error) {
65
- const formatted = formatCSSError(result.error);
90
+ let result: GeneratedStylesResult;
91
+ try {
92
+ result = await this.generateStylesheet(ctx, rawCss, candidates);
93
+ } catch (error) {
94
+ const formatted = formatCSSError(error instanceof Error ? error : String(error));
66
95
  logger.error("Tailwind error", {
67
96
  error: formatted.message,
68
97
  suggestion: formatted.suggestion,
@@ -105,12 +134,20 @@ body::before {
105
134
  }
106
135
 
107
136
  logger.debug("CSS generated", {
137
+ projectScope,
108
138
  candidates: candidates.size,
109
139
  cssLength: result.css.length,
110
140
  fromCache: "fromCache" in result ? result.fromCache : false,
111
141
  cssHash: "hash" in result ? result.hash : undefined,
112
142
  });
113
143
 
144
+ if (preparedContext && "hash" in result) {
145
+ await storePreparedProjectCSS(preparedContext, {
146
+ css: result.css,
147
+ hash: result.hash,
148
+ });
149
+ }
150
+
114
151
  return this.respond(
115
152
  responseBuilder.withContentType("text/css; charset=utf-8", result.css, HTTP_OK),
116
153
  );
@@ -155,14 +192,49 @@ body::before {
155
192
  rawCss: string,
156
193
  candidates: Set<string>,
157
194
  ): Promise<GeneratedStylesResult> {
158
- if (!ctx.projectSlug) {
159
- return generateTailwindCSS(rawCss, candidates);
160
- }
195
+ const projectScope = ctx.projectSlug ?? ctx.projectDir;
161
196
 
162
- return getProjectCSS(ctx.projectSlug, rawCss, candidates, {
197
+ return getProjectCSS(projectScope, rawCss, candidates, {
163
198
  minify: true,
164
199
  environment: "preview",
165
200
  buildMode: "production",
166
201
  });
167
202
  }
203
+
204
+ private getContentContext(ctx: HandlerContext): ResolvedContentContext | null {
205
+ const wrappedFs = ctx.adapter.fs as { getUnderlyingAdapter?: () => unknown };
206
+ if (typeof wrappedFs.getUnderlyingAdapter !== "function") return null;
207
+
208
+ const fsAdapter = wrappedFs.getUnderlyingAdapter() as {
209
+ getContentContext?: () => ResolvedContentContext | null;
210
+ };
211
+
212
+ return typeof fsAdapter.getContentContext === "function" ? fsAdapter.getContentContext() : null;
213
+ }
214
+
215
+ private createPreparedCSSContext(
216
+ projectScope: string | undefined,
217
+ rawCss: string,
218
+ styleProfileHash: string,
219
+ contentContext: ResolvedContentContext | null,
220
+ ctx: HandlerContext,
221
+ ) {
222
+ if (!projectScope) return undefined;
223
+
224
+ return createPreparedProjectCSSContext(
225
+ projectScope,
226
+ resolveStyleContentVersion(contentContext, {
227
+ releaseId: ctx.releaseId,
228
+ branch: ctx.parsedDomain?.branch,
229
+ environmentName: ctx.environmentName,
230
+ }),
231
+ rawCss,
232
+ styleProfileHash,
233
+ {
234
+ minify: true,
235
+ environment: "preview",
236
+ buildMode: "production",
237
+ },
238
+ );
239
+ }
168
240
  }