123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- // @ts-check
- "use strict";
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
- Object.defineProperty(exports, "createWatcher", {
- enumerable: true,
- get: function() {
- return createWatcher;
- }
- });
- const _chokidar = /*#__PURE__*/ _interop_require_default(require("chokidar"));
- const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
- const _micromatch = /*#__PURE__*/ _interop_require_default(require("micromatch"));
- const _normalizepath = /*#__PURE__*/ _interop_require_default(require("normalize-path"));
- const _path = /*#__PURE__*/ _interop_require_default(require("path"));
- const _utils = require("./utils.js");
- function _interop_require_default(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(()=>{}, (e)=>{
- console.error(e.toString());
- });
- }
- /**
- *
- * @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" || filePath === null) {
- 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, _utils.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.configBag.dependencies));
- watcher.add(state.contentPatterns.all);
- }
- };
- }
|