watching.js
8.11 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
// @ts-check
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "createWatcher", {
enumerable: true,
get: ()=>createWatcher
});
const _chokidar = /*#__PURE__*/ _interopRequireDefault(require("chokidar"));
const _fs = /*#__PURE__*/ _interopRequireDefault(require("fs"));
const _micromatch = /*#__PURE__*/ _interopRequireDefault(require("micromatch"));
const _normalizePath = /*#__PURE__*/ _interopRequireDefault(require("normalize-path"));
const _path = /*#__PURE__*/ _interopRequireDefault(require("path"));
const _utilsJs = require("./utils.js");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
function createWatcher(args, { state , rebuild }) {
let shouldPoll = args["--poll"];
let shouldCoalesceWriteEvents = shouldPoll || process.platform === "win32";
// Polling interval in milliseconds
// Used only when polling or coalescing add/change events on Windows
let pollInterval = 10;
let watcher = _chokidar.default.watch([], {
// Force checking for atomic writes in all situations
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
// This only works when watching directories though
atomic: true,
usePolling: shouldPoll,
interval: shouldPoll ? pollInterval : undefined,
ignoreInitial: true,
awaitWriteFinish: shouldCoalesceWriteEvents ? {
stabilityThreshold: 50,
pollInterval: pollInterval
} : false
});
// A queue of rebuilds, file reads, etc… to run
let chain = Promise.resolve();
/**
* A list of files that have been changed since the last rebuild
*
* @type {{file: string, content: () => Promise<string>, extension: string}[]}
*/ let changedContent = [];
/**
* A list of files for which a rebuild has already been queued.
* This is used to prevent duplicate rebuilds when multiple events are fired for the same file.
* The rebuilt file is cleared from this list when it's associated rebuild has _started_
* This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should
**/ let pendingRebuilds = new Set();
let _timer;
let _reject;
/**
* Rebuilds the changed files and resolves when the rebuild is
* complete regardless of whether it was successful or not
*/ async function rebuildAndContinue() {
let changes = changedContent.splice(0);
// There are no changes to rebuild so we can just do nothing
if (changes.length === 0) {
return Promise.resolve();
}
// Clear all pending rebuilds for the about-to-be-built files
changes.forEach((change)=>pendingRebuilds.delete(change.file));
// Resolve the promise even when the rebuild fails
return rebuild(changes).then(()=>{}, ()=>{});
}
/**
*
* @param {*} file
* @param {(() => Promise<string>) | null} content
* @param {boolean} skipPendingCheck
* @returns {Promise<void>}
*/ function recordChangedFile(file, content = null, skipPendingCheck = false) {
file = _path.default.resolve(file);
// Applications like Vim/Neovim fire both rename and change events in succession for atomic writes
// In that case rebuild has already been queued by rename, so can be skipped in change
if (pendingRebuilds.has(file) && !skipPendingCheck) {
return Promise.resolve();
}
// Mark that a rebuild of this file is going to happen
// It MUST happen synchronously before the rebuild is queued for this to be effective
pendingRebuilds.add(file);
changedContent.push({
file,
content: content !== null && content !== void 0 ? content : ()=>_fs.default.promises.readFile(file, "utf8"),
extension: _path.default.extname(file).slice(1)
});
if (_timer) {
clearTimeout(_timer);
_reject();
}
// If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired
chain = chain.then(()=>new Promise((resolve, reject)=>{
_timer = setTimeout(resolve, 10);
_reject = reject;
}));
// Resolves once this file has been rebuilt (or the rebuild for this file has failed)
// This queues as many rebuilds as there are changed files
// But those rebuilds happen after some delay
// And will immediately resolve if there are no changes
chain = chain.then(rebuildAndContinue, rebuildAndContinue);
return chain;
}
watcher.on("change", (file)=>recordChangedFile(file));
watcher.on("add", (file)=>recordChangedFile(file));
// Restore watching any files that are "removed"
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
// TODO: An an optimization we should allow removal when the config changes
watcher.on("unlink", (file)=>{
file = (0, _normalizePath.default)(file);
// Only re-add the file if it's not covered by a dynamic pattern
if (!_micromatch.default.some([
file
], state.contentPatterns.dynamic)) {
watcher.add(file);
}
});
// Some applications such as Visual Studio (but not VS Code)
// will only fire a rename event for atomic writes and not a change event
// This is very likely a chokidar bug but it's one we need to work around
// We treat this as a change event and rebuild the CSS
watcher.on("raw", (evt, filePath, meta)=>{
if (evt !== "rename") {
return;
}
let watchedPath = meta.watchedPath;
// Watched path might be the file itself
// Or the directory it is in
filePath = watchedPath.endsWith(filePath) ? watchedPath : _path.default.join(watchedPath, filePath);
// Skip this event since the files it is for does not match any of the registered content globs
if (!_micromatch.default.some([
filePath
], state.contentPatterns.all)) {
return;
}
// Skip since we've already queued a rebuild for this file that hasn't happened yet
if (pendingRebuilds.has(filePath)) {
return;
}
// We'll go ahead and add the file to the pending rebuilds list here
// It'll be removed when the rebuild starts unless the read fails
// which will be taken care of as well
pendingRebuilds.add(filePath);
async function enqueue() {
try {
// We need to read the file as early as possible outside of the chain
// because it may be gone by the time we get to it. doing the read
// immediately increases the chance that the file is still there
let content = await (0, _utilsJs.readFileWithRetries)(_path.default.resolve(filePath));
if (content === undefined) {
return;
}
// This will push the rebuild onto the chain
// We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux
// This is because the order of events and timing is different on Linux
// @ts-ignore: TypeScript isn't picking up that content is a string here
await recordChangedFile(filePath, ()=>content, true);
} catch {
// If reading the file fails, it's was probably a deleted temporary file
// So we can ignore it and no rebuild is needed
}
}
enqueue().then(()=>{
// If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list
pendingRebuilds.delete(filePath);
});
});
return {
fswatcher: watcher,
refreshWatchedFiles () {
watcher.add(Array.from(state.contextDependencies));
watcher.add(Array.from(state.configDependencies));
watcher.add(state.contentPatterns.all);
}
};
}