-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcss_cache.ts
More file actions
210 lines (194 loc) · 7.35 KB
/
css_cache.ts
File metadata and controls
210 lines (194 loc) · 7.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* Cache infrastructure for incremental CSS class extraction.
*
* Provides per-file caching with content hash validation to avoid
* re-extracting classes from unchanged files.
*
* @module
*/
import {join} from 'node:path';
import {hash_insecure} from '@fuzdev/fuz_util/hash.js';
import type {SourceLocation, ExtractionDiagnostic} from './diagnostics.js';
import type {ExtractionData} from './css_class_extractor.js';
import type {CacheOperations} from './operations.js';
/**
* Default cache directory relative to project root.
*/
export const DEFAULT_CACHE_DIR = '.fuz/cache/css';
/**
* CSS cache version. Bump when any of these change:
* - `CachedExtraction` schema
* - `extract_css_classes_with_locations()` logic or output
* - `ExtractionDiagnostic` or `SourceLocation` structure
*
* v1: Initial version with classes and diagnostics
* v2: Use null instead of empty arrays, add explicit_classes, elements, css_variables
* v3: Add explicit_elements, explicit_variables for @fuz-elements/@fuz-variables comments
* v4: Filter incomplete CSS variables in dynamic templates (e.g., `var(--prefix_{expr})`)
* v5: Remove css_variables and explicit_variables (now detected via simple regex scan)
* v6: Re-add explicit_variables for @fuz-variables comments (regex scan misses dynamic templates)
*/
export const CSS_CACHE_VERSION = 6;
/**
* Cached extraction result for a single file.
* Uses `null` instead of empty arrays to avoid allocation overhead.
*/
export interface CachedExtraction {
/** Cache version - invalidates cache when bumped */
v: number;
/** SHA-256 hash of the source file contents */
content_hash: string;
/** Classes as [name, locations] tuples, or null if none */
classes: Array<[string, Array<SourceLocation>]> | null;
/** Classes from @fuz-classes comments, or null if none */
explicit_classes: Array<string> | null;
/** Extraction diagnostics, or null if none */
diagnostics: Array<ExtractionDiagnostic> | null;
/** HTML elements found in the file, or null if none */
elements: Array<string> | null;
/** Elements from @fuz-elements comments, or null if none */
explicit_elements: Array<string> | null;
/** Variables from @fuz-variables comments, or null if none */
explicit_variables: Array<string> | null;
}
/**
* Computes the cache file path for a source file.
* Cache structure mirrors source tree: `src/lib/Foo.svelte` → `.fuz/cache/css/src/lib/Foo.svelte.json`
*
* @param source_path - absolute path to the source file
* @param cache_dir - absolute path to the cache directory
* @param project_root - normalized project root (must end with `/`)
*/
export const get_cache_path = (
source_path: string,
cache_dir: string,
project_root: string,
): string => {
if (!source_path.startsWith(project_root)) {
throw new Error(`Source path "${source_path}" is not under project root "${project_root}"`);
}
const relative = source_path.slice(project_root.length);
return join(cache_dir, relative + '.json');
};
/**
* Computes cache path for a file, handling both internal and external paths.
* Internal files use relative paths mirroring source tree.
* External files (outside project root) use hashed absolute paths in `_external/`.
*
* @param file_id - absolute path to the source file
* @param cache_dir - absolute path to the cache directory
* @param project_root - normalized project root (must end with `/`)
*/
export const get_file_cache_path = (
file_id: string,
cache_dir: string,
project_root: string,
): string => {
const is_internal = file_id.startsWith(project_root);
return is_internal
? get_cache_path(file_id, cache_dir, project_root)
: join(cache_dir, '_external', hash_insecure(file_id).slice(0, 16) + '.json');
};
/**
* Loads a cached extraction result from disk.
* Returns `null` if the cache is missing, corrupted, or has a version mismatch.
* This makes the cache self-healing: any error triggers re-extraction.
*
* @param ops - filesystem operations for dependency injection
* @param cache_path - absolute path to the cache file
*/
export const load_cached_extraction = async (
ops: CacheOperations,
cache_path: string,
): Promise<CachedExtraction | null> => {
try {
const content = await ops.read_text({path: cache_path});
if (!content) return null;
const cached = JSON.parse(content) as CachedExtraction;
// Invalidate if version mismatch
if (cached.v !== CSS_CACHE_VERSION) {
return null;
}
return cached;
} catch {
// Handles: invalid JSON, truncated file
// All cases: return null to trigger re-extraction (self-healing)
return null;
}
};
/**
* Saves an extraction result to the cache.
* Uses atomic write (temp file + rename) for crash safety.
* Normalizes empty collections to null to avoid allocation overhead on load.
*
* @param ops - filesystem operations for dependency injection
* @param cache_path - absolute path to the cache file
* @param content_hash - SHA-256 hash of the source file contents
* @param extraction - extraction data to cache
*/
export const save_cached_extraction = async (
ops: CacheOperations,
cache_path: string,
content_hash: string,
extraction: ExtractionData,
): Promise<void> => {
// Convert to null if empty to save allocation on load
const classes_array =
extraction.classes && extraction.classes.size > 0
? Array.from(extraction.classes.entries())
: null;
const explicit_array =
extraction.explicit_classes && extraction.explicit_classes.size > 0
? Array.from(extraction.explicit_classes)
: null;
const diagnostics_array =
extraction.diagnostics && extraction.diagnostics.length > 0 ? extraction.diagnostics : null;
const elements_array =
extraction.elements && extraction.elements.size > 0 ? Array.from(extraction.elements) : null;
const explicit_elements_array =
extraction.explicit_elements && extraction.explicit_elements.size > 0
? Array.from(extraction.explicit_elements)
: null;
const explicit_variables_array =
extraction.explicit_variables && extraction.explicit_variables.size > 0
? Array.from(extraction.explicit_variables)
: null;
const data: CachedExtraction = {
v: CSS_CACHE_VERSION,
content_hash,
classes: classes_array,
explicit_classes: explicit_array,
diagnostics: diagnostics_array,
elements: elements_array,
explicit_elements: explicit_elements_array,
explicit_variables: explicit_variables_array,
};
await ops.write_text_atomic({path: cache_path, content: JSON.stringify(data)});
};
/**
* Deletes a cached extraction file.
* Silently succeeds if the file doesn't exist.
*
* @param ops - filesystem operations for dependency injection
* @param cache_path - absolute path to the cache file
*/
export const delete_cached_extraction = async (
ops: CacheOperations,
cache_path: string,
): Promise<void> => {
await ops.unlink({path: cache_path});
};
/**
* Converts a cached extraction back to the runtime format.
* Preserves null semantics (null = empty).
*
* @param cached - cached extraction data
*/
export const from_cached_extraction = (cached: CachedExtraction): ExtractionData => ({
classes: cached.classes ? new Map(cached.classes) : null,
explicit_classes: cached.explicit_classes ? new Set(cached.explicit_classes) : null,
diagnostics: cached.diagnostics,
elements: cached.elements ? new Set(cached.elements) : null,
explicit_elements: cached.explicit_elements ? new Set(cached.explicit_elements) : null,
explicit_variables: cached.explicit_variables ? new Set(cached.explicit_variables) : null,
});