183 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			183 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| // @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);
 | |
|         }
 | |
|     };
 | |
| }
 |