Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore(haste-map): refactor massive class into multiple files#16180

Merged
SimenB merged 19 commits into
jestjs:mainfrom
SimenB:split-haste-map
May 15, 2026
Merged

chore(haste-map): refactor massive class into multiple files#16180
SimenB merged 19 commits into
jestjs:mainfrom
SimenB:split-haste-map

Conversation

@SimenB
Copy link
Copy Markdown
Member

@SimenB SimenB commented May 14, 2026

Summary

I am exploring using @parcel/watcher https://www.npmjs.com/package/@parcel/watcher. This first PR splits up the godclass in jest-haste-map to make the future changes easier.

Behavior-preserving refactor of packages/jest-haste-map/src/index.ts. The file was 1157 lines with a single HasteMap class owning every concern: option normalization, cache I/O, crawl orchestration, file-processing pipeline, watch orchestration, the change queue, duplicate recovery, and worker management.

This PR splits it into focused modules, leaving index.ts at ~350 lines of orchestration:

  • lib/util.tscopy, copyMap, createEmptyMap
  • lib/buildIgnoreMatcher.ts — ignore predicate builder
  • lib/CacheManager.ts — v8 serialize/deserialize cache I/O
  • lib/WorkerPool.tsjest-worker lifecycle
  • crawlers/index.ts — watchman → node fallback driver
  • watchers/index.tsWatcherDriver + shouldUseWatchman
  • watchers/ChangeQueue.ts — watch-mode event accumulation
  • lib/FileProcessor.ts_processFile / buildHasteMap + DuplicateError

Each extraction is a separate commit with unit tests. The public API (JestHasteMap, DuplicateError, IHasteMap, all type re-exports) is unchanged.

Also includes a small perf fix in ChangeQueue: the previous O(n) linear scan for duplicate events (eventsQueue.some(...)) is replaced with a Set<string> keyed on ${type}:${filePath}:${mtime}, making dedup O(1).

Additional fixes included

Bug fix: haste-id module collision deleted surviving platform entries

FileProcessor.ts had an off-by-one: after deleting a colliding platform entry from a module's platform map, it checked === 1 (one entry remaining) before deleting the whole module entry, but should have checked === 0. This caused sibling platform entries (g, android, etc.) to be incorrectly dropped when only the colliding platform should have been removed.

Bug fix: watch-mode startup failure cleanup

Three related issues in watchers/index.ts:

  1. On watcher ready-timeout, the 'ready' listener was left registered and watcher.close() was never called, leaking the backend's resources.
  2. Promise.all in WatcherDriver.start() meant that if one root's watcher failed to start, already-started sibling watchers were silently abandoned without being closed. Fixed with Promise.allSettled + close surviving watchers before re-throwing as AggregateError.
  3. In index.ts, if WatcherDriver.start() threw, the ChangeQueue interval that was already started would continue firing indefinitely. Fixed with a try/catch that stops the queue on failure.

TypeScript hygiene

  • All new test files (lib/__tests__/, crawlers/__tests__/, watchers/__tests__/) now pass yarn typecheck:tests. Rewrote mock setup to use jest.fn<typeof SomeFn>(), jest.mocked(), and jest.MockedClass<typeof X> instead of as any / as unknown as Console casts and bare jest.fn().mockReturnValue().
  • Converted several small existing .js test files to TypeScript (get_mock_name, dependencyExtractor, fast_path, getPlatformExtension, normalizePathSep, isWatchmanInstalled).

Test plan

Green CI. New unit tests added for each extracted module, plus regression tests for the collision fix and watcher cleanup fix.

SimenB added 8 commits May 15, 2026 01:49
Move copy/copyMap/createEmptyMap to lib/util.ts and extract the ignore
matcher logic into lib/buildIgnoreMatcher.ts so ChangeQueue can reuse
it without importing from index.ts. HasteMap._ignore now delegates to a
stored matcher that is rebuilt when watch mode flips retainAllFiles.
Cache read/persist logic moves to lib/CacheManager.ts. HasteMap.read()
and _persist() now delegate to it; setupCachePath creates the instance.
The v8 serialize/readFileSync/writeFileSync imports are removed from
index.ts.
Worker lifecycle moves to lib/WorkerPool.ts. HasteMap._getWorker and
_cleanup now delegate to it; the worker and jest-worker imports are
removed from index.ts.
The watchman→node fallback logic moves to crawlers/index.ts. HasteMap._crawl
builds the CrawlerOptions and delegates; the individual crawler imports and
CrawlerOptions type import are removed from index.ts.
Watcher backend selection, per-root setup, and lifecycle tracking move
to watchers/index.ts (WatcherDriver + shouldUseWatchman). HasteMap drops
FSEventsWatcher/NodeWatcher/WatchmanWatcher imports, the local Watcher
type, _watchers array, _shouldUseWatchman, and isWatchmanInstalledPromise.
Move the change-event accumulation state (promise chain, events queue,
mustCopy flag, emit interval, onChange/emitChange logic) out of the
_watch closure into a dedicated ChangeQueue class. Replace the O(n)
eventsQueue.some() linear scan with a Set<string> keyed on
`${type}:${filePath}:${mtime}` for O(1) dedup.
Move _processFile, _buildHasteMap, and DuplicateError out of the
HasteMap god class into FileProcessor. HasteMap keeps thin delegators
so call sites are unchanged. DuplicateError moves to FileProcessor to
avoid a circular import; index.ts re-exports it to preserve the public
API surface.
…n up comments

Rebuild _fileProcessor after _watch() mutates retainAllFiles/throwOnModuleCollision
so node_modules files get sha1-only processing (not full worker calls) in watch mode.
Remove two redundant "what" callback comments from FileProcessor.ts.
Remove misleading jest.resetModules() from shouldUseWatchman test.
Copilot AI review requested due to automatic review settings May 14, 2026 23:51
@netlify
Copy link
Copy Markdown

netlify Bot commented May 14, 2026

Deploy Preview for jestjs ready!

Name Link
🔨 Latest commit 3cc71b1
🔍 Latest deploy log https://app.netlify.com/projects/jestjs/deploys/6a06cd482a01cd0008cce105
😎 Deploy Preview https://deploy-preview-16180--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@SimenB SimenB changed the title Split haste map chore(haste-map): refactor massive class into multiple files May 14, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 14, 2026

Open in StackBlitz

babel-jest

npm i https://pkg.pr.new/babel-jest@16180

babel-plugin-jest-hoist

npm i https://pkg.pr.new/babel-plugin-jest-hoist@16180

babel-preset-jest

npm i https://pkg.pr.new/babel-preset-jest@16180

create-jest

npm i https://pkg.pr.new/create-jest@16180

@jest/diff-sequences

npm i https://pkg.pr.new/@jest/diff-sequences@16180

expect

npm i https://pkg.pr.new/expect@16180

@jest/expect-utils

npm i https://pkg.pr.new/@jest/expect-utils@16180

jest

npm i https://pkg.pr.new/jest@16180

jest-changed-files

npm i https://pkg.pr.new/jest-changed-files@16180

jest-circus

npm i https://pkg.pr.new/jest-circus@16180

jest-cli

npm i https://pkg.pr.new/jest-cli@16180

jest-config

npm i https://pkg.pr.new/jest-config@16180

@jest/console

npm i https://pkg.pr.new/@jest/console@16180

@jest/core

npm i https://pkg.pr.new/@jest/core@16180

@jest/create-cache-key-function

npm i https://pkg.pr.new/@jest/create-cache-key-function@16180

jest-diff

npm i https://pkg.pr.new/jest-diff@16180

jest-docblock

npm i https://pkg.pr.new/jest-docblock@16180

jest-each

npm i https://pkg.pr.new/jest-each@16180

@jest/environment

npm i https://pkg.pr.new/@jest/environment@16180

jest-environment-jsdom

npm i https://pkg.pr.new/jest-environment-jsdom@16180

@jest/environment-jsdom-abstract

npm i https://pkg.pr.new/@jest/environment-jsdom-abstract@16180

jest-environment-node

npm i https://pkg.pr.new/jest-environment-node@16180

@jest/expect

npm i https://pkg.pr.new/@jest/expect@16180

@jest/fake-timers

npm i https://pkg.pr.new/@jest/fake-timers@16180

@jest/get-type

npm i https://pkg.pr.new/@jest/get-type@16180

@jest/globals

npm i https://pkg.pr.new/@jest/globals@16180

jest-haste-map

npm i https://pkg.pr.new/jest-haste-map@16180

jest-jasmine2

npm i https://pkg.pr.new/jest-jasmine2@16180

jest-leak-detector

npm i https://pkg.pr.new/jest-leak-detector@16180

jest-matcher-utils

npm i https://pkg.pr.new/jest-matcher-utils@16180

jest-message-util

npm i https://pkg.pr.new/jest-message-util@16180

jest-mock

npm i https://pkg.pr.new/jest-mock@16180

@jest/pattern

npm i https://pkg.pr.new/@jest/pattern@16180

jest-phabricator

npm i https://pkg.pr.new/jest-phabricator@16180

jest-regex-util

npm i https://pkg.pr.new/jest-regex-util@16180

@jest/reporters

npm i https://pkg.pr.new/@jest/reporters@16180

jest-resolve

npm i https://pkg.pr.new/jest-resolve@16180

jest-resolve-dependencies

npm i https://pkg.pr.new/jest-resolve-dependencies@16180

jest-runner

npm i https://pkg.pr.new/jest-runner@16180

jest-runtime

npm i https://pkg.pr.new/jest-runtime@16180

@jest/schemas

npm i https://pkg.pr.new/@jest/schemas@16180

jest-snapshot

npm i https://pkg.pr.new/jest-snapshot@16180

@jest/snapshot-utils

npm i https://pkg.pr.new/@jest/snapshot-utils@16180

@jest/source-map

npm i https://pkg.pr.new/@jest/source-map@16180

@jest/test-result

npm i https://pkg.pr.new/@jest/test-result@16180

@jest/test-sequencer

npm i https://pkg.pr.new/@jest/test-sequencer@16180

@jest/transform

npm i https://pkg.pr.new/@jest/transform@16180

@jest/types

npm i https://pkg.pr.new/@jest/types@16180

jest-util

npm i https://pkg.pr.new/jest-util@16180

jest-validate

npm i https://pkg.pr.new/jest-validate@16180

jest-watcher

npm i https://pkg.pr.new/jest-watcher@16180

jest-worker

npm i https://pkg.pr.new/jest-worker@16180

pretty-format

npm i https://pkg.pr.new/pretty-format@16180

commit: 1fbe176

Remove unused WatcherDriver.watcherCount getter.
Remove "what" comment from ChangeQueue._pendingEventKeys.
Add missing FileProcessor test for getSha1 path (retainAllFiles + node_modules).
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors jest-haste-map by extracting cache, crawl, watch, worker, file-processing, and utility responsibilities out of the main HasteMap class while preserving the public API.

Changes:

  • Splits HasteMap internals into focused modules under lib/, crawlers/, and watchers/.
  • Adds unit tests for the extracted helpers and drivers.
  • Replaces watch event duplicate detection with a Set-based deduplication path.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/jest-haste-map/src/index.ts Rewires HasteMap orchestration to use extracted modules.
packages/jest-haste-map/src/crawlers/index.ts Adds crawler selection and watchman-to-node fallback helper.
packages/jest-haste-map/src/crawlers/__tests__/index.test.ts Tests crawler selection and fallback behavior.
packages/jest-haste-map/src/lib/CacheManager.ts Extracts haste map cache read/write logic.
packages/jest-haste-map/src/lib/FileProcessor.ts Extracts file metadata processing and haste map building.
packages/jest-haste-map/src/lib/WorkerPool.ts Extracts worker lifecycle management.
packages/jest-haste-map/src/lib/buildIgnoreMatcher.ts Extracts ignore predicate construction.
packages/jest-haste-map/src/lib/util.ts Adds shared copy and empty-map helpers.
packages/jest-haste-map/src/lib/__tests__/CacheManager.test.ts Tests cache manager behavior.
packages/jest-haste-map/src/lib/__tests__/FileProcessor.test.ts Tests extracted file processing behavior.
packages/jest-haste-map/src/lib/__tests__/WorkerPool.test.ts Tests worker pool behavior.
packages/jest-haste-map/src/lib/__tests__/buildIgnoreMatcher.test.ts Tests ignore matcher behavior.
packages/jest-haste-map/src/lib/__tests__/util.test.ts Tests shared utility helpers.
packages/jest-haste-map/src/watchers/ChangeQueue.ts Extracts watch change queue processing and Set-based deduplication.
packages/jest-haste-map/src/watchers/index.ts Adds watch backend selection and watcher driver.
packages/jest-haste-map/src/watchers/__tests__/ChangeQueue.test.ts Tests change queue behavior.
packages/jest-haste-map/src/watchers/__tests__/index.test.ts Tests watcher driver and watchman selection behavior.
packages/jest-haste-map/src/watchers/__tests__/tsconfig.json Adds TypeScript test project config for watcher tests.
Comments suppressed due to low confidence (2)

packages/jest-haste-map/src/crawlers/tests/index.test.ts:74

  • This assertion also invokes the watchman-fallback path with the real console, so the expected failure case will emit a warning during a passing test. Use a mocked console for these fallback tests to avoid noisy CI output.
    await expect(crawl(crawlerOptions, true, console)).rejects.toThrow(

packages/jest-haste-map/src/crawlers/tests/index.test.ts:83

  • This fallback test passes the real console, which causes the warning emitted before the retry failure to appear in normal test output. A mocked console keeps the test quiet and makes the warning behavior explicit.
    await expect(crawl(crawlerOptions, true, console)).rejects.toThrow(

Comment thread packages/jest-haste-map/src/watchers/__tests__/ChangeQueue.test.ts Outdated
Comment thread packages/jest-haste-map/src/watchers/ChangeQueue.ts
Comment thread packages/jest-haste-map/src/lib/__tests__/FileProcessor.test.ts
Comment thread packages/jest-haste-map/src/crawlers/__tests__/index.test.ts Outdated
SimenB added 2 commits May 15, 2026 02:05
Fix import order in ChangeQueue.test.ts (parent paths before local).
Pass mocked console to crawl() and FileProcessor in tests that trigger
warnings/errors, keeping test output clean.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 11 comments.

Comments suppressed due to low confidence (10)

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:71

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:95

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:120

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:147

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:165

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:185

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:209

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:233

  • WorkerPool's constructor requires a workerPath option, so this mocked construction is not type-correct and will fail the test typecheck. Include the required option or use a typed mock object instead.
      const pool = new MockWorkerPool({maxWorkers: 1});

packages/jest-haste-map/src/watchers/tests/index.test.ts:89

  • Untyped jest.fn() defaults to an unknown return type, which makes mockResolvedValue(undefined) invalid under the repository's strict mock typings. Give these close mocks explicit () => Promise<void> types or implementations so this test compiles.
    const closeA = jest.fn().mockResolvedValue(undefined);
    const closeB = jest.fn().mockResolvedValue(undefined);

packages/jest-haste-map/src/lib/tests/FileProcessor.test.ts:93

  • Untyped jest.fn() defaults to an unknown return type, so mockRejectedValue(enoent) is invalid with these mock typings unless the mock is typed as returning a Promise. Add an explicit async function type or implementation before configuring the rejection.
        worker: jest.fn().mockRejectedValue(enoent),

Comment thread packages/jest-haste-map/src/lib/FileProcessor.ts Outdated
Comment thread packages/jest-haste-map/src/lib/__tests__/FileProcessor.test.ts Outdated
Comment thread packages/jest-haste-map/src/lib/__tests__/FileProcessor.test.ts Outdated
Comment thread packages/jest-haste-map/src/watchers/__tests__/ChangeQueue.test.ts Outdated
Comment thread packages/jest-haste-map/src/lib/__tests__/FileProcessor.test.ts Outdated
Comment thread packages/jest-haste-map/src/lib/__tests__/FileProcessor.test.ts Outdated
Comment thread packages/jest-haste-map/src/watchers/__tests__/ChangeQueue.test.ts Outdated
Comment thread packages/jest-haste-map/src/watchers/index.ts Outdated
Comment thread packages/jest-haste-map/src/index.ts Outdated
Comment thread packages/jest-haste-map/src/watchers/index.ts Outdated
SimenB added 6 commits May 15, 2026 08:15
Add @types/node to the three new __tests__ tsconfigs, export the
Callbacks type from ChangeQueue.ts, and rewrite the new test files to
use typed jest mock APIs (jest.fn<T>(), jest.mocked(), jest.MockedObject)
instead of bare jest.fn() / as-any casts.
When a haste module collision occurred, the code deleted the colliding
platform entry from the module map but then checked
`Object.keys(moduleMap).length === 1` to decide whether to remove the
whole map entry. That check was off-by-one: after deleting one platform,
a count of 1 means there is still one valid sibling — only a count of 0
should trigger full deletion.
Three related fixes for watch-mode startup failures:

- _createWatcher: name the ready handler so it can be removed on
  timeout; call watcher.close() after rejecting to avoid leaking the
  backend handle (B2)
- WatcherDriver.start: use Promise.allSettled so all roots are attempted
  before failing; close successfully-started watchers before throwing an
  AggregateError when any root fails (B3)
- HasteMap._watch: stop the ChangeQueue interval if WatcherDriver.start
  rejects, so the interval doesn't leak when watch-mode startup fails (B4)
Converts get_mock_name, dependencyExtractor, fast_path, getPlatformExtension,
isWatchmanInstalled, and normalizePathSep tests from .js to .ts. Adds
@types/node to all four __tests__ tsconfig.json files (extends-first per
convention). Also makes DependencyExtractor.extract's filePath and
defaultExtract parameters optional since the built-in extractor only
uses code.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

packages/jest-haste-map/src/lib/tests/normalizePathSep.test.ts:26

  • This mock targets path, but normalizePathSep imports node:path, so the mock is not applied. On non-Windows hosts the Windows test will keep using the real POSIX separator and fail; mock the same node:path specifier that the module under test imports.
    packages/jest-haste-map/src/lib/tests/normalizePathSep.test.ts:14
  • This mock uses path, but the module under test imports node:path, so this does not replace the dependency. On Windows this test will still load the real Windows path module and the POSIX expectation will fail; mock node:path instead.

This issue also appears on line 23 of the same file.

Comment thread CHANGELOG.md Outdated
Comment on lines 5 to 7
- `[jest-haste-map]` Fix module map entry being deleted when only one platform collides and sibling platforms remain ([#16180](https://github.com/jestjs/jest/pull/16180))
- `[expect, jest-message-util, jest-pattern, jest-regex-util, jest-util]` Revert `node:` protocol imports to restore webpack/browser-bundle compatibility ([#16167](https://github.com/jestjs/jest/pull/16167))
- `[@jest-environment/jsdom-abstract]` Make `@types/jsdom` a peer dependency ([#16166](https://github.com/jestjs/jest/pull/16166))
@SimenB SimenB merged commit 67097ed into jestjs:main May 15, 2026
8 of 9 checks passed
@SimenB SimenB deleted the split-haste-map branch May 15, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants