auklet 0.2.10 → 0.3.1

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.
@@ -23,6 +23,7 @@ export declare class StyleProcessor {
23
23
  private throwMissingLocalStyleImport;
24
24
  collectImportedStyleFiles(styleFiles: Array<string>): Set<string>;
25
25
  collectImportedStyleFileReferences(styleFiles: Array<string>): StyleFileImportReference[];
26
+ collectStyleImportReferences(styleFiles: Array<string>): StyleFileImportReference[];
26
27
  collectStyleImportSpecifiers(styleFiles: Array<string>): Set<string>;
27
28
  assertNoLocalStyleImportCycles(styleFiles: Array<string>): void;
28
29
  assertNoSourceRootEscapingLocalStyleImports(styleFiles: Array<string>): void;
@@ -98,6 +98,21 @@ export class StyleProcessor {
98
98
  }
99
99
  return imports;
100
100
  }
101
+ collectStyleImportReferences(styleFiles) {
102
+ const imports = [];
103
+ for (const styleFile of styleFiles) {
104
+ const css = fs.readFileSync(styleFile, 'utf8');
105
+ const root = this.parse(css, styleFile);
106
+ root.walkAtRules('import', (rule) => {
107
+ const specifier = this.parseImportSpecifier(rule.params);
108
+ if (!specifier)
109
+ return;
110
+ const { reference } = this.resolveStyleImportReference(specifier, styleFile);
111
+ imports.push(reference);
112
+ });
113
+ }
114
+ return imports;
115
+ }
101
116
  collectStyleImportSpecifiers(styleFiles) {
102
117
  const specifiers = new Set();
103
118
  for (const styleFile of styleFiles) {
@@ -1,18 +1,42 @@
1
1
  import type { HotUpdateOptions, ViteDevServer } from 'vite';
2
2
  import type { ModuleStyleGraph } from '#auklet/css/vite/moduleGraph/graph';
3
+ type TrackedVirtualStyleFileKind = 'entry' | 'dependency';
4
+ export type AukletStyleHmrOptions = {
5
+ pruneDelayMs?: number;
6
+ };
3
7
  export declare class AukletStyleHmr {
4
8
  private readonly graph;
5
9
  private readonly lastUpdateTimes;
6
10
  private suppressFullReloadUntil;
7
11
  private readonly virtualIdsByDependency;
8
- constructor(graph: () => ModuleStyleGraph);
9
- trackVirtualStyleDependency(file: string, virtualId: string): void;
10
- hasTrackedStyleDependency(file: string): boolean;
12
+ private readonly filesByVirtualId;
13
+ private readonly schedulePruneStaleVirtualDependencies;
14
+ private readonly pruneDelayMs;
15
+ constructor(graph: () => ModuleStyleGraph, options?: AukletStyleHmrOptions);
16
+ trackVirtualStyleDependency(file: string, virtualId: string, kind?: TrackedVirtualStyleFileKind): void;
17
+ replaceVirtualStyleDependency(virtualId: string, files: Array<string>, fileKinds?: Array<{
18
+ file: string;
19
+ kind: TrackedVirtualStyleFileKind;
20
+ }>): void;
21
+ hasTrackedStyleDependency(file: string, moduleGraph?: Pick<ViteDevServer['moduleGraph'], 'getModuleById'>): boolean;
22
+ pruneStaleVirtualDependencies(moduleGraph: Pick<ViteDevServer['moduleGraph'], 'getModuleById'>): void;
23
+ scheduleStaleVirtualDependencyPrune(moduleGraph: Pick<ViteDevServer['moduleGraph'], 'getModuleById'>): void;
24
+ cancelStaleVirtualDependencyPrune(): void;
11
25
  installFullReloadGuard(server: Pick<ViteDevServer, 'ws'>): void;
12
- handleStyleHotUpdate(context: HotUpdateOptions): never[] | undefined;
13
- handleSourceModuleChange(server: Pick<ViteDevServer, 'moduleGraph' | 'ws'>, file: string): boolean;
14
- private sendVirtualStyleUpdates;
26
+ handleStyleHotUpdate(context: HotUpdateOptions): Promise<never[] | undefined>;
27
+ handleSourceModuleChange(server: Pick<ViteDevServer, 'moduleGraph' | 'ws'>, file: string): Promise<boolean>;
28
+ private parseTrackedVirtualIds;
29
+ private createTrackedVirtualStyleUpdates;
30
+ private invalidateTrackedVirtualPackages;
31
+ private getDependencyVirtualIds;
32
+ private getTrackedStyleFileEntries;
33
+ private createTrackedVirtualStyleResults;
34
+ private parseTrackedVirtualId;
35
+ private parseTrackedVirtualIdWithKind;
36
+ private refreshTrackedVirtualStyleUpdates;
37
+ private sameStyleLoadResult;
15
38
  private suppressFullReload;
16
39
  private shouldSuppressFullReload;
17
40
  private isDuplicateUpdate;
18
41
  }
42
+ export {};
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
- import { isString } from 'aidly';
2
+ import { isString, throttle } from 'aidly';
3
3
  import { normalizeFileKey } from '#auklet/utils';
4
4
  import { createAukletLogger } from '#auklet/logger';
5
5
  const FULL_RELOAD_SUPPRESS_MS = 100;
6
6
  const DUPLICATE_UPDATE_IGNORE_MS = 500;
7
+ const RESOLVED_VIRTUAL_ID_PREFIX = '\0auklet-css:';
7
8
  const logger = createAukletLogger({ scope: 'css:vite' });
8
9
  const toBrowserVirtualPath = (id) => {
9
10
  return `/@id/${id.replace('\0', '__x00__')}`;
@@ -11,20 +12,41 @@ const toBrowserVirtualPath = (id) => {
11
12
  const getRelativeFile = (file) => {
12
13
  return path.relative(process.cwd(), file);
13
14
  };
14
- const addVirtualStyleDependency = (virtualIdsByDependency, file, virtualId) => {
15
+ const addVirtualStyleDependency = (virtualIdsByDependency, filesByVirtualId, file, virtualId, kind) => {
15
16
  const normalizedFile = normalizeFileKey(file);
16
17
  const values = virtualIdsByDependency.get(normalizedFile) ?? new Set();
17
18
  values.add(virtualId);
18
19
  virtualIdsByDependency.set(normalizedFile, values);
20
+ const files = filesByVirtualId.get(virtualId) ??
21
+ new Map();
22
+ const previousKind = files.get(normalizedFile);
23
+ files.set(normalizedFile, previousKind === 'entry' || kind === 'entry' ? 'entry' : kind);
24
+ filesByVirtualId.set(virtualId, files);
19
25
  };
20
- const getDependencyVirtualIds = (virtualIdsByDependency, file) => {
21
- return Array.from(virtualIdsByDependency.get(normalizeFileKey(file)) ?? []);
26
+ const removeVirtualStyleDependency = (virtualIdsByDependency, filesByVirtualId, file, virtualId) => {
27
+ const normalizedFile = normalizeFileKey(file);
28
+ const values = virtualIdsByDependency.get(normalizedFile);
29
+ if (values) {
30
+ values.delete(virtualId);
31
+ if (!values.size) {
32
+ virtualIdsByDependency.delete(normalizedFile);
33
+ }
34
+ }
35
+ const files = filesByVirtualId.get(virtualId);
36
+ if (files) {
37
+ files.delete(normalizedFile);
38
+ if (!files.size) {
39
+ filesByVirtualId.delete(virtualId);
40
+ }
41
+ }
22
42
  };
23
- const getDependencyVirtualModules = (virtualIdsByDependency, server, file) => {
24
- return getDependencyVirtualIds(virtualIdsByDependency, file).flatMap((id) => {
25
- const module = server.moduleGraph.getModuleById(id);
26
- return module ? [module] : [];
27
- });
43
+ const sameStringSets = (left, right) => {
44
+ if (left.length !== right.length)
45
+ return false;
46
+ const rightSet = new Set(right);
47
+ if (rightSet.size !== left.length)
48
+ return false;
49
+ return left.every((item) => rightSet.has(item));
28
50
  };
29
51
  const createJsUpdates = (virtualIds, timestamp) => {
30
52
  return virtualIds.map((id) => {
@@ -44,14 +66,47 @@ export class AukletStyleHmr {
44
66
  lastUpdateTimes = new Map();
45
67
  suppressFullReloadUntil = 0;
46
68
  virtualIdsByDependency = new Map();
47
- constructor(graph) {
69
+ filesByVirtualId = new Map();
70
+ schedulePruneStaleVirtualDependencies;
71
+ pruneDelayMs;
72
+ constructor(graph, options = {}) {
48
73
  this.graph = graph;
74
+ this.pruneDelayMs = options.pruneDelayMs ?? 2 * 60 * 1000;
75
+ this.schedulePruneStaleVirtualDependencies = throttle(this.pruneDelayMs, (moduleGraph) => this.pruneStaleVirtualDependencies(moduleGraph));
76
+ }
77
+ trackVirtualStyleDependency(file, virtualId, kind = 'dependency') {
78
+ addVirtualStyleDependency(this.virtualIdsByDependency, this.filesByVirtualId, file, virtualId, kind);
79
+ }
80
+ replaceVirtualStyleDependency(virtualId, files, fileKinds = []) {
81
+ const normalizedFiles = new Set(files.map((file) => normalizeFileKey(file)));
82
+ const previousFiles = this.filesByVirtualId.get(virtualId) ?? new Map();
83
+ const nextKinds = new Map(fileKinds.map((item) => [normalizeFileKey(item.file), item.kind]));
84
+ for (const file of previousFiles.keys()) {
85
+ if (normalizedFiles.has(file))
86
+ continue;
87
+ removeVirtualStyleDependency(this.virtualIdsByDependency, this.filesByVirtualId, file, virtualId);
88
+ }
89
+ for (const file of normalizedFiles) {
90
+ addVirtualStyleDependency(this.virtualIdsByDependency, this.filesByVirtualId, file, virtualId, nextKinds.get(file) ?? 'dependency');
91
+ }
92
+ }
93
+ hasTrackedStyleDependency(file, moduleGraph) {
94
+ return this.getDependencyVirtualIds(file, moduleGraph).length > 0;
95
+ }
96
+ pruneStaleVirtualDependencies(moduleGraph) {
97
+ for (const [normalizedFile, virtualIds] of this.virtualIdsByDependency) {
98
+ for (const virtualId of Array.from(virtualIds)) {
99
+ if (moduleGraph.getModuleById(virtualId))
100
+ continue;
101
+ removeVirtualStyleDependency(this.virtualIdsByDependency, this.filesByVirtualId, normalizedFile, virtualId);
102
+ }
103
+ }
49
104
  }
50
- trackVirtualStyleDependency(file, virtualId) {
51
- addVirtualStyleDependency(this.virtualIdsByDependency, file, virtualId);
105
+ scheduleStaleVirtualDependencyPrune(moduleGraph) {
106
+ this.schedulePruneStaleVirtualDependencies(moduleGraph);
52
107
  }
53
- hasTrackedStyleDependency(file) {
54
- return (getDependencyVirtualIds(this.virtualIdsByDependency, file).length > 0);
108
+ cancelStaleVirtualDependencyPrune() {
109
+ this.schedulePruneStaleVirtualDependencies.cancel();
55
110
  }
56
111
  installFullReloadGuard(server) {
57
112
  const send = server.ws.send.bind(server.ws);
@@ -69,49 +124,199 @@ export class AukletStyleHmr {
69
124
  send(payload);
70
125
  });
71
126
  }
72
- handleStyleHotUpdate(context) {
127
+ async handleStyleHotUpdate(context) {
73
128
  const graph = this.graph();
74
- if (!graph.isSourceGraphFile(context.file) ||
75
- !graph.isStyleFile(context.file)) {
76
- return;
129
+ if (!graph.isStyleFile(context.file)) {
130
+ return undefined;
131
+ }
132
+ const isSourceGraphFile = graph.isSourceGraphFile(context.file);
133
+ const trackedEntries = this.getTrackedStyleFileEntries(context.file, context.server.moduleGraph);
134
+ if (!trackedEntries.length) {
135
+ if (isSourceGraphFile) {
136
+ graph.invalidateFile(context.file);
137
+ }
138
+ return undefined;
139
+ }
140
+ const entryEntries = isSourceGraphFile
141
+ ? trackedEntries.filter((item) => item.kind === 'entry')
142
+ : [];
143
+ const dependencyEntries = isSourceGraphFile
144
+ ? trackedEntries.filter((item) => item.kind === 'dependency')
145
+ : trackedEntries;
146
+ const previousResults = entryEntries.length
147
+ ? this.createTrackedVirtualStyleResults(graph, entryEntries)
148
+ : null;
149
+ if (isSourceGraphFile) {
150
+ graph.invalidateFile(context.file);
151
+ }
152
+ else {
153
+ this.invalidateTrackedVirtualPackages(graph, dependencyEntries);
77
154
  }
78
- this.suppressFullReload();
79
- graph.invalidateFile(context.file);
80
155
  if (this.isDuplicateUpdate(context.file)) {
81
156
  return [];
82
157
  }
83
- const updates = this.sendVirtualStyleUpdates(context.file, context.server, context.timestamp);
84
- logger.info(`package css hmr ${getRelativeFile(context.file)} tracked=${updates.tracked} updates=${updates.sent}`);
158
+ const updates = [
159
+ ...(dependencyEntries.length
160
+ ? this.createTrackedVirtualStyleUpdates(context.server, dependencyEntries, context.timestamp)
161
+ : []),
162
+ ...(entryEntries.length
163
+ ? await this.refreshTrackedVirtualStyleUpdates(context.server, entryEntries, previousResults, context.timestamp)
164
+ : []),
165
+ ];
166
+ if (!updates.length) {
167
+ return undefined;
168
+ }
169
+ this.suppressFullReload();
170
+ context.server.ws.send({
171
+ type: 'update',
172
+ updates,
173
+ });
174
+ logger.info(`package css hmr ${getRelativeFile(context.file)} tracked=${trackedEntries.length} updates=${updates.length}`);
85
175
  return [];
86
176
  }
87
- handleSourceModuleChange(server, file) {
177
+ async handleSourceModuleChange(server, file) {
88
178
  const graph = this.graph();
89
179
  if (!graph.isSourceGraphFile(file) || !graph.isSourceModuleFile(file)) {
90
180
  return false;
91
181
  }
182
+ const virtualIds = this.getDependencyVirtualIds(file, server.moduleGraph);
183
+ if (!virtualIds.length) {
184
+ graph.invalidateFile(file);
185
+ return false;
186
+ }
187
+ const parsedVirtualIds = this.parseTrackedVirtualIds(graph, virtualIds);
188
+ if (!parsedVirtualIds.length) {
189
+ graph.invalidateFile(file);
190
+ return false;
191
+ }
192
+ const previousResults = this.createTrackedVirtualStyleResults(graph, parsedVirtualIds);
92
193
  graph.invalidateFile(file);
93
- const updates = this.sendVirtualStyleUpdates(file, server, Date.now());
94
- logger.info(`package css source hmr ${getRelativeFile(file)} tracked=${updates.tracked} updates=${updates.sent}`);
194
+ const updates = await this.refreshTrackedVirtualStyleUpdates(server, parsedVirtualIds, previousResults, Date.now());
195
+ if (!updates.length) {
196
+ return false;
197
+ }
198
+ this.suppressFullReload();
199
+ server.ws.send({
200
+ type: 'update',
201
+ updates,
202
+ });
203
+ logger.info(`package css source hmr ${getRelativeFile(file)} tracked=${parsedVirtualIds.length} updates=${updates.length}`);
95
204
  return true;
96
205
  }
97
- sendVirtualStyleUpdates(file, server, timestamp) {
98
- const virtualIds = getDependencyVirtualIds(this.virtualIdsByDependency, file);
99
- const modules = getDependencyVirtualModules(this.virtualIdsByDependency, server, file);
100
- for (const module of modules) {
101
- server.moduleGraph.invalidateModule(module);
206
+ parseTrackedVirtualIds(graph, virtualIds) {
207
+ return virtualIds
208
+ .map((id) => this.parseTrackedVirtualId(graph, id))
209
+ .filter((item) => item !== null);
210
+ }
211
+ createTrackedVirtualStyleUpdates(server, parsedVirtualIds, timestamp) {
212
+ const changedVirtualIds = [];
213
+ for (const item of parsedVirtualIds) {
214
+ changedVirtualIds.push(item.id);
215
+ const module = server.moduleGraph.getModuleById(item.id);
216
+ if (module) {
217
+ server.moduleGraph.invalidateModule(module);
218
+ }
219
+ }
220
+ return createJsUpdates(changedVirtualIds, timestamp);
221
+ }
222
+ invalidateTrackedVirtualPackages(graph, parsedVirtualIds) {
223
+ const packageNames = new Set(parsedVirtualIds.map((item) => item.parsed.packageName));
224
+ for (const packageName of packageNames) {
225
+ graph.invalidatePackage(packageName);
226
+ }
227
+ }
228
+ getDependencyVirtualIds(file, moduleGraph) {
229
+ const normalizedFile = normalizeFileKey(file);
230
+ const virtualIds = Array.from(this.virtualIdsByDependency.get(normalizedFile) ?? []);
231
+ if (!moduleGraph) {
232
+ return virtualIds;
233
+ }
234
+ const liveVirtualIds = virtualIds.filter((virtualId) => moduleGraph.getModuleById(virtualId));
235
+ if (liveVirtualIds.length !== virtualIds.length) {
236
+ const liveSet = new Set(liveVirtualIds);
237
+ for (const virtualId of virtualIds) {
238
+ if (liveSet.has(virtualId))
239
+ continue;
240
+ removeVirtualStyleDependency(this.virtualIdsByDependency, this.filesByVirtualId, normalizedFile, virtualId);
241
+ }
242
+ }
243
+ return liveVirtualIds;
244
+ }
245
+ getTrackedStyleFileEntries(file, moduleGraph) {
246
+ const normalizedFile = normalizeFileKey(file);
247
+ const virtualIds = Array.from(this.virtualIdsByDependency.get(normalizedFile) ?? []);
248
+ const entries = virtualIds
249
+ .map((id) => this.parseTrackedVirtualIdWithKind(id, normalizedFile, moduleGraph))
250
+ .filter((item) => Boolean(item));
251
+ if (!moduleGraph) {
252
+ return entries;
253
+ }
254
+ if (entries.length !== virtualIds.length) {
255
+ const liveSet = new Set(entries.map((item) => item.id));
256
+ for (const virtualId of virtualIds) {
257
+ if (liveSet.has(virtualId))
258
+ continue;
259
+ removeVirtualStyleDependency(this.virtualIdsByDependency, this.filesByVirtualId, normalizedFile, virtualId);
260
+ }
261
+ }
262
+ return entries;
263
+ }
264
+ createTrackedVirtualStyleResults(graph, parsedVirtualIds) {
265
+ const previousResults = new Map();
266
+ for (const item of parsedVirtualIds) {
267
+ previousResults.set(item.id, graph.peekPackageStyleCode(item.parsed));
102
268
  }
103
- const updates = createJsUpdates(virtualIds, timestamp);
104
- if (updates.length) {
105
- server.ws.send({
106
- type: 'update',
107
- updates,
108
- });
269
+ return previousResults;
270
+ }
271
+ parseTrackedVirtualId(graph, id) {
272
+ const parsedId = id.startsWith(RESOLVED_VIRTUAL_ID_PREFIX)
273
+ ? id.slice(RESOLVED_VIRTUAL_ID_PREFIX.length)
274
+ : id;
275
+ const parsed = graph.parsePackageStyleId(parsedId);
276
+ if (!parsed || !graph.getPackageNames().includes(parsed.packageName)) {
277
+ return null;
278
+ }
279
+ return {
280
+ id,
281
+ parsed,
282
+ kind: 'dependency',
283
+ };
284
+ }
285
+ parseTrackedVirtualIdWithKind(id, normalizedFile, moduleGraph) {
286
+ if (moduleGraph && !moduleGraph.getModuleById(id)) {
287
+ return null;
109
288
  }
289
+ const parsed = this.parseTrackedVirtualId(this.graph(), id);
290
+ if (!parsed)
291
+ return null;
292
+ const kind = this.filesByVirtualId.get(id)?.get(normalizedFile) ?? 'dependency';
110
293
  return {
111
- sent: updates.length,
112
- tracked: virtualIds.length,
294
+ ...parsed,
295
+ kind,
113
296
  };
114
297
  }
298
+ async refreshTrackedVirtualStyleUpdates(server, parsedVirtualIds, previousResults, timestamp) {
299
+ const graph = this.graph();
300
+ const changedVirtualIds = [];
301
+ for (const item of parsedVirtualIds) {
302
+ const nextResult = await graph.createPackageStyleCode(item.parsed);
303
+ const previousResult = previousResults.get(item.id);
304
+ if (!previousResult ||
305
+ !this.sameStyleLoadResult(previousResult, nextResult)) {
306
+ changedVirtualIds.push(item.id);
307
+ const module = server.moduleGraph.getModuleById(item.id);
308
+ if (module) {
309
+ server.moduleGraph.invalidateModule(module);
310
+ }
311
+ }
312
+ }
313
+ return createJsUpdates(changedVirtualIds, timestamp);
314
+ }
315
+ sameStyleLoadResult(left, right) {
316
+ return (left.code === right.code &&
317
+ sameStringSets(left.watchFiles, right.watchFiles) &&
318
+ sameStringSets(left.dependencyPackages ?? [], right.dependencyPackages ?? []));
319
+ }
115
320
  suppressFullReload() {
116
321
  this.suppressFullReloadUntil = Date.now() + FULL_RELOAD_SUPPRESS_MS;
117
322
  }
@@ -16,6 +16,7 @@ export declare class ModuleStyleGraph {
16
16
  getPackageNames(): string[];
17
17
  getWatchRoots(): Promise<string[]>;
18
18
  createPackageStyleCode(parsed: PackageStyleId): Promise<import("#auklet/css/vite/moduleGraph/types").PackageStyleLoadResult>;
19
+ peekPackageStyleCode(parsed: PackageStyleId): import("#auklet/css/vite/moduleGraph/types").PackageStyleLoadResult | null;
19
20
  invalidatePackage(packageName: string): void;
20
21
  invalidateFile(file: string): string | null;
21
22
  isSourceModuleFile(file: string): boolean;
@@ -60,6 +60,9 @@ export class ModuleStyleGraph {
60
60
  createPackageStyleCode(parsed) {
61
61
  return this.styleCodeFactory.createPackageStyleCode(parsed, this.requestCache);
62
62
  }
63
+ peekPackageStyleCode(parsed) {
64
+ return this.requestCache.peekLoadResult(parsed);
65
+ }
63
66
  invalidatePackage(packageName) {
64
67
  this.requestCache.invalidatePackage(packageName);
65
68
  }
@@ -4,4 +4,8 @@ export declare function mergeLoadResults(...results: Array<PackageStyleLoadResul
4
4
  watchFiles: string[];
5
5
  cacheInputFiles: string[];
6
6
  dependencyPackages: string[];
7
+ watchFileKinds: {
8
+ file: string;
9
+ kind: "dependency" | "entry";
10
+ }[];
7
11
  };
@@ -1,5 +1,15 @@
1
1
  // 合并递归 style graph 加载结果,同时保留 CSS 顺序并去重 watch files。
2
2
  export function mergeLoadResults(...results) {
3
+ const watchFileKinds = new Map();
4
+ for (const result of results) {
5
+ for (const item of result.watchFileKinds ?? []) {
6
+ const currentKind = watchFileKinds.get(item.file);
7
+ if (currentKind === 'entry' || item.kind === currentKind) {
8
+ continue;
9
+ }
10
+ watchFileKinds.set(item.file, item.kind === 'entry' ? 'entry' : (currentKind ?? item.kind));
11
+ }
12
+ }
3
13
  return {
4
14
  code: results
5
15
  .map((result) => result.code)
@@ -8,5 +18,9 @@ export function mergeLoadResults(...results) {
8
18
  watchFiles: Array.from(new Set(results.flatMap((result) => result.watchFiles))),
9
19
  cacheInputFiles: Array.from(new Set(results.flatMap((result) => result.cacheInputFiles ?? []))),
10
20
  dependencyPackages: Array.from(new Set(results.flatMap((result) => result.dependencyPackages ?? []))),
21
+ watchFileKinds: Array.from(watchFileKinds, ([file, kind]) => ({
22
+ file,
23
+ kind,
24
+ })),
11
25
  };
12
26
  }
@@ -1,10 +1,12 @@
1
1
  import type { PackageStyleLoadResult } from '#auklet/css/vite/moduleGraph/types';
2
2
  export type PersistentStyleGraphCacheOptions = {
3
3
  root: string;
4
+ maxCacheFiles?: number;
4
5
  };
5
6
  export declare class PersistentStyleGraphCache {
6
- private static readonly cleanedRoots;
7
+ private static readonly lastCleanupTimes;
7
8
  private readonly cacheRoot;
9
+ private readonly maxCacheFiles;
8
10
  constructor(options: PersistentStyleGraphCacheOptions);
9
11
  createKey(value: unknown): string;
10
12
  read(key: string): PackageStyleLoadResult | null;
@@ -4,6 +4,7 @@ import crypto from 'node:crypto';
4
4
  import { toPosixPath } from '#auklet/utils';
5
5
  const cacheVersion = 'v1';
6
6
  const maxCacheFiles = 5000;
7
+ const cleanupIntervalMs = 5 * 60 * 1000;
7
8
  const staleCacheAgeMs = 7 * 24 * 60 * 60 * 1000;
8
9
  const stableStringify = (value) => {
9
10
  if (Array.isArray(value)) {
@@ -68,10 +69,12 @@ const statCachedFile = (file) => {
68
69
  };
69
70
  };
70
71
  export class PersistentStyleGraphCache {
71
- static cleanedRoots = new Set();
72
+ static lastCleanupTimes = new Map();
72
73
  cacheRoot;
74
+ maxCacheFiles;
73
75
  constructor(options) {
74
76
  this.cacheRoot = path.join(options.root, 'node_modules', '.auklet', 'cache', 'vite-style', cacheVersion);
77
+ this.maxCacheFiles = options.maxCacheFiles ?? maxCacheFiles;
75
78
  }
76
79
  createKey(value) {
77
80
  return hashValue({
@@ -96,10 +99,10 @@ export class PersistentStyleGraphCache {
96
99
  }
97
100
  }
98
101
  write(key, result, inputFiles) {
99
- const files = Array.from(new Map(inputFiles
100
- .map((file) => statCachedFile(file))
101
- .map((file) => [file.file, file])).values());
102
102
  try {
103
+ const files = Array.from(new Map(inputFiles
104
+ .map((file) => statCachedFile(file))
105
+ .map((file) => [file.file, file])).values());
103
106
  fs.mkdirSync(this.cacheRoot, { recursive: true });
104
107
  fs.writeFileSync(this.getCacheFile(key), `${JSON.stringify({
105
108
  files,
@@ -114,11 +117,12 @@ export class PersistentStyleGraphCache {
114
117
  this.cleanupStaleEntries();
115
118
  }
116
119
  cleanupStaleEntries() {
117
- if (PersistentStyleGraphCache.cleanedRoots.has(this.cacheRoot))
120
+ const now = Date.now();
121
+ const lastCleanupTime = PersistentStyleGraphCache.lastCleanupTimes.get(this.cacheRoot) ?? 0;
122
+ if (now - lastCleanupTime < cleanupIntervalMs)
118
123
  return;
119
- PersistentStyleGraphCache.cleanedRoots.add(this.cacheRoot);
124
+ PersistentStyleGraphCache.lastCleanupTimes.set(this.cacheRoot, now);
120
125
  try {
121
- const now = Date.now();
122
126
  const entries = fs
123
127
  .readdirSync(this.cacheRoot, { withFileTypes: true })
124
128
  .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
@@ -137,7 +141,7 @@ export class PersistentStyleGraphCache {
137
141
  }
138
142
  freshEntries.push(entry);
139
143
  }
140
- const overflowCount = freshEntries.length - maxCacheFiles;
144
+ const overflowCount = freshEntries.length - this.maxCacheFiles;
141
145
  if (overflowCount <= 0)
142
146
  return;
143
147
  for (const entry of freshEntries
@@ -4,6 +4,10 @@ import type { StyleProcessor } from '#auklet/css/core/styleProcessor';
4
4
  import type { WorkspaceStyleResolver } from '#auklet/css/core/workspaceStyleResolver';
5
5
  import type { StylePackageSource } from '#auklet/css/vite/moduleGraph/packageSource/types';
6
6
  import type { LoadAukletConfig, PackageStyleId, PackageStyleLoadResult } from '#auklet/css/vite/moduleGraph/types';
7
+ type LoadResultEntry = {
8
+ commit?: () => PackageStyleLoadResult;
9
+ result: PackageStyleLoadResult;
10
+ };
7
11
  export type PackageStyleContext = {
8
12
  normalizedConfig: NormalizedAukletConfig;
9
13
  context: ResolvedModuleStyleBuildContext;
@@ -25,9 +29,12 @@ export declare class ModuleStyleGraphRequestCache {
25
29
  private readonly options;
26
30
  private readonly contexts;
27
31
  private readonly loadResults;
32
+ private readonly settledLoadResults;
28
33
  private readonly loadResultDependencies;
34
+ private readonly loadResultVersions;
29
35
  private readonly loadAukletConfig;
30
36
  private readonly persistentCache;
37
+ private graphVersion;
31
38
  constructor(options: ModuleStyleGraphRequestCacheOptions);
32
39
  getPackageNames(): string[];
33
40
  isKnownPackageName(packageName: string): boolean;
@@ -68,13 +75,14 @@ export declare class ModuleStyleGraphRequestCache {
68
75
  sourceRoot: string;
69
76
  styleProcessor: StyleProcessor;
70
77
  } | null>;
71
- getLoadResult(parsed: PackageStyleId, create: () => Promise<PackageStyleLoadResult>): Promise<PackageStyleLoadResult>;
78
+ getLoadResult(parsed: PackageStyleId, create: () => Promise<LoadResultEntry>): Promise<PackageStyleLoadResult>;
72
79
  invalidatePackage(packageName: string): void;
73
80
  readPersistentLoadResult(parsed: PackageStyleId, context: PackageStyleContext): PackageStyleLoadResult | null;
74
81
  writePersistentLoadResult(parsed: PackageStyleId, context: PackageStyleContext, result: PackageStyleLoadResult): {
75
82
  cacheInputFiles: string[];
76
83
  code: string;
77
84
  watchFiles: Array<string>;
85
+ watchFileKinds?: Array<import("#auklet/css/vite/moduleGraph/types").PackageStyleWatchFile>;
78
86
  dependencyPackages?: Array<string>;
79
87
  };
80
88
  private createContext;
@@ -87,4 +95,10 @@ export declare class ModuleStyleGraphRequestCache {
87
95
  private getLoadResultKey;
88
96
  private getLoadResultPackageName;
89
97
  private invalidateLoadResults;
98
+ private discardLoadResult;
99
+ peekLoadResult(parsed: PackageStyleId): PackageStyleLoadResult | null;
100
+ private getGraphVersion;
101
+ private bumpGraphVersion;
102
+ private bumpLoadResultVersion;
90
103
  }
104
+ export {};
@@ -8,9 +8,12 @@ export class ModuleStyleGraphRequestCache {
8
8
  options;
9
9
  contexts = new Map();
10
10
  loadResults = new Map();
11
+ settledLoadResults = new Map();
11
12
  loadResultDependencies = new Map();
13
+ loadResultVersions = new Map();
12
14
  loadAukletConfig;
13
15
  persistentCache;
16
+ graphVersion = 0;
14
17
  constructor(options) {
15
18
  this.options = options;
16
19
  this.loadAukletConfig = options.loadAukletConfig ?? loadAukletConfig;
@@ -37,14 +40,53 @@ export class ModuleStyleGraphRequestCache {
37
40
  const cachedResult = this.loadResults.get(key);
38
41
  if (cachedResult)
39
42
  return cachedResult;
40
- const result = create().then((value) => {
41
- this.loadResultDependencies.set(key, new Set(value.dependencyPackages ?? []));
42
- return value;
43
+ const version = this.bumpLoadResultVersion(key);
44
+ const graphVersion = this.getGraphVersion();
45
+ let result;
46
+ result = create()
47
+ .then(async (entry) => {
48
+ if (this.getGraphVersion() !== graphVersion) {
49
+ const currentResult = this.loadResults.get(key);
50
+ if (currentResult && currentResult !== result) {
51
+ return currentResult;
52
+ }
53
+ this.discardLoadResult(key);
54
+ return this.getLoadResult(parsed, create);
55
+ }
56
+ if (this.loadResultVersions.get(key) === version) {
57
+ this.loadResultDependencies.set(key, new Set(entry.result.dependencyPackages ?? []));
58
+ }
59
+ try {
60
+ const committedResult = entry.commit?.();
61
+ if (committedResult) {
62
+ entry.result = committedResult;
63
+ }
64
+ }
65
+ catch {
66
+ // Persistent cache writes are best-effort and must not fail dev CSS.
67
+ }
68
+ this.settledLoadResults.set(key, entry.result);
69
+ return entry.result;
70
+ })
71
+ .catch((error) => {
72
+ if (this.getGraphVersion() !== graphVersion) {
73
+ const currentResult = this.loadResults.get(key);
74
+ if (currentResult && currentResult !== result) {
75
+ return currentResult;
76
+ }
77
+ this.discardLoadResult(key);
78
+ return this.getLoadResult(parsed, create);
79
+ }
80
+ if (this.loadResultVersions.get(key) === version) {
81
+ this.discardLoadResult(key);
82
+ }
83
+ throw error;
43
84
  });
44
85
  this.loadResults.set(key, result);
45
86
  return result;
46
87
  }
47
88
  invalidatePackage(packageName) {
89
+ this.bumpGraphVersion();
48
90
  this.contexts.delete(packageName);
49
91
  this.invalidateLoadResults(packageName);
50
92
  }
@@ -208,8 +250,29 @@ export class ModuleStyleGraphRequestCache {
208
250
  continue;
209
251
  }
210
252
  }
211
- this.loadResults.delete(key);
212
- this.loadResultDependencies.delete(key);
253
+ this.discardLoadResult(key);
213
254
  }
214
255
  }
256
+ discardLoadResult(key) {
257
+ this.bumpGraphVersion();
258
+ this.bumpLoadResultVersion(key);
259
+ this.loadResults.delete(key);
260
+ this.loadResultDependencies.delete(key);
261
+ this.settledLoadResults.delete(key);
262
+ }
263
+ peekLoadResult(parsed) {
264
+ return this.settledLoadResults.get(this.getLoadResultKey(parsed)) ?? null;
265
+ }
266
+ getGraphVersion() {
267
+ return this.graphVersion;
268
+ }
269
+ bumpGraphVersion() {
270
+ this.graphVersion += 1;
271
+ return this.graphVersion;
272
+ }
273
+ bumpLoadResultVersion(key) {
274
+ const nextVersion = (this.loadResultVersions.get(key) ?? 0) + 1;
275
+ this.loadResultVersions.set(key, nextVersion);
276
+ return nextVersion;
277
+ }
215
278
  }
@@ -14,6 +14,7 @@ export declare class StyleCodeFactory {
14
14
  private createModuleStyleCode;
15
15
  private createSourceModuleStyleCode;
16
16
  private createOwnSourceStyleCode;
17
+ private collectSourceStyleWatchFiles;
17
18
  private toDevModuleImportSpecifier;
18
19
  private toDevExternalStyleSpecifier;
19
20
  private parsePackageStyleIdInRequest;
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import postcss from 'postcss';
3
4
  import { mergeLoadResults } from '#auklet/css/vite/moduleGraph/loadResult';
@@ -14,19 +15,30 @@ export class StyleCodeFactory {
14
15
  this.config = config;
15
16
  }
16
17
  async createPackageStyleCode(parsed, cache) {
17
- return cache.getLoadResult(parsed, async () => this.normalizeTopLevelImports(await this.createUncachedPackageStyleCode(parsed, cache)));
18
- }
19
- async createUncachedPackageStyleCode(parsed, cache) {
20
- const context = await cache.getContext(parsed);
21
- if (!context) {
18
+ return cache.getLoadResult(parsed, async () => {
19
+ const context = await cache.getContext(parsed);
20
+ if (!context) {
21
+ return {
22
+ result: {
23
+ code: '',
24
+ watchFiles: [],
25
+ },
26
+ };
27
+ }
28
+ const cachedResult = cache.readPersistentLoadResult(parsed, context);
29
+ if (cachedResult) {
30
+ return {
31
+ result: cachedResult,
32
+ };
33
+ }
34
+ const result = this.normalizeTopLevelImports(await this.createUncachedPackageStyleCode(parsed, cache, context));
22
35
  return {
23
- code: '',
24
- watchFiles: [],
36
+ result,
37
+ commit: () => cache.writePersistentLoadResult(parsed, context, result),
25
38
  };
26
- }
27
- const cachedResult = cache.readPersistentLoadResult(parsed, context);
28
- if (cachedResult)
29
- return cachedResult;
39
+ });
40
+ }
41
+ async createUncachedPackageStyleCode(parsed, cache, context) {
30
42
  let result;
31
43
  if (parsed.stylePath === STYLE_ENTRY) {
32
44
  result = await this.createStyleCode(context, cache);
@@ -45,7 +57,7 @@ export class StyleCodeFactory {
45
57
  else {
46
58
  result = await this.createSourceModuleStyleCode(context, cache, parsed.stylePath);
47
59
  }
48
- return cache.writePersistentLoadResult(parsed, context, this.normalizeTopLevelImports(result));
60
+ return result;
49
61
  }
50
62
  async createStyleCode(context, cache) {
51
63
  const results = [];
@@ -155,7 +167,7 @@ export class StyleCodeFactory {
155
167
  async createSourceModuleStyleCode(context, cache, stylePath) {
156
168
  context.packageContext.assertPreservedLocalStyleImports();
157
169
  const sourceModuleDir = removeStyleExtension(stylePath);
158
- const { styleFiles, sourceFiles } = context.packageContext;
170
+ const { sourceFiles } = context.packageContext;
159
171
  const entry = createModuleStyleEntryPlan(context.packageContext, sourceModuleDir);
160
172
  const sourceStyleDir = path.join(context.sourceRoot, sourceModuleDir, this.config.output.styleDir);
161
173
  const moduleStyleResults = [];
@@ -178,16 +190,31 @@ export class StyleCodeFactory {
178
190
  }
179
191
  }
180
192
  const ownStyleCode = this.createOwnSourceStyleCode(context, entry.ownStyleFiles);
193
+ const sourceWatchFiles = this.collectSourceStyleWatchFiles(context, entry.ownStyleFiles);
181
194
  return mergeLoadResults(...moduleStyleResults, {
182
195
  code: [createImportCode(moduleStyleSpecifiers), ownStyleCode]
183
196
  .filter((code) => code.trim())
184
197
  .join('\n'),
185
198
  watchFiles: [
186
199
  ...context.configPaths,
187
- ...styleFiles,
200
+ ...sourceWatchFiles.map((item) => item.file),
188
201
  ...moduleStyleWatchFiles,
189
202
  ...sourceFiles.filter((file) => /\.(ts|tsx)$/.test(file)),
190
203
  ],
204
+ watchFileKinds: [
205
+ ...context.configPaths.map((file) => ({
206
+ file,
207
+ kind: 'dependency',
208
+ })),
209
+ ...sourceWatchFiles,
210
+ ...moduleStyleWatchFiles.map((file) => ({
211
+ file,
212
+ kind: 'dependency',
213
+ })),
214
+ ...sourceFiles
215
+ .filter((file) => /\.(ts|tsx)$/.test(file))
216
+ .map((file) => ({ file, kind: 'dependency' })),
217
+ ],
191
218
  cacheInputFiles: moduleStyleCacheInputFiles,
192
219
  });
193
220
  }
@@ -209,6 +236,42 @@ export class StyleCodeFactory {
209
236
  }
210
237
  return root.nodes?.length ? context.styleProcessor.stringify(root) : '';
211
238
  }
239
+ collectSourceStyleWatchFiles(context, styleFiles) {
240
+ const watchFiles = new Map();
241
+ const pending = styleFiles.map((file) => ({
242
+ file,
243
+ kind: 'entry',
244
+ }));
245
+ const visited = new Set();
246
+ while (pending.length) {
247
+ const current = pending.pop();
248
+ if (!current)
249
+ continue;
250
+ const styleFile = current.file;
251
+ const normalized = path.resolve(styleFile);
252
+ if (visited.has(normalized))
253
+ continue;
254
+ visited.add(normalized);
255
+ const previousKind = watchFiles.get(normalized);
256
+ watchFiles.set(normalized, previousKind === 'entry' || current.kind === 'entry'
257
+ ? 'entry'
258
+ : 'dependency');
259
+ for (const reference of context.styleProcessor.collectStyleImportReferences([normalized])) {
260
+ if (!context.resolver.isStyleFile(reference.imported) ||
261
+ !fs.existsSync(reference.imported)) {
262
+ continue;
263
+ }
264
+ pending.push({
265
+ file: reference.imported,
266
+ kind: 'dependency',
267
+ });
268
+ }
269
+ }
270
+ return Array.from(watchFiles, ([file, kind]) => ({
271
+ file,
272
+ kind,
273
+ }));
274
+ }
212
275
  toDevModuleImportSpecifier(specifier, context, cache, sourceStyleDir) {
213
276
  return createDevModuleStyleSpecifier(specifier, {
214
277
  sourceStyleDir,
@@ -9,10 +9,16 @@ export type PackageStyleId = {
9
9
  packageName: string;
10
10
  stylePath: string;
11
11
  };
12
+ export type PackageStyleWatchFileKind = 'entry' | 'dependency';
13
+ export type PackageStyleWatchFile = {
14
+ file: string;
15
+ kind: PackageStyleWatchFileKind;
16
+ };
12
17
  export type PackageStyleLoadResult = {
13
18
  code: string;
14
19
  cacheInputFiles?: Array<string>;
15
20
  watchFiles: Array<string>;
21
+ watchFileKinds?: Array<PackageStyleWatchFile>;
16
22
  dependencyPackages?: Array<string>;
17
23
  };
18
24
  export type LoadAukletConfig = (packageRoot: string, options?: {
@@ -15,6 +15,6 @@ export declare function aukletStylePlugin(options?: AukletStylePluginOptions): {
15
15
  configureServer(server: ViteDevServer): Promise<void>;
16
16
  hotUpdate: {
17
17
  order: "pre";
18
- handler(context: HotUpdateOptions): never[] | undefined;
18
+ handler(context: HotUpdateOptions): Promise<never[] | undefined>;
19
19
  };
20
20
  };
@@ -2,10 +2,12 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { AukletStyleHmr } from '#auklet/css/vite/hmr';
4
4
  import { ModuleStyleGraph } from '#auklet/css/vite/moduleGraph/graph';
5
+ import { createAukletLogger } from '#auklet/logger';
5
6
  const WORKSPACE_FILE = 'pnpm-workspace.yaml';
6
7
  const VIRTUAL_ID_PREFIX = 'virtual:auklet-css:';
7
8
  const RESOLVED_VIRTUAL_ID_PREFIX = '\0auklet-css:';
8
9
  const BROWSER_VIRTUAL_ID_PREFIX = 'auklet-css:';
10
+ const logger = createAukletLogger({ scope: 'css:vite' });
9
11
  const stripQuery = (id) => id.split('?')[0];
10
12
  const toResolvedVirtualId = (id) => {
11
13
  if (id.startsWith(RESOLVED_VIRTUAL_ID_PREFIX)) {
@@ -42,6 +44,20 @@ const resolveGraphRoot = (mode, viteRoot) => {
42
44
  return findWorkspaceRoot(viteRoot) ?? process.cwd();
43
45
  return viteRoot;
44
46
  };
47
+ const createWatcherErrorPayload = (error, file) => {
48
+ const err = error instanceof Error
49
+ ? error
50
+ : new Error(typeof error === 'string' ? error : String(error));
51
+ return {
52
+ type: 'error',
53
+ err: {
54
+ message: err.message,
55
+ stack: err.stack ?? err.message,
56
+ plugin: 'auklet-css',
57
+ id: file,
58
+ },
59
+ };
60
+ };
45
61
  const invalidateVirtualModules = (server, graph) => {
46
62
  const modules = [];
47
63
  for (const packageName of graph.getPackageNames()) {
@@ -57,6 +73,7 @@ const invalidateVirtualModules = (server, graph) => {
57
73
  };
58
74
  export function aukletStylePlugin(options = {}) {
59
75
  let graph = null;
76
+ let moduleGraph = null;
60
77
  const getGraph = () => {
61
78
  if (!graph) {
62
79
  graph = createModuleStyleGraph(options, process.cwd());
@@ -93,15 +110,24 @@ export function aukletStylePlugin(options = {}) {
93
110
  if (!parsed)
94
111
  return null;
95
112
  const result = await graph.createPackageStyleCode(parsed);
113
+ if (moduleGraph) {
114
+ hmr.pruneStaleVirtualDependencies(moduleGraph);
115
+ }
116
+ hmr.replaceVirtualStyleDependency(id, result.watchFiles, result.watchFileKinds);
96
117
  for (const file of result.watchFiles) {
97
- hmr.trackVirtualStyleDependency(file, id);
98
118
  this.addWatchFile?.(file);
99
119
  }
100
120
  return result.code;
101
121
  },
102
122
  async configureServer(server) {
103
123
  const graph = getGraph();
124
+ moduleGraph = server.moduleGraph;
104
125
  hmr.installFullReloadGuard(server);
126
+ const close = server.close.bind(server);
127
+ server.close = (async () => {
128
+ hmr.cancelStaleVirtualDependencyPrune();
129
+ return close();
130
+ });
105
131
  server.watcher.add(await graph.getWatchRoots());
106
132
  const invalidateStyleGraph = (file) => {
107
133
  if (!graph.isSourceGraphFile(file))
@@ -117,23 +143,35 @@ export function aukletStylePlugin(options = {}) {
117
143
  };
118
144
  server.watcher.on('add', reloadStyleGraph);
119
145
  server.watcher.on('unlink', reloadStyleGraph);
120
- server.watcher.on('change', (file) => {
121
- if (graph.isStyleConfigFile(file)) {
122
- reloadStyleGraph(file);
123
- }
124
- else if (graph.isStyleFile(file) &&
125
- hmr.hasTrackedStyleDependency(file)) {
126
- hmr.handleStyleHotUpdate({
127
- file,
128
- modules: [],
129
- server,
130
- timestamp: Date.now(),
131
- type: 'update',
132
- read: async () => '',
133
- });
146
+ server.watcher.on('change', async (file) => {
147
+ try {
148
+ hmr.scheduleStaleVirtualDependencyPrune(server.moduleGraph);
149
+ if (graph.isStyleConfigFile(file)) {
150
+ reloadStyleGraph(file);
151
+ }
152
+ else if (graph.isStyleFile(file)) {
153
+ if (hmr.hasTrackedStyleDependency(file)) {
154
+ await hmr.handleStyleHotUpdate({
155
+ file,
156
+ modules: [],
157
+ server,
158
+ timestamp: Date.now(),
159
+ type: 'update',
160
+ read: async () => '',
161
+ });
162
+ }
163
+ else {
164
+ graph.invalidateFile(file);
165
+ }
166
+ }
167
+ else if (graph.isSourceModuleFile(file)) {
168
+ await hmr.handleSourceModuleChange(server, file);
169
+ }
134
170
  }
135
- else if (graph.isSourceModuleFile(file)) {
136
- hmr.handleSourceModuleChange(server, file);
171
+ catch (error) {
172
+ logger.error('package css change watcher failed');
173
+ logger.error(error);
174
+ server.ws.send(createWatcherErrorPayload(error, file));
137
175
  }
138
176
  });
139
177
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auklet",
3
- "version": "0.2.10",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "author": "chentao.arthur",
6
6
  "description": "Build utilities for TypeScript packages and module CSS output.",