@ttsc/unplugin 0.30.2 → 0.30.4
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/lib/bun.js +3 -16
- package/lib/bun.js.map +1 -1
- package/lib/bun.mjs +3 -16
- package/lib/bun.mjs.map +1 -1
- package/lib/core/esbuild.d.cts +4 -0
- package/lib/core/esbuild.d.mts +4 -0
- package/lib/core/esbuild.d.ts +4 -0
- package/lib/core/esbuild.js +110 -0
- package/lib/core/esbuild.js.map +1 -0
- package/lib/core/esbuild.mjs +108 -0
- package/lib/core/esbuild.mjs.map +1 -0
- package/lib/core/index.js +60 -89
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +61 -90
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.cts +18 -8
- package/lib/core/transform.d.mts +18 -8
- package/lib/core/transform.d.ts +18 -8
- package/lib/core/transform.js +472 -125
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +472 -125
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/viteServe.d.cts +35 -38
- package/lib/core/viteServe.d.mts +35 -38
- package/lib/core/viteServe.d.ts +35 -38
- package/lib/core/viteServe.js +731 -94
- package/lib/core/viteServe.js.map +1 -1
- package/lib/core/viteServe.mjs +732 -94
- package/lib/core/viteServe.mjs.map +1 -1
- package/package.json +3 -3
- package/src/bun.ts +2 -17
- package/src/core/esbuild.ts +127 -0
- package/src/core/index.ts +62 -100
- package/src/core/transform.ts +620 -170
- package/src/core/viteServe.ts +838 -157
package/src/core/viteServe.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import { createFilesystemPathIdentityContext } from "ttsc/path-identity";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* cannot observe a path whose parent directories do not exist yet. One `stat`
|
|
15
|
-
* every half second per unavailable path is negligible against a dev server's
|
|
16
|
-
* baseline. Rich inputs replay only the predicates the compiler recorded.
|
|
17
|
-
*/
|
|
18
|
-
const MISSING_INPUT_POLL_INTERVAL = 500;
|
|
5
|
+
import {
|
|
6
|
+
type TtscWatchInput,
|
|
7
|
+
type TtscWatchInputBaseline,
|
|
8
|
+
type TtscWatchInputEvidence,
|
|
9
|
+
captureWatchInputBaseline,
|
|
10
|
+
pathIdentityKey,
|
|
11
|
+
validateGraphInputObservation,
|
|
12
|
+
watchInputEvidenceMatchesBaseline,
|
|
13
|
+
} from "./transform";
|
|
19
14
|
|
|
20
15
|
/** One module node inside a Vite module graph; opaque to this module. */
|
|
21
16
|
type ViteModuleNodeLike = object;
|
|
@@ -49,194 +44,880 @@ interface ViteEnvironmentLike {
|
|
|
49
44
|
* carries.
|
|
50
45
|
*/
|
|
51
46
|
export interface ViteDevServerLike {
|
|
47
|
+
config?: { root?: string };
|
|
52
48
|
environments?: Record<string, ViteEnvironmentLike>;
|
|
53
49
|
hot?: ViteHotChannelLike;
|
|
54
50
|
moduleGraph?: ViteModuleGraphLike;
|
|
55
51
|
ws?: ViteHotChannelLike;
|
|
56
52
|
}
|
|
57
53
|
|
|
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
|
-
|
|
54
|
+
interface InputCondition {
|
|
55
|
+
baseline?: TtscWatchInputBaseline;
|
|
56
|
+
evidence?: TtscWatchInputEvidence;
|
|
57
|
+
importers: Set<string>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface InputEntry {
|
|
61
|
+
aliases: Set<string>;
|
|
62
|
+
/** Latest native event already associated with this registered spelling. */
|
|
63
|
+
changedAt: number;
|
|
64
|
+
conditions: Map<string, InputCondition>;
|
|
65
|
+
fallback: boolean;
|
|
66
|
+
file: string;
|
|
67
|
+
links: Set<string>;
|
|
68
|
+
renameAliases: Set<string>;
|
|
69
|
+
scopes: Set<WatchScope>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface LinkedPath {
|
|
73
|
+
target: string | undefined;
|
|
74
|
+
inputs: Set<InputEntry>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface WatchScope {
|
|
78
|
+
entries: Set<InputEntry>;
|
|
79
|
+
failed: boolean;
|
|
80
|
+
root: string;
|
|
81
|
+
pinned: boolean;
|
|
82
|
+
startedAt: number;
|
|
83
|
+
watcher?: { close(): void };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ViteServeWatchOperations {
|
|
87
|
+
/** Override case-policy discovery for a simulated host filesystem. */
|
|
88
|
+
caseSensitive?(directory: string): boolean;
|
|
89
|
+
/** Override path semantics when testing a non-host platform. */
|
|
90
|
+
platform?: NodeJS.Platform;
|
|
91
|
+
poll(listener: () => void): { close(): void };
|
|
90
92
|
watch(
|
|
91
|
-
|
|
93
|
+
root: string,
|
|
94
|
+
listener: (eventType: string, file: string | null) => void,
|
|
95
|
+
onError: () => void,
|
|
96
|
+
): { close(): void };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Serve-time compiler dependencies never enter Vite's runtime import graph. */
|
|
100
|
+
export interface ViteServeInputWatch {
|
|
101
|
+
attach(server: ViteDevServerLike): void;
|
|
102
|
+
begin(): number;
|
|
103
|
+
dispose(): Promise<void>;
|
|
104
|
+
/** Release every compiler input owned by one removed source module. */
|
|
105
|
+
forget(importer: string): void;
|
|
106
|
+
replace(
|
|
92
107
|
importer: string,
|
|
93
|
-
|
|
108
|
+
inputs: readonly TtscWatchInput[],
|
|
109
|
+
failed?: boolean,
|
|
110
|
+
startedAt?: number,
|
|
94
111
|
): void;
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
/**
|
|
115
|
+
* A bounded recursive filesystem observer shared by all served modules.
|
|
116
|
+
*
|
|
117
|
+
* Vite resolves transform-context addWatchFile as a runtime import, including
|
|
118
|
+
* type-only .server files and non-module plugin assets. Use a separate watcher
|
|
119
|
+
* for compiler inputs, including node_modules, which Vite's watcher ignores.
|
|
120
|
+
* Ordinary files use events after their initial subscription is observed.
|
|
121
|
+
* Missing spellings and directory predicates use the recursive observer for
|
|
122
|
+
* their nearest available scope. Inputs a native scope cannot safely cover
|
|
123
|
+
* share one bounded fallback poll; linked files also share topology checks
|
|
124
|
+
* because retargeting a junction need not emit events on its old descendants.
|
|
125
|
+
*/
|
|
126
|
+
export function createViteServeInputWatch(
|
|
127
|
+
operations: Partial<ViteServeWatchOperations> = {},
|
|
128
|
+
): ViteServeInputWatch {
|
|
129
|
+
const entries = new Map<string, InputEntry>();
|
|
130
|
+
const aliases = new Map<string, Set<InputEntry>>();
|
|
131
|
+
const renameAliases = new Map<string, Set<InputEntry>>();
|
|
132
|
+
const importerInputs = new Map<string, Map<string, string>>();
|
|
133
|
+
const pending = new Set<InputEntry>();
|
|
134
|
+
const polled = new Set<InputEntry>();
|
|
135
|
+
const links = new Map<string, LinkedPath>();
|
|
136
|
+
const scopes = new Map<string, WatchScope>();
|
|
137
|
+
const componentLinks = new Map<string, string | null>();
|
|
138
|
+
const missingComponents = new Set<string>();
|
|
139
|
+
const changes = new Map<string, number>();
|
|
140
|
+
let server: ViteDevServerLike | undefined;
|
|
141
|
+
let projectRoot: string | undefined;
|
|
142
|
+
let changeSequence = 0;
|
|
143
|
+
let historyFloor = 0;
|
|
144
|
+
let linkIterator: MapIterator<[string, LinkedPath]> | undefined;
|
|
145
|
+
let pollIterator: SetIterator<InputEntry> | undefined;
|
|
146
|
+
let poller: { close(): void } | undefined;
|
|
147
|
+
let flushTimer: NodeJS.Timeout | undefined;
|
|
102
148
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
const open = operations.watch ?? openRecursiveWatch;
|
|
150
|
+
const openPoller = operations.poll ?? openWatchPoller;
|
|
151
|
+
const platform = operations.platform ?? process.platform;
|
|
152
|
+
const createCaseIdentities = () =>
|
|
153
|
+
createFilesystemPathIdentityContext({
|
|
154
|
+
...(operations.caseSensitive === undefined
|
|
155
|
+
? {}
|
|
156
|
+
: { caseSensitive: operations.caseSensitive }),
|
|
157
|
+
platform,
|
|
158
|
+
throwOnRealpathError: false,
|
|
159
|
+
});
|
|
160
|
+
let caseIdentities = createCaseIdentities();
|
|
161
|
+
const directoryCaseSensitivity = new Map<string, boolean>();
|
|
162
|
+
let pathIdentityMemosDirty = false;
|
|
109
163
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
/** Drop path facts after topology or ownership changes make them stale. */
|
|
165
|
+
const resetPathIdentityMemos = (): void => {
|
|
166
|
+
caseIdentities = createCaseIdentities();
|
|
167
|
+
directoryCaseSensitivity.clear();
|
|
168
|
+
pathIdentityMemosDirty = false;
|
|
169
|
+
};
|
|
115
170
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
171
|
+
/** Lexical event key under the nearest existing directory's case policy. */
|
|
172
|
+
const watchPathKey = (file: string): string => {
|
|
173
|
+
const absolute = path.resolve(file);
|
|
174
|
+
if (platform !== "win32" && platform !== "darwin") {
|
|
175
|
+
return absolute;
|
|
176
|
+
}
|
|
177
|
+
let current = path.dirname(absolute);
|
|
178
|
+
const traversed: string[] = [];
|
|
179
|
+
let sensitive: boolean | undefined;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const cacheKey = platform === "win32" ? current.toLowerCase() : current;
|
|
182
|
+
sensitive = directoryCaseSensitivity.get(cacheKey);
|
|
183
|
+
if (sensitive !== undefined) break;
|
|
184
|
+
traversed.push(cacheKey);
|
|
185
|
+
try {
|
|
186
|
+
if (fs.statSync(current).isDirectory()) {
|
|
187
|
+
sensitive = caseIdentities.caseSensitive(current);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// A missing suffix inherits the nearest existing ancestor's policy.
|
|
192
|
+
}
|
|
193
|
+
const parent = path.dirname(current);
|
|
194
|
+
if (parent === current) {
|
|
195
|
+
// The shared identity resolver uses the platform default when even the
|
|
196
|
+
// volume root cannot answer the read-only case-sensitivity probe.
|
|
197
|
+
sensitive = caseIdentities.caseSensitive(current);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
current = parent;
|
|
201
|
+
}
|
|
202
|
+
for (const directory of traversed) {
|
|
203
|
+
directoryCaseSensitivity.set(directory, sensitive);
|
|
204
|
+
}
|
|
205
|
+
return sensitive ? absolute : absolute.toLowerCase();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const unbindAlias = (alias: string, entry: InputEntry): void => {
|
|
209
|
+
const indexed = aliases.get(alias);
|
|
210
|
+
indexed?.delete(entry);
|
|
211
|
+
if (indexed?.size === 0) aliases.delete(alias);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const closeScope = (scope: WatchScope): void => {
|
|
215
|
+
scopes.delete(watchPathKey(scope.root));
|
|
216
|
+
try {
|
|
217
|
+
scope.watcher?.close();
|
|
218
|
+
} catch {
|
|
219
|
+
// The generation no longer trusts this scope, so cleanup is best effort.
|
|
220
|
+
}
|
|
221
|
+
scope.watcher = undefined;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const recordChange = (
|
|
225
|
+
eventType: string,
|
|
226
|
+
file: string,
|
|
227
|
+
): { direct: Set<string>; parent: Set<string> } => {
|
|
228
|
+
const absolute = path.resolve(file);
|
|
229
|
+
const parent = path.dirname(absolute);
|
|
230
|
+
const direct = new Set<string>();
|
|
231
|
+
const parents = new Set<string>();
|
|
232
|
+
if (eventType === "rename") {
|
|
233
|
+
// Keep the active identity context until affected entries are removed.
|
|
234
|
+
// Their alias and scope indexes were built with that context; changing
|
|
235
|
+
// only the resolver here could make later events unable to reach them.
|
|
236
|
+
// `check()` resets the memos atomically if this topology change removes
|
|
237
|
+
// an entry, while unrelated renames leave still-valid indexes intact.
|
|
238
|
+
componentLinks.clear();
|
|
239
|
+
missingComponents.clear();
|
|
240
|
+
}
|
|
241
|
+
changeSequence += 1;
|
|
242
|
+
direct.add(watchPathKey(absolute));
|
|
243
|
+
parents.add(watchPathKey(parent));
|
|
244
|
+
for (const key of direct) {
|
|
245
|
+
componentLinks.delete(key);
|
|
246
|
+
missingComponents.delete(key);
|
|
247
|
+
changes.set(key, changeSequence);
|
|
248
|
+
}
|
|
249
|
+
for (const key of parents) changes.set(key, changeSequence);
|
|
250
|
+
if (changes.size > MAX_CHANGE_HISTORY) {
|
|
251
|
+
changes.clear();
|
|
252
|
+
historyFloor = changeSequence;
|
|
119
253
|
}
|
|
120
|
-
|
|
121
|
-
poller = undefined;
|
|
254
|
+
return { direct, parent: parents };
|
|
122
255
|
};
|
|
123
|
-
|
|
256
|
+
|
|
257
|
+
const remove = (entry: InputEntry): void => {
|
|
258
|
+
entries.delete(entry.file);
|
|
259
|
+
pending.delete(entry);
|
|
260
|
+
polled.delete(entry);
|
|
261
|
+
for (const alias of entry.aliases) unbindAlias(alias, entry);
|
|
262
|
+
entry.aliases.clear();
|
|
263
|
+
for (const alias of entry.renameAliases) {
|
|
264
|
+
const indexed = renameAliases.get(alias);
|
|
265
|
+
indexed?.delete(entry);
|
|
266
|
+
if (indexed?.size === 0) renameAliases.delete(alias);
|
|
267
|
+
}
|
|
268
|
+
entry.renameAliases.clear();
|
|
269
|
+
for (const scope of entry.scopes) {
|
|
270
|
+
scope.entries.delete(entry);
|
|
271
|
+
if (scope.entries.size === 0 && !scope.pinned) closeScope(scope);
|
|
272
|
+
}
|
|
273
|
+
entry.scopes.clear();
|
|
274
|
+
for (const file of entry.links) {
|
|
275
|
+
const link = links.get(file);
|
|
276
|
+
link?.inputs.delete(entry);
|
|
277
|
+
if (link?.inputs.size === 0) links.delete(file);
|
|
278
|
+
}
|
|
279
|
+
entry.links.clear();
|
|
280
|
+
// Component resolution is only an observation-time optimization. Entries
|
|
281
|
+
// can introduce arbitrary missing ancestors, so discard the shared memo
|
|
282
|
+
// when one leaves rather than retaining its path components indefinitely.
|
|
283
|
+
componentLinks.clear();
|
|
284
|
+
missingComponents.clear();
|
|
285
|
+
if (links.size === 0) linkIterator = undefined;
|
|
286
|
+
if (polled.size === 0) pollIterator = undefined;
|
|
287
|
+
pathIdentityMemosDirty = true;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const check = (selected: Iterable<InputEntry>): void => {
|
|
124
291
|
const importers = new Set<string>();
|
|
125
|
-
for (const
|
|
126
|
-
if (
|
|
127
|
-
|
|
292
|
+
for (const entry of selected) {
|
|
293
|
+
if (entries.get(entry.file) !== entry) continue;
|
|
294
|
+
let baseline: TtscWatchInputBaseline | undefined;
|
|
295
|
+
for (const [key, condition] of entry.conditions) {
|
|
296
|
+
const state = condition.evidence?.state;
|
|
297
|
+
let changed: boolean;
|
|
298
|
+
if (state?.codec === "predicates") {
|
|
299
|
+
changed =
|
|
300
|
+
validateGraphInputObservation(entry.file, state.observation)
|
|
301
|
+
.length !== 0;
|
|
302
|
+
} else {
|
|
303
|
+
baseline ??= captureWatchInputBaseline(entry.file);
|
|
304
|
+
changed =
|
|
305
|
+
baseline === undefined ||
|
|
306
|
+
(condition.evidence?.state !== undefined
|
|
307
|
+
? !watchInputEvidenceMatchesBaseline(condition.evidence, baseline)
|
|
308
|
+
: JSON.stringify(condition.baseline) !==
|
|
309
|
+
JSON.stringify(baseline));
|
|
310
|
+
}
|
|
311
|
+
if (!changed) continue;
|
|
312
|
+
for (const importer of condition.importers) importers.add(importer);
|
|
313
|
+
entry.conditions.delete(key);
|
|
314
|
+
}
|
|
315
|
+
if (entry.conditions.size === 0) {
|
|
316
|
+
remove(entry);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (pathIdentityMemosDirty) resetPathIdentityMemos();
|
|
320
|
+
updatePoller();
|
|
321
|
+
if (server !== undefined && importers.size !== 0) {
|
|
322
|
+
invalidateImporters(server, importers);
|
|
323
|
+
sendFullReload(server);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const enqueue = (eventType: string, file: string): void => {
|
|
328
|
+
const absolute = path.resolve(file);
|
|
329
|
+
const eventKeys = recordChange(eventType, absolute);
|
|
330
|
+
for (const key of eventKeys.direct) {
|
|
331
|
+
for (const entry of aliases.get(key) ?? []) {
|
|
332
|
+
entry.changedAt = changeSequence;
|
|
333
|
+
pending.add(entry);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const key of eventKeys.parent) {
|
|
337
|
+
for (const entry of aliases.get(key) ?? []) {
|
|
338
|
+
entry.changedAt = changeSequence;
|
|
339
|
+
pending.add(entry);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (eventType === "rename") {
|
|
343
|
+
const exact = new Set<InputEntry>();
|
|
344
|
+
for (const key of eventKeys.direct) {
|
|
345
|
+
for (const entry of renameAliases.get(key) ?? []) exact.add(entry);
|
|
346
|
+
}
|
|
347
|
+
// Linux may report only the destination spelling of a directory rename.
|
|
348
|
+
// That spelling cannot be indexed before the move. Fall back to the
|
|
349
|
+
// renamed entry's parent only when no exact old spelling matched; the
|
|
350
|
+
// baseline check below still invalidates solely inputs that really moved.
|
|
351
|
+
const selected = exact;
|
|
352
|
+
if (selected.size === 0) {
|
|
353
|
+
for (const key of eventKeys.parent) {
|
|
354
|
+
for (const entry of renameAliases.get(key) ?? []) selected.add(entry);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const entry of selected) {
|
|
358
|
+
entry.changedAt = changeSequence;
|
|
359
|
+
pending.add(entry);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (pending.size === 0 || flushTimer !== undefined) return;
|
|
363
|
+
flushTimer = setTimeout(() => {
|
|
364
|
+
flushTimer = undefined;
|
|
365
|
+
const selected = [...pending];
|
|
366
|
+
pending.clear();
|
|
367
|
+
check(selected);
|
|
368
|
+
}, 0);
|
|
369
|
+
flushTimer.unref();
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const bindAlias = (entry: InputEntry, alias: string): void => {
|
|
373
|
+
alias = watchPathKey(alias);
|
|
374
|
+
if (entry.aliases.has(alias)) return;
|
|
375
|
+
entry.aliases.add(alias);
|
|
376
|
+
let indexed = aliases.get(alias);
|
|
377
|
+
if (indexed === undefined) {
|
|
378
|
+
indexed = new Set();
|
|
379
|
+
aliases.set(alias, indexed);
|
|
380
|
+
}
|
|
381
|
+
indexed.add(entry);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const bindRenameAncestors = (entry: InputEntry, file: string): void => {
|
|
385
|
+
let current = path.dirname(path.resolve(file));
|
|
386
|
+
for (;;) {
|
|
387
|
+
const key = watchPathKey(current);
|
|
388
|
+
if (!entry.renameAliases.has(key)) {
|
|
389
|
+
entry.renameAliases.add(key);
|
|
390
|
+
let indexed = renameAliases.get(key);
|
|
391
|
+
if (indexed === undefined) {
|
|
392
|
+
indexed = new Set();
|
|
393
|
+
renameAliases.set(key, indexed);
|
|
394
|
+
}
|
|
395
|
+
indexed.add(entry);
|
|
128
396
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
397
|
+
const parent = path.dirname(current);
|
|
398
|
+
if (parent === current) return;
|
|
399
|
+
current = parent;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const ensureScope = (
|
|
404
|
+
root: string,
|
|
405
|
+
external: boolean,
|
|
406
|
+
pinned = false,
|
|
407
|
+
): WatchScope | undefined => {
|
|
408
|
+
root = path.resolve(root);
|
|
409
|
+
const key = watchPathKey(root);
|
|
410
|
+
let scope = scopes.get(key);
|
|
411
|
+
if (scope === undefined) {
|
|
412
|
+
if (
|
|
413
|
+
external &&
|
|
414
|
+
[...scopes.values()].filter((candidate) => !candidate.pinned).length >=
|
|
415
|
+
MAX_EXTERNAL_WATCH_SCOPES
|
|
416
|
+
)
|
|
417
|
+
return undefined;
|
|
418
|
+
// Opening a scope is itself an observation boundary. Advance the same
|
|
419
|
+
// sequence returned by begin() so an external scope first discovered
|
|
420
|
+
// after compilation cannot claim it was already live at that token when
|
|
421
|
+
// no filesystem event happened in between.
|
|
422
|
+
changeSequence += 1;
|
|
423
|
+
scope = {
|
|
424
|
+
entries: new Set(),
|
|
425
|
+
failed: false,
|
|
426
|
+
pinned,
|
|
427
|
+
root,
|
|
428
|
+
startedAt: changeSequence,
|
|
429
|
+
};
|
|
430
|
+
scopes.set(key, scope);
|
|
431
|
+
try {
|
|
432
|
+
const owned = scope;
|
|
433
|
+
scope.watcher = open(
|
|
434
|
+
root,
|
|
435
|
+
(eventType, file) => {
|
|
436
|
+
if (scopes.get(key) !== owned) return;
|
|
437
|
+
if (file === null) {
|
|
438
|
+
resetPathIdentityMemos();
|
|
439
|
+
changeSequence += 1;
|
|
440
|
+
historyFloor = changeSequence;
|
|
441
|
+
changes.clear();
|
|
442
|
+
for (const candidate of owned.entries) {
|
|
443
|
+
candidate.changedAt = changeSequence;
|
|
444
|
+
pending.add(candidate);
|
|
445
|
+
}
|
|
446
|
+
scheduleFlush();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
enqueue(
|
|
450
|
+
eventType,
|
|
451
|
+
path.isAbsolute(file) ? file : path.resolve(root, file),
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
() => {
|
|
455
|
+
if (scopes.get(key) !== owned) return;
|
|
456
|
+
owned.failed = true;
|
|
457
|
+
try {
|
|
458
|
+
owned.watcher?.close();
|
|
459
|
+
} catch {
|
|
460
|
+
// The fallback owns validation now; a failed native handle is no
|
|
461
|
+
// longer useful, and cleanup must not replace that recovery.
|
|
462
|
+
}
|
|
463
|
+
owned.watcher = undefined;
|
|
464
|
+
for (const entry of owned.entries) requirePolling(entry);
|
|
465
|
+
updatePoller();
|
|
466
|
+
},
|
|
467
|
+
);
|
|
468
|
+
if (scope.failed) {
|
|
469
|
+
// An injected or platform watcher may report failure synchronously
|
|
470
|
+
// during construction, before its handle can be assigned above.
|
|
471
|
+
try {
|
|
472
|
+
scope.watcher?.close();
|
|
473
|
+
} catch {}
|
|
474
|
+
scope.watcher = undefined;
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
scope.failed = true;
|
|
132
478
|
}
|
|
479
|
+
} else if (pinned) {
|
|
480
|
+
scope.pinned = true;
|
|
133
481
|
}
|
|
134
|
-
|
|
135
|
-
|
|
482
|
+
return scope;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const bindScope = (
|
|
486
|
+
root: string,
|
|
487
|
+
entry: InputEntry,
|
|
488
|
+
external: boolean,
|
|
489
|
+
): boolean => {
|
|
490
|
+
const scope = ensureScope(root, external);
|
|
491
|
+
if (scope === undefined) return false;
|
|
492
|
+
scope.entries.add(entry);
|
|
493
|
+
entry.scopes.add(scope);
|
|
494
|
+
return !scope.failed;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
function requirePolling(entry: InputEntry): void {
|
|
498
|
+
entry.fallback = true;
|
|
499
|
+
if (polled.size === 0) pollIterator = undefined;
|
|
500
|
+
polled.add(entry);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const scheduleFlush = (): void => {
|
|
504
|
+
if (pending.size === 0 || flushTimer !== undefined) return;
|
|
505
|
+
flushTimer = setTimeout(() => {
|
|
506
|
+
flushTimer = undefined;
|
|
507
|
+
const selected = [...pending];
|
|
508
|
+
pending.clear();
|
|
509
|
+
check(selected);
|
|
510
|
+
}, 0);
|
|
511
|
+
flushTimer.unref();
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function updatePoller(): void {
|
|
515
|
+
const needed = links.size !== 0 || polled.size !== 0;
|
|
516
|
+
if (!needed) {
|
|
517
|
+
poller?.close();
|
|
518
|
+
poller = undefined;
|
|
136
519
|
return;
|
|
137
520
|
}
|
|
138
|
-
|
|
139
|
-
|
|
521
|
+
if (poller !== undefined) return;
|
|
522
|
+
poller = openPoller(() => {
|
|
523
|
+
const selected = new Set<InputEntry>();
|
|
524
|
+
for (
|
|
525
|
+
let count = 0;
|
|
526
|
+
count < MAX_FALLBACK_PROBES_PER_TICK && polled.size !== 0;
|
|
527
|
+
count += 1
|
|
528
|
+
) {
|
|
529
|
+
pollIterator ??= polled.values();
|
|
530
|
+
let next = pollIterator.next();
|
|
531
|
+
if (next.done) {
|
|
532
|
+
pollIterator = polled.values();
|
|
533
|
+
next = pollIterator.next();
|
|
534
|
+
}
|
|
535
|
+
if (next.done) break;
|
|
536
|
+
selected.add(next.value);
|
|
537
|
+
}
|
|
538
|
+
// Files reached through one linked directory share one topology check.
|
|
539
|
+
// Content edits remain event-driven; retargeting a junction does not
|
|
540
|
+
// reliably emit an event on its previously watched descendants. Reconcile
|
|
541
|
+
// a fixed-size slice as a safety net; ordinary retargets arrive at once
|
|
542
|
+
// through the project-root observer, while even an enormous dependency
|
|
543
|
+
// graph has constant idle CPU cost.
|
|
544
|
+
for (
|
|
545
|
+
let count = 0;
|
|
546
|
+
count < MAX_LINK_PROBES_PER_TICK && links.size !== 0;
|
|
547
|
+
count += 1
|
|
548
|
+
) {
|
|
549
|
+
linkIterator ??= links.entries();
|
|
550
|
+
let next = linkIterator.next();
|
|
551
|
+
if (next.done) {
|
|
552
|
+
linkIterator = links.entries();
|
|
553
|
+
next = linkIterator.next();
|
|
554
|
+
}
|
|
555
|
+
if (next.done) break;
|
|
556
|
+
const [file, link] = next.value;
|
|
557
|
+
const target = realpath(file);
|
|
558
|
+
if (target !== link.target) {
|
|
559
|
+
link.target = target;
|
|
560
|
+
for (const entry of link.inputs) {
|
|
561
|
+
selected.add(entry);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
check(selected);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const observe = (entry: InputEntry): void => {
|
|
570
|
+
bindAlias(entry, entry.file);
|
|
571
|
+
bindRenameAncestors(entry, entry.file);
|
|
572
|
+
const root = projectRoot;
|
|
573
|
+
if (root !== undefined && containsPath(root, entry.file)) {
|
|
574
|
+
if (!bindScope(root, entry, false)) requirePolling(entry);
|
|
575
|
+
} else {
|
|
576
|
+
const external = nearestExistingDirectory(entry.file);
|
|
577
|
+
if (external === undefined || !bindScope(external, entry, true))
|
|
578
|
+
requirePolling(entry);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const target = realpath(entry.file);
|
|
582
|
+
if (hasMultipleLinks(entry.file)) requirePolling(entry);
|
|
583
|
+
if (target !== undefined) {
|
|
584
|
+
bindAlias(entry, target);
|
|
585
|
+
bindRenameAncestors(entry, target);
|
|
586
|
+
if (!sameSpelling(entry.file, target)) {
|
|
587
|
+
let link = links.get(entry.file);
|
|
588
|
+
if (link === undefined) {
|
|
589
|
+
link = { target, inputs: new Set() };
|
|
590
|
+
if (links.size === 0) linkIterator = undefined;
|
|
591
|
+
links.set(entry.file, link);
|
|
592
|
+
}
|
|
593
|
+
link.inputs.add(entry);
|
|
594
|
+
entry.links.add(entry.file);
|
|
595
|
+
if (root === undefined || !containsPath(root, target)) {
|
|
596
|
+
const targetRoot = nearestExistingDirectory(target);
|
|
597
|
+
if (targetRoot === undefined || !bindScope(targetRoot, entry, true))
|
|
598
|
+
requirePolling(entry);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (root !== undefined) {
|
|
603
|
+
for (const linkedFile of linkedComponents(
|
|
604
|
+
entry.file,
|
|
605
|
+
root,
|
|
606
|
+
componentLinks,
|
|
607
|
+
missingComponents,
|
|
608
|
+
watchPathKey,
|
|
609
|
+
)) {
|
|
610
|
+
bindAlias(entry, linkedFile);
|
|
611
|
+
let link = links.get(linkedFile);
|
|
612
|
+
if (link === undefined) {
|
|
613
|
+
link = { target: realpath(linkedFile), inputs: new Set() };
|
|
614
|
+
if (links.size === 0) linkIterator = undefined;
|
|
615
|
+
links.set(linkedFile, link);
|
|
616
|
+
}
|
|
617
|
+
link.inputs.add(entry);
|
|
618
|
+
entry.links.add(linkedFile);
|
|
619
|
+
if (link.target === undefined) requirePolling(entry);
|
|
620
|
+
else if (
|
|
621
|
+
!containsPath(root, link.target) &&
|
|
622
|
+
!bindScope(link.target, entry, true)
|
|
623
|
+
)
|
|
624
|
+
requirePolling(entry);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
140
627
|
};
|
|
141
628
|
|
|
142
629
|
return {
|
|
143
630
|
attach(next) {
|
|
144
631
|
server = next;
|
|
632
|
+
projectRoot = path.resolve(next.config?.root ?? process.cwd());
|
|
633
|
+
ensureScope(projectRoot, false, true);
|
|
634
|
+
},
|
|
635
|
+
begin() {
|
|
636
|
+
return changeSequence;
|
|
145
637
|
},
|
|
146
|
-
dispose() {
|
|
638
|
+
async dispose() {
|
|
147
639
|
entries.clear();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
640
|
+
aliases.clear();
|
|
641
|
+
renameAliases.clear();
|
|
642
|
+
importerInputs.clear();
|
|
643
|
+
pending.clear();
|
|
644
|
+
polled.clear();
|
|
645
|
+
links.clear();
|
|
646
|
+
componentLinks.clear();
|
|
647
|
+
missingComponents.clear();
|
|
648
|
+
changes.clear();
|
|
649
|
+
linkIterator = undefined;
|
|
650
|
+
pollIterator = undefined;
|
|
651
|
+
poller?.close();
|
|
652
|
+
if (flushTimer !== undefined) clearTimeout(flushTimer);
|
|
653
|
+
poller = undefined;
|
|
654
|
+
flushTimer = undefined;
|
|
655
|
+
for (const scope of [...scopes.values()]) closeScope(scope);
|
|
656
|
+
resetPathIdentityMemos();
|
|
657
|
+
// Retain the attached server across overlapping Vite restart containers.
|
|
158
658
|
},
|
|
159
|
-
|
|
160
|
-
|
|
659
|
+
forget(importer) {
|
|
660
|
+
importer = path.resolve(importer);
|
|
661
|
+
const previous = importerInputs.get(importer);
|
|
662
|
+
if (previous === undefined) return;
|
|
663
|
+
importerInputs.delete(importer);
|
|
664
|
+
for (const [file, key] of previous) {
|
|
665
|
+
const entry = entries.get(file);
|
|
666
|
+
const condition = entry?.conditions.get(key);
|
|
667
|
+
condition?.importers.delete(importer);
|
|
668
|
+
if (condition?.importers.size === 0) entry?.conditions.delete(key);
|
|
669
|
+
if (entry?.conditions.size === 0) remove(entry);
|
|
670
|
+
}
|
|
671
|
+
if (pathIdentityMemosDirty) resetPathIdentityMemos();
|
|
672
|
+
updatePoller();
|
|
161
673
|
},
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
674
|
+
replace(importer, inputs, failed = false, startedAt) {
|
|
675
|
+
if (server === undefined) return;
|
|
676
|
+
importer = path.resolve(importer);
|
|
677
|
+
const previous =
|
|
678
|
+
importerInputs.get(importer) ?? new Map<string, string>();
|
|
679
|
+
if (failed) {
|
|
680
|
+
// An exception can omit the dependency whose deletion caused it.
|
|
681
|
+
// Keep the last successful spellings until a successful delivery can
|
|
682
|
+
// replace them, observing their current failed state for recovery.
|
|
683
|
+
const reported = new Set(
|
|
684
|
+
inputs.map((input) => path.resolve(input.file)),
|
|
685
|
+
);
|
|
686
|
+
inputs = [
|
|
687
|
+
...inputs,
|
|
688
|
+
...[...previous.keys()]
|
|
689
|
+
.filter((file) => !reported.has(file))
|
|
690
|
+
.map((file) => ({ file })),
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
const current = new Map<string, string>();
|
|
694
|
+
const added = new Set<InputEntry>();
|
|
695
|
+
const touched = new Set<InputEntry>();
|
|
696
|
+
for (const input of inputs) {
|
|
697
|
+
const file = path.resolve(input.file);
|
|
698
|
+
const evidence = input.evidence;
|
|
699
|
+
const key = JSON.stringify(evidence ?? null);
|
|
700
|
+
current.set(file, key);
|
|
701
|
+
let entry = entries.get(file);
|
|
702
|
+
if (entry === undefined) {
|
|
703
|
+
entry = {
|
|
704
|
+
aliases: new Set(),
|
|
705
|
+
changedAt: 0,
|
|
706
|
+
file,
|
|
707
|
+
conditions: new Map(),
|
|
708
|
+
fallback: false,
|
|
709
|
+
links: new Set(),
|
|
710
|
+
renameAliases: new Set(),
|
|
711
|
+
scopes: new Set(),
|
|
712
|
+
};
|
|
713
|
+
entries.set(file, entry);
|
|
714
|
+
added.add(entry);
|
|
715
|
+
observe(entry);
|
|
716
|
+
}
|
|
717
|
+
touched.add(entry);
|
|
718
|
+
let condition = entry.conditions.get(key);
|
|
719
|
+
if (condition === undefined) {
|
|
720
|
+
condition = {
|
|
721
|
+
evidence,
|
|
722
|
+
baseline:
|
|
723
|
+
evidence?.state === undefined
|
|
724
|
+
? captureWatchInputBaseline(file)
|
|
725
|
+
: undefined,
|
|
726
|
+
importers: new Set(),
|
|
727
|
+
};
|
|
728
|
+
entry.conditions.set(key, condition);
|
|
729
|
+
}
|
|
730
|
+
condition.importers.add(importer);
|
|
731
|
+
}
|
|
732
|
+
for (const [file, key] of previous) {
|
|
733
|
+
if (current.get(file) === key) continue;
|
|
734
|
+
const entry = entries.get(file);
|
|
735
|
+
const condition = entry?.conditions.get(key);
|
|
736
|
+
condition?.importers.delete(importer);
|
|
737
|
+
if (condition?.importers.size === 0) entry?.conditions.delete(key);
|
|
738
|
+
if (entry?.conditions.size === 0 && !current.has(file)) {
|
|
739
|
+
remove(entry);
|
|
740
|
+
}
|
|
169
741
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
742
|
+
if (pathIdentityMemosDirty) resetPathIdentityMemos();
|
|
743
|
+
if (current.size === 0) importerInputs.delete(importer);
|
|
744
|
+
else importerInputs.set(importer, current);
|
|
745
|
+
// Registration and removal can each touch thousands of compiler inputs.
|
|
746
|
+
// Decide the one shared poller's state once per atomic replacement,
|
|
747
|
+
// rather than rescanning the whole graph once per input.
|
|
748
|
+
updatePoller();
|
|
749
|
+
// Watchers are live before this proof. It closes the compile-to-subscribe
|
|
750
|
+
// race without manufacturing one native subscription per input.
|
|
751
|
+
if (touched.size !== 0) {
|
|
752
|
+
if (startedAt === undefined || startedAt < historyFloor) {
|
|
753
|
+
check(touched);
|
|
754
|
+
} else {
|
|
755
|
+
const raced: InputEntry[] = [];
|
|
756
|
+
for (const entry of touched) {
|
|
757
|
+
if (entry.fallback || entry.changedAt > startedAt) {
|
|
758
|
+
raced.push(entry);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const observedAfterCompile =
|
|
762
|
+
added.has(entry) &&
|
|
763
|
+
(someSet(
|
|
764
|
+
entry.aliases,
|
|
765
|
+
(alias) => (changes.get(alias) ?? 0) > startedAt,
|
|
766
|
+
) ||
|
|
767
|
+
someSet(
|
|
768
|
+
entry.renameAliases,
|
|
769
|
+
(alias) => (changes.get(alias) ?? 0) > startedAt,
|
|
770
|
+
));
|
|
771
|
+
if (
|
|
772
|
+
someSet(
|
|
773
|
+
entry.scopes,
|
|
774
|
+
(scope) =>
|
|
775
|
+
scope.failed ||
|
|
776
|
+
scope.startedAt > startedAt ||
|
|
777
|
+
observedAfterCompile,
|
|
778
|
+
)
|
|
779
|
+
) {
|
|
780
|
+
raced.push(entry);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (raced.length !== 0) check(raced);
|
|
784
|
+
}
|
|
179
785
|
}
|
|
180
786
|
},
|
|
181
787
|
};
|
|
182
788
|
}
|
|
183
789
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
): string {
|
|
189
|
-
// Missing aliases can share a physical parent now and later retarget or
|
|
190
|
-
// diverge. Neither predicate may let one lexical spelling answer for another.
|
|
191
|
-
const predicate =
|
|
192
|
-
typeof condition === "string"
|
|
193
|
-
? condition
|
|
194
|
-
: `predicates:${JSON.stringify([
|
|
195
|
-
condition.accessibleEntries === undefined
|
|
196
|
-
? null
|
|
197
|
-
: [
|
|
198
|
-
condition.accessibleEntries.directories,
|
|
199
|
-
condition.accessibleEntries.files,
|
|
200
|
-
],
|
|
201
|
-
condition.directoryExists ?? null,
|
|
202
|
-
condition.fileExists ?? null,
|
|
203
|
-
condition.readFile === undefined
|
|
204
|
-
? null
|
|
205
|
-
: condition.readFile.ok
|
|
206
|
-
? [true, condition.readFile.hash]
|
|
207
|
-
: [false],
|
|
208
|
-
condition.realpath === undefined
|
|
209
|
-
? null
|
|
210
|
-
: condition.realpath.ok
|
|
211
|
-
? [true, condition.realpath.path]
|
|
212
|
-
: [false],
|
|
213
|
-
condition.stat ?? null,
|
|
214
|
-
])}`;
|
|
215
|
-
return `${predicate}:${path.resolve(input)}`;
|
|
216
|
-
}
|
|
790
|
+
const MAX_EXTERNAL_WATCH_SCOPES = 16;
|
|
791
|
+
const MAX_CHANGE_HISTORY = 100_000;
|
|
792
|
+
const MAX_FALLBACK_PROBES_PER_TICK = 64;
|
|
793
|
+
const MAX_LINK_PROBES_PER_TICK = 64;
|
|
217
794
|
|
|
218
|
-
/**
|
|
219
|
-
function
|
|
220
|
-
|
|
795
|
+
/** Test a set without allocating a transient array on the registration path. */
|
|
796
|
+
function someSet<T>(
|
|
797
|
+
values: ReadonlySet<T>,
|
|
798
|
+
predicate: (value: T) => boolean,
|
|
221
799
|
): boolean {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
800
|
+
for (const value of values) if (predicate(value)) return true;
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function openWatchPoller(listener: () => void): { close(): void } {
|
|
805
|
+
const timer = setInterval(listener, 500);
|
|
806
|
+
timer.unref();
|
|
807
|
+
return { close: () => clearInterval(timer) };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function openRecursiveWatch(
|
|
811
|
+
root: string,
|
|
812
|
+
listener: (eventType: string, file: string | null) => void,
|
|
813
|
+
onError: () => void,
|
|
814
|
+
): { close(): void } {
|
|
815
|
+
const watcher = fs.watch(
|
|
816
|
+
root,
|
|
817
|
+
{ persistent: false, recursive: true },
|
|
818
|
+
(eventType, file) =>
|
|
819
|
+
listener(eventType, file === null ? null : String(file)),
|
|
820
|
+
);
|
|
821
|
+
watcher.on("error", onError);
|
|
822
|
+
return { close: () => watcher.close() };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function containsPath(root: string, file: string): boolean {
|
|
826
|
+
const relative = path.relative(path.resolve(root), path.resolve(file));
|
|
827
|
+
return (
|
|
828
|
+
relative === "" ||
|
|
829
|
+
(relative !== ".." &&
|
|
830
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
831
|
+
!path.isAbsolute(relative))
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function nearestExistingDirectory(file: string): string | undefined {
|
|
836
|
+
let current = path.resolve(file);
|
|
228
837
|
try {
|
|
229
|
-
if (!fs.
|
|
230
|
-
|
|
838
|
+
if (!fs.statSync(current).isDirectory()) current = path.dirname(current);
|
|
839
|
+
} catch {
|
|
840
|
+
current = path.dirname(current);
|
|
841
|
+
}
|
|
842
|
+
for (;;) {
|
|
843
|
+
try {
|
|
844
|
+
if (fs.statSync(current).isDirectory()) {
|
|
845
|
+
return path.dirname(current) === current ? undefined : current;
|
|
846
|
+
}
|
|
847
|
+
} catch {
|
|
848
|
+
// Keep climbing to the nearest directory a recursive watch can own.
|
|
231
849
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
850
|
+
const parent = path.dirname(current);
|
|
851
|
+
if (parent === current) return undefined;
|
|
852
|
+
current = parent;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function linkedComponents(
|
|
857
|
+
file: string,
|
|
858
|
+
root: string,
|
|
859
|
+
cached: Map<string, string | null>,
|
|
860
|
+
missing: Set<string>,
|
|
861
|
+
keyOf: (file: string) => string,
|
|
862
|
+
): string[] {
|
|
863
|
+
const relative = path.relative(root, file);
|
|
864
|
+
if (
|
|
865
|
+
relative === "" ||
|
|
866
|
+
relative === ".." ||
|
|
867
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
868
|
+
path.isAbsolute(relative)
|
|
869
|
+
)
|
|
870
|
+
return [];
|
|
871
|
+
const output: string[] = [];
|
|
872
|
+
let current = path.resolve(root);
|
|
873
|
+
for (const component of relative.split(path.sep).slice(0, -1)) {
|
|
874
|
+
current = path.join(current, component);
|
|
875
|
+
const key = keyOf(current);
|
|
876
|
+
if (missing.has(key)) break;
|
|
877
|
+
const known = cached.get(key);
|
|
878
|
+
if (known !== undefined) {
|
|
879
|
+
if (known !== null) output.push(current);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
884
|
+
cached.set(key, realpath(current) ?? current);
|
|
885
|
+
output.push(current);
|
|
886
|
+
} else {
|
|
887
|
+
cached.set(key, null);
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
missing.add(key);
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return output;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function realpath(file: string): string | undefined {
|
|
898
|
+
try {
|
|
899
|
+
return fs.realpathSync.native(file);
|
|
900
|
+
} catch {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Whether writes can reach this input through an unobserved hardlink alias. */
|
|
906
|
+
function hasMultipleLinks(file: string): boolean {
|
|
907
|
+
try {
|
|
908
|
+
const stats = fs.statSync(file);
|
|
909
|
+
return stats.isFile() && stats.nlink > 1;
|
|
235
910
|
} catch {
|
|
236
911
|
return false;
|
|
237
912
|
}
|
|
238
913
|
}
|
|
239
914
|
|
|
915
|
+
function sameSpelling(left: string, right: string): boolean {
|
|
916
|
+
return process.platform === "win32"
|
|
917
|
+
? path.resolve(left).toLowerCase() === path.resolve(right).toLowerCase()
|
|
918
|
+
: path.resolve(left) === path.resolve(right);
|
|
919
|
+
}
|
|
920
|
+
|
|
240
921
|
/**
|
|
241
922
|
* Invalidate every module-graph node of the registered importers so the next
|
|
242
923
|
* request retransforms them. Importers keep their original absolute spelling so
|