UNPKG

@remotion/bundler

Version:

Bundle Remotion compositions using Webpack

287 lines (286 loc) 16 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WatchIgnoreNextChangePlugin = void 0; // Webpack plugin that suppresses the next file change for dynamically specified files. // Used to prevent a full webpack rebuild when the studio writes to a watched file // (e.g. updating default props). On ignoreNextChange(), the current file and directory // timestamps are snapshotted. When watchpack fires the aggregated callback, the old // timestamps are restored, the file is removed from changedFiles, and the original // callback is skipped (which would trigger _invalidate → compile). The watchpack // listeners are re-registered and unpaused so subsequent real changes still work. const node_path_1 = __importDefault(require("node:path")); class WatchIgnoreNextChangePlugin { filesToIgnore = new Set(); dirsToIgnore = new Set(); snapshotFileTimestamps = new Map(); snapshotDirTimestamps = new Map(); currentWatcher = null; trace; constructor(trace) { this.trace = trace; } ignoreNextChange(file) { var _a; this.filesToIgnore.add(file); const dir = node_path_1.default.dirname(file); this.dirsToIgnore.add(dir); // Snapshot current timestamps from the watcher BEFORE the file is written if ((_a = this.currentWatcher) === null || _a === void 0 ? void 0 : _a.getInfo) { const info = this.currentWatcher.getInfo(); const fileTs = info.fileTimeInfoEntries.get(file); if (fileTs !== undefined) { this.snapshotFileTimestamps.set(file, fileTs); } const dirTs = info.contextTimeInfoEntries.get(dir); if (dirTs !== undefined) { this.snapshotDirTimestamps.set(dir, dirTs); } // Also snapshot ancestor directories — watchpack may report them // as changed alongside the file itself. let ancestor = node_path_1.default.dirname(dir); while (ancestor !== node_path_1.default.dirname(ancestor)) { const ancestorTs = info.contextTimeInfoEntries.get(ancestor); if (ancestorTs !== undefined) { this.snapshotDirTimestamps.set(ancestor, ancestorTs); } ancestor = node_path_1.default.dirname(ancestor); } } this.trace('[WatchIgnoreNextChange] Registered ignore for', file); } unignoreNextChange(file) { this.filesToIgnore.delete(file); const dir = node_path_1.default.dirname(file); this.dirsToIgnore.delete(dir); this.snapshotFileTimestamps.delete(file); this.snapshotDirTimestamps.delete(dir); } apply(compiler) { compiler.hooks.afterEnvironment.tap('WatchIgnoreNextChangePlugin', () => { const wfs = compiler.watchFileSystem; if (!(wfs === null || wfs === void 0 ? void 0 : wfs.watch)) { return; } const originalWatch = wfs.watch.bind(wfs); const self = this; wfs.watch = (files, directories, missing, startTime, options, callback, callbackUndelayed) => { const isAncestorOfIgnoredFile = (dir) => { for (const file of self.filesToIgnore) { if (file.startsWith(dir + node_path_1.default.sep)) { return true; } } return false; }; const wrappedCallbackUndelayed = (fileName, changeTime) => { if (self.filesToIgnore.has(fileName) || self.dirsToIgnore.has(fileName) || isAncestorOfIgnoredFile(fileName)) { self.trace('[WatchIgnoreNextChange] Suppressed callbackUndelayed for', fileName); return; } callbackUndelayed(fileName, changeTime); }; const wrappedCallback = (err, fileTimestamps, dirTimestamps, changedFiles, removedFiles) => { var _a, _b, _c, _d; const hasIgnoredFiles = self.filesToIgnore.size > 0; const suppressedFiles = []; const suppressedDirs = []; if (fileTimestamps) { for (const file of [...self.filesToIgnore]) { const wasInChanged = (_a = changedFiles === null || changedFiles === void 0 ? void 0 : changedFiles.has(file)) !== null && _a !== void 0 ? _a : false; if (wasInChanged) { changedFiles.delete(file); } const prev = self.snapshotFileTimestamps.get(file); if (prev !== undefined) { fileTimestamps.set(file, prev); } if (wasInChanged) { suppressedFiles.push(file); self.filesToIgnore.delete(file); self.snapshotFileTimestamps.delete(file); } } } if (dirTimestamps) { for (const dir of [...self.dirsToIgnore]) { const wasInChanged = (_b = changedFiles === null || changedFiles === void 0 ? void 0 : changedFiles.has(dir)) !== null && _b !== void 0 ? _b : false; if (wasInChanged) { changedFiles.delete(dir); } const prev = self.snapshotDirTimestamps.get(dir); if (prev !== undefined) { dirTimestamps.set(dir, prev); } if (wasInChanged) { suppressedDirs.push(dir); self.dirsToIgnore.delete(dir); self.snapshotDirTimestamps.delete(dir); } } } if (removedFiles) { for (const file of self.filesToIgnore) { removedFiles.delete(file); } for (const dir of self.dirsToIgnore) { removedFiles.delete(dir); } } // Remove ancestor directories of suppressed files from changedFiles. // Watchpack may report parent directories (e.g. /src) as changed // alongside the file itself (e.g. /src/Sub/index.tsx). // dirsToIgnore only has the immediate parent, so ancestors // like /src would remain and trigger a rebuild. if (changedFiles && suppressedFiles.length > 0) { for (const changed of [...changedFiles]) { for (const file of suppressedFiles) { if (file.startsWith(changed + node_path_1.default.sep)) { changedFiles.delete(changed); if (dirTimestamps) { const prev = self.snapshotDirTimestamps.get(changed); if (prev !== undefined) { dirTimestamps.set(changed, prev); } } break; } } } } const remainingChanges = (_c = changedFiles === null || changedFiles === void 0 ? void 0 : changedFiles.size) !== null && _c !== void 0 ? _c : 0; const remainingRemovals = (_d = removedFiles === null || removedFiles === void 0 ? void 0 : removedFiles.size) !== null && _d !== void 0 ? _d : 0; if (hasIgnoredFiles && remainingChanges === 0 && remainingRemovals === 0) { self.trace('[WatchIgnoreNextChange] All changes suppressed, re-registering watcher. Suppressed files:', suppressedFiles, 'dirs:', suppressedDirs); // Don't call the callback — that would trigger _invalidate() → compile(). // Instead, re-register the watchpack listeners and unpause, // so the watch loop stays alive for future changes. // NodeWatchFileSystem uses: // this.watcher.once("change", callbackUndelayed) // this.watcher.once("aggregated", (changes, removals) => { this.watcher.pause(); callback(...) }) // After "aggregated" fires, the watcher is paused and the .once listeners are consumed. // We need to restore them. const watchpack = wfs.watcher; if (watchpack) { watchpack.once('change', wrappedCallbackUndelayed); watchpack.once('aggregated', (...args) => { const changes = args[0]; const removals = args[1]; watchpack.pause(); const fs = wfs.inputFileSystem; if (fs === null || fs === void 0 ? void 0 : fs.purge) { for (const item of changes) { fs.purge(item); } for (const item of removals) { fs.purge(item); } } const fetchedFileTimestamps = new Map(); const fetchedContextTimestamps = new Map(); watchpack.collectTimeInfoEntries(fetchedFileTimestamps, fetchedContextTimestamps); wrappedCallback(null, fetchedFileTimestamps, fetchedContextTimestamps, changes, removals); }); watchpack.paused = false; // Clear suppressed files from accumulated changes. // While paused, watchpack's _onChange still adds to // aggregatedChanges. If we don't clear them, unpausing // will cause the same files to re-trigger via _onTimeout. for (const file of suppressedFiles) { watchpack.aggregatedChanges.delete(file); watchpack.aggregatedRemovals.delete(file); } for (const dir of suppressedDirs) { watchpack.aggregatedChanges.delete(dir); watchpack.aggregatedRemovals.delete(dir); } // Also clear ancestor directories of suppressed files for (const file of suppressedFiles) { let ancestor = node_path_1.default.dirname(node_path_1.default.dirname(file)); while (ancestor !== node_path_1.default.dirname(ancestor)) { watchpack.aggregatedChanges.delete(ancestor); watchpack.aggregatedRemovals.delete(ancestor); ancestor = node_path_1.default.dirname(ancestor); } } } else { callback(err, fileTimestamps, dirTimestamps, changedFiles, removedFiles); } return; } if (hasIgnoredFiles) { self.trace('[WatchIgnoreNextChange] Partial suppression. Remaining changes:', changedFiles ? [...changedFiles] : []); } callback(err, fileTimestamps, dirTimestamps, changedFiles, removedFiles); }; const watcher = originalWatch(files, directories, missing, startTime, options, wrappedCallback, wrappedCallbackUndelayed); self.currentWatcher = watcher; const originalGetFileTimeInfoEntries = watcher.getFileTimeInfoEntries.bind(watcher); watcher.getFileTimeInfoEntries = () => { const entries = originalGetFileTimeInfoEntries(); for (const file of self.filesToIgnore) { const prev = self.snapshotFileTimestamps.get(file); if (prev !== undefined) { entries.set(file, prev); } } return entries; }; const originalGetContextTimeInfoEntries = watcher.getContextTimeInfoEntries.bind(watcher); watcher.getContextTimeInfoEntries = () => { const entries = originalGetContextTimeInfoEntries(); for (const dir of self.dirsToIgnore) { const prev = self.snapshotDirTimestamps.get(dir); if (prev !== undefined) { entries.set(dir, prev); } } return entries; }; if (watcher.getInfo) { const originalGetInfo = watcher.getInfo.bind(watcher); watcher.getInfo = () => { const info = originalGetInfo(); for (const file of self.filesToIgnore) { info.changes.delete(file); const prev = self.snapshotFileTimestamps.get(file); if (prev !== undefined) { info.fileTimeInfoEntries.set(file, prev); } } for (const dir of self.dirsToIgnore) { info.changes.delete(dir); const prev = self.snapshotDirTimestamps.get(dir); if (prev !== undefined) { info.contextTimeInfoEntries.set(dir, prev); } } return info; }; } if (watcher.getAggregatedChanges) { const originalGetAggregatedChanges = watcher.getAggregatedChanges.bind(watcher); watcher.getAggregatedChanges = () => { const changes = originalGetAggregatedChanges(); for (const file of self.filesToIgnore) { changes.delete(file); } for (const dir of self.dirsToIgnore) { changes.delete(dir); } return changes; }; } return watcher; }; }); } } exports.WatchIgnoreNextChangePlugin = WatchIgnoreNextChangePlugin;