@remotion/bundler
Version:
Bundle Remotion compositions using Webpack
287 lines (286 loc) • 16 kB
JavaScript
"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;