in packages/jest-haste-map/src/index.ts [817:1028]
private _watch(hasteMap: InternalHasteMap): Promise<void> {
if (!this._options.watch) {
return Promise.resolve();
}
// In watch mode, we'll only warn about module collisions and we'll retain
// all files, even changes to node_modules.
this._options.throwOnModuleCollision = false;
this._options.retainAllFiles = true;
// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
const Watcher =
canUseWatchman && this._options.useWatchman
? WatchmanWatcher
: FSEventsWatcher.isSupported()
? FSEventsWatcher
: NodeWatcher;
const extensions = this._options.extensions;
const ignorePattern = this._options.ignorePattern;
const rootDir = this._options.rootDir;
let changeQueue: Promise<null | void> = Promise.resolve();
let eventsQueue: EventsQueue = [];
// We only need to copy the entire haste map once on every "frame".
let mustCopy = true;
const createWatcher = (root: string): Promise<Watcher> => {
const watcher = new Watcher(root, {
dot: true,
glob: extensions.map(extension => `**/*.${extension}`),
ignored: ignorePattern,
});
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error('Failed to start watch mode.')),
MAX_WAIT_TIME,
);
watcher.once('ready', () => {
clearTimeout(rejectTimeout);
watcher.on('all', onChange);
resolve(watcher);
});
});
};
const emitChange = () => {
if (eventsQueue.length) {
mustCopy = true;
const changeEvent: ChangeEvent = {
eventsQueue,
hasteFS: new HasteFS({files: hasteMap.files, rootDir}),
moduleMap: new HasteModuleMap({
duplicates: hasteMap.duplicates,
map: hasteMap.map,
mocks: hasteMap.mocks,
rootDir,
}),
};
this.emit('change', changeEvent);
eventsQueue = [];
}
};
const onChange = (
type: string,
filePath: string,
root: string,
stat?: Stats,
) => {
filePath = path.join(root, normalizePathSep(filePath));
if (
(stat && stat.isDirectory()) ||
this._ignore(filePath) ||
!extensions.some(extension => filePath.endsWith(extension))
) {
return;
}
const relativeFilePath = fastPath.relative(rootDir, filePath);
const fileMetadata = hasteMap.files.get(relativeFilePath);
// The file has been accessed, not modified
if (
type === 'change' &&
fileMetadata &&
stat &&
fileMetadata[H.MTIME] === stat.mtime.getTime()
) {
return;
}
changeQueue = changeQueue
.then(() => {
// If we get duplicate events for the same file, ignore them.
if (
eventsQueue.find(
event =>
event.type === type &&
event.filePath === filePath &&
((!event.stat && !stat) ||
(!!event.stat &&
!!stat &&
event.stat.mtime.getTime() === stat.mtime.getTime())),
)
) {
return null;
}
if (mustCopy) {
mustCopy = false;
hasteMap = {
clocks: new Map(hasteMap.clocks),
duplicates: new Map(hasteMap.duplicates),
files: new Map(hasteMap.files),
map: new Map(hasteMap.map),
mocks: new Map(hasteMap.mocks),
};
}
const add = () => {
eventsQueue.push({filePath, stat, type});
return null;
};
const fileMetadata = hasteMap.files.get(relativeFilePath);
// If it's not an addition, delete the file and all its metadata
if (fileMetadata != null) {
const moduleName = fileMetadata[H.ID];
const platform =
getPlatformExtension(filePath, this._options.platforms) ||
H.GENERIC_PLATFORM;
hasteMap.files.delete(relativeFilePath);
let moduleMap = hasteMap.map.get(moduleName);
if (moduleMap != null) {
// We are forced to copy the object because jest-haste-map exposes
// the map as an immutable entity.
moduleMap = copy(moduleMap);
delete moduleMap[platform];
if (Object.keys(moduleMap).length === 0) {
hasteMap.map.delete(moduleName);
} else {
hasteMap.map.set(moduleName, moduleMap);
}
}
if (
this._options.mocksPattern &&
this._options.mocksPattern.test(filePath)
) {
const mockName = getMockName(filePath);
hasteMap.mocks.delete(mockName);
}
this._recoverDuplicates(hasteMap, relativeFilePath, moduleName);
}
// If the file was added or changed,
// parse it and update the haste map.
if (type === 'add' || type === 'change') {
invariant(
stat,
'since the file exists or changed, it should have stats',
);
const fileMetadata: FileMetaData = [
'',
stat.mtime.getTime(),
stat.size,
0,
'',
null,
];
hasteMap.files.set(relativeFilePath, fileMetadata);
const promise = this._processFile(
hasteMap,
hasteMap.map,
hasteMap.mocks,
filePath,
{forceInBand: true},
);
// Cleanup
this._cleanup();
if (promise) {
return promise.then(add);
} else {
// If a file in node_modules has changed,
// emit an event regardless.
add();
}
} else {
add();
}
return null;
})
.catch((error: Error) => {
this._console.error(
`jest-haste-map: watch error:\n ${error.stack}\n`,
);
});
};
this._changeInterval = setInterval(emitChange, CHANGE_INTERVAL);
return Promise.all(this._options.roots.map(createWatcher)).then(
watchers => {
this._watchers = watchers;
},
);
}