watching.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. // @ts-check
  2. "use strict";
  3. Object.defineProperty(exports, "__esModule", {
  4. value: true
  5. });
  6. Object.defineProperty(exports, "createWatcher", {
  7. enumerable: true,
  8. get: function() {
  9. return createWatcher;
  10. }
  11. });
  12. const _chokidar = /*#__PURE__*/ _interop_require_default(require("chokidar"));
  13. const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
  14. const _micromatch = /*#__PURE__*/ _interop_require_default(require("micromatch"));
  15. const _normalizepath = /*#__PURE__*/ _interop_require_default(require("normalize-path"));
  16. const _path = /*#__PURE__*/ _interop_require_default(require("path"));
  17. const _utils = require("./utils.js");
  18. function _interop_require_default(obj) {
  19. return obj && obj.__esModule ? obj : {
  20. default: obj
  21. };
  22. }
  23. function createWatcher(args, { state , rebuild }) {
  24. let shouldPoll = args["--poll"];
  25. let shouldCoalesceWriteEvents = shouldPoll || process.platform === "win32";
  26. // Polling interval in milliseconds
  27. // Used only when polling or coalescing add/change events on Windows
  28. let pollInterval = 10;
  29. let watcher = _chokidar.default.watch([], {
  30. // Force checking for atomic writes in all situations
  31. // This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
  32. // This only works when watching directories though
  33. atomic: true,
  34. usePolling: shouldPoll,
  35. interval: shouldPoll ? pollInterval : undefined,
  36. ignoreInitial: true,
  37. awaitWriteFinish: shouldCoalesceWriteEvents ? {
  38. stabilityThreshold: 50,
  39. pollInterval: pollInterval
  40. } : false
  41. });
  42. // A queue of rebuilds, file reads, etc… to run
  43. let chain = Promise.resolve();
  44. /**
  45. * A list of files that have been changed since the last rebuild
  46. *
  47. * @type {{file: string, content: () => Promise<string>, extension: string}[]}
  48. */ let changedContent = [];
  49. /**
  50. * A list of files for which a rebuild has already been queued.
  51. * This is used to prevent duplicate rebuilds when multiple events are fired for the same file.
  52. * The rebuilt file is cleared from this list when it's associated rebuild has _started_
  53. * This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should
  54. **/ let pendingRebuilds = new Set();
  55. let _timer;
  56. let _reject;
  57. /**
  58. * Rebuilds the changed files and resolves when the rebuild is
  59. * complete regardless of whether it was successful or not
  60. */ async function rebuildAndContinue() {
  61. let changes = changedContent.splice(0);
  62. // There are no changes to rebuild so we can just do nothing
  63. if (changes.length === 0) {
  64. return Promise.resolve();
  65. }
  66. // Clear all pending rebuilds for the about-to-be-built files
  67. changes.forEach((change)=>pendingRebuilds.delete(change.file));
  68. // Resolve the promise even when the rebuild fails
  69. return rebuild(changes).then(()=>{}, (e)=>{
  70. console.error(e.toString());
  71. });
  72. }
  73. /**
  74. *
  75. * @param {*} file
  76. * @param {(() => Promise<string>) | null} content
  77. * @param {boolean} skipPendingCheck
  78. * @returns {Promise<void>}
  79. */ function recordChangedFile(file, content = null, skipPendingCheck = false) {
  80. file = _path.default.resolve(file);
  81. // Applications like Vim/Neovim fire both rename and change events in succession for atomic writes
  82. // In that case rebuild has already been queued by rename, so can be skipped in change
  83. if (pendingRebuilds.has(file) && !skipPendingCheck) {
  84. return Promise.resolve();
  85. }
  86. // Mark that a rebuild of this file is going to happen
  87. // It MUST happen synchronously before the rebuild is queued for this to be effective
  88. pendingRebuilds.add(file);
  89. changedContent.push({
  90. file,
  91. content: content !== null && content !== void 0 ? content : ()=>_fs.default.promises.readFile(file, "utf8"),
  92. extension: _path.default.extname(file).slice(1)
  93. });
  94. if (_timer) {
  95. clearTimeout(_timer);
  96. _reject();
  97. }
  98. // If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired
  99. chain = chain.then(()=>new Promise((resolve, reject)=>{
  100. _timer = setTimeout(resolve, 10);
  101. _reject = reject;
  102. }));
  103. // Resolves once this file has been rebuilt (or the rebuild for this file has failed)
  104. // This queues as many rebuilds as there are changed files
  105. // But those rebuilds happen after some delay
  106. // And will immediately resolve if there are no changes
  107. chain = chain.then(rebuildAndContinue, rebuildAndContinue);
  108. return chain;
  109. }
  110. watcher.on("change", (file)=>recordChangedFile(file));
  111. watcher.on("add", (file)=>recordChangedFile(file));
  112. // Restore watching any files that are "removed"
  113. // 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)
  114. // TODO: An an optimization we should allow removal when the config changes
  115. watcher.on("unlink", (file)=>{
  116. file = (0, _normalizepath.default)(file);
  117. // Only re-add the file if it's not covered by a dynamic pattern
  118. if (!_micromatch.default.some([
  119. file
  120. ], state.contentPatterns.dynamic)) {
  121. watcher.add(file);
  122. }
  123. });
  124. // Some applications such as Visual Studio (but not VS Code)
  125. // will only fire a rename event for atomic writes and not a change event
  126. // This is very likely a chokidar bug but it's one we need to work around
  127. // We treat this as a change event and rebuild the CSS
  128. watcher.on("raw", (evt, filePath, meta)=>{
  129. if (evt !== "rename" || filePath === null) {
  130. return;
  131. }
  132. let watchedPath = meta.watchedPath;
  133. // Watched path might be the file itself
  134. // Or the directory it is in
  135. filePath = watchedPath.endsWith(filePath) ? watchedPath : _path.default.join(watchedPath, filePath);
  136. // Skip this event since the files it is for does not match any of the registered content globs
  137. if (!_micromatch.default.some([
  138. filePath
  139. ], state.contentPatterns.all)) {
  140. return;
  141. }
  142. // Skip since we've already queued a rebuild for this file that hasn't happened yet
  143. if (pendingRebuilds.has(filePath)) {
  144. return;
  145. }
  146. // We'll go ahead and add the file to the pending rebuilds list here
  147. // It'll be removed when the rebuild starts unless the read fails
  148. // which will be taken care of as well
  149. pendingRebuilds.add(filePath);
  150. async function enqueue() {
  151. try {
  152. // We need to read the file as early as possible outside of the chain
  153. // because it may be gone by the time we get to it. doing the read
  154. // immediately increases the chance that the file is still there
  155. let content = await (0, _utils.readFileWithRetries)(_path.default.resolve(filePath));
  156. if (content === undefined) {
  157. return;
  158. }
  159. // This will push the rebuild onto the chain
  160. // We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux
  161. // This is because the order of events and timing is different on Linux
  162. // @ts-ignore: TypeScript isn't picking up that content is a string here
  163. await recordChangedFile(filePath, ()=>content, true);
  164. } catch {
  165. // If reading the file fails, it's was probably a deleted temporary file
  166. // So we can ignore it and no rebuild is needed
  167. }
  168. }
  169. enqueue().then(()=>{
  170. // If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list
  171. pendingRebuilds.delete(filePath);
  172. });
  173. });
  174. return {
  175. fswatcher: watcher,
  176. refreshWatchedFiles () {
  177. watcher.add(Array.from(state.contextDependencies));
  178. watcher.add(Array.from(state.configBag.dependencies));
  179. watcher.add(state.contentPatterns.all);
  180. }
  181. };
  182. }