5.8 KiB
jest-haste-map — agent notes
What lives here
jest-haste-map crawls the file system, maintains an in-memory module map, and drives watch mode. src/index.ts is the orchestration entry point.
Key files to know:
lib/FileProcessor.ts—processFile(worker dispatch, haste-id extraction, duplicate tracking) andbuildHasteMap(initial build loop).lib/CacheManager.ts— v8 serialize/deserialize for the on-disk cache. Sync I/O is intentional — at haste-map's scale, async overhead adds no value and switching tofs.promisesis not a free win.lib/walk.ts— sharedfdir-backed directory walker used by the node crawler (crawlers/node.tsfind),FSEventsWatcher.tsstartup, andNodeWatcher.jsstartup (viawatchers/common.jsrecReaddir). Exportswalk(opts, done): void(callback-based) andWalkOptions,WalkEntryKind. Usesfdir's.withCallback()API with a bounded callback-drivenlstatpool (defaultMath.max(os.availableParallelism() * 4, 32)inflight).fdiris constructed withnew Fdir({fs})so it usesgraceful-fsforreaddir— mockgraceful-fs.readdirin tests to control directory traversal.WalkOptions.statCacheis an optionalMap<string, Stats>passed by callers that invokewalk()multiple times (e.g. once per root): a path already in the map skips itslstatcall. The cache must not be stored on watcher instances or passed to runtime (post-startup) walks — stale stats would be returned for files changed after startup.find()andWatcherDriver.start()each create a fresh cache, use it for all per-root startup walks, then discard it.watchers/ChangeQueue.ts— 30 ms debounce, O(1) mtime-dedup viaSet<string>, copy-on-write for the live map, file-processing dispatch.crawlers/watchman.ts— fb-watchman with clock-based incremental updates.crawlers/node.ts—findNative(find(1)shell-out) +find(fdirvialib/walk);forceNodeFilesystemAPIgates shell-out vsfdir.watchers/types.ts—IWatcher,WatcherOptions,WatcherCtor. New backends must implementIWatcherand accept(root: string, opts: WatcherOptions).watchers/WatchmanWatcher.js— macOS/Linux watchman.FSEventsWatcher.ts— macOS native (startup walk vialib/walk).NodeWatcher.js— cross-platform fallback.
Data model
FileMetaData is a positional 6-tuple; use the H constants from src/constants.ts:
| Index | Constant | Value |
|---|---|---|
| 0 | H.ID |
haste module name |
| 1 | H.MTIME |
mtime (ms) |
| 2 | H.SIZE |
file size |
| 3 | H.VISITED |
0 = unprocessed, 1 = processed |
| 4 | H.DEPENDENCIES |
NUL-delimited dep list string |
| 5 | H.SHA1 |
sha1 or null |
InternalHasteMap.map is haste-id → {platform → [path, type]}. duplicates tracks haste-id collisions. clocks persists watchman's incremental state.
Hard rules
Platform collision check is === 0, not === 1. FileProcessor checks Object.keys(moduleMap).length === 0 before deleting the whole module entry. The old === 1 incorrectly dropped surviving sibling entries. Don't revert it.
ChangeQueue stat is required for add/change events. The queue reads stat.isDirectory(), stat.mtime.getTime(), and stat.size. New watcher backends must supply stat (via lstat if the backend doesn't provide it). Delete events omit stat — that is correct.
WatchmanWatcher ≠ parcel-watcher. They both may use watchman internally but are independent codepaths. WatchmanWatcher stores clocks in InternalHasteMap.clocks; a ParcelWatcher would use parcel's opaque snapshot files.
useWatchman, enableSymlinks, forceNodeFilesystemAPI are flat fields on InternalOptions, copied directly from the Options input in the constructor. All decisions flow through shouldUseWatchman(useWatchman).
enableSymlinks guard fires when enableSymlinks && useWatchman.
enableSymlinks symlink semantics. When false (default), symlinks are excluded by all walkers. When true: find (fdir path) passes enableSymlinks to walk(), which sets excludeSymlinks: false and keeps resolveSymlinks: false (fdir mode 2) — symlinks are included at their original link path (not realpath); walk() then calls fs.stat (follows the link) to get target stats (mtime/size). This preserves the path Jest uses to require the file while reporting the target's metadata. findNative (shell find path) uses ( -type f -o -type l ) when enableSymlinks is true to include symlinks, then fs.stats each result. Do NOT set fdir's resolveSymlinks: true — it calls realpath and returns the resolved path, which would cause haste-map to track files under the wrong path.
Config wiring from jest-config to jest-haste-map: HasteMap.Options fields come from two places in ProjectConfig: haste.enableSymlinks → enableSymlinks, haste.forceNodeFilesystemAPI → forceNodeFilesystemAPI. The useWatchman field comes from the caller (e.g. jest-runtime passes options?.watchman; jest-core passes globalConfig.watchman). If you add a new haste.* config key that needs to reach HasteMap, add it to HasteConfig in jest-types/src/Config.ts, HasteConfig schema in jest-schemas/src/raw-types.ts, Defaults.ts (if it has a default), ValidConfig.ts (both initialOptions.haste and initialProjectOptions.haste), and the HasteMap.create(...) call in jest-runtime/src/index.ts.
Tests
src/__tests__/index.test.js is the large integration suite — mocks NodeWatcher/WatchmanWatcher, drives events via mockEmitters[root].emit('all', ...). Per-module tests live in src/lib/__tests__/, src/watchers/__tests__/, src/crawlers/__tests__/.