Summary
During the full project rewrite path of vp migrate, the same package source tree is traversed synchronously multiple times to collect Vitest browser/provider signals.
Under the following full-miss conditions:
- the source contains no relevant Vitest browser/provider references;
- no tsconfig contains Vitest types that must be retained;
- package.json contains no webdriverio/provider dependency that causes an early return;
the Vitest signal-detection phase reads every eligible TS/JS file through Node.js fs.readFileSync:
- 5 times for a standalone project;
- 9 times for each workspace package in a monorepo.
These counts cover only the JavaScript-side Vitest signal scans. They do not represent all file I/O performed during migration. For example, the later Rust/NAPI import rewriter also traverses and reads source files, but those reads are not visible to an fs.readFileSync probe.
Local reproduction
I created temporary test projects containing many small .ts files with no Vitest browser/provider references. Their package.json files also contained no dependencies that would cause the checks to return early.
I then monitored fs.readFileSync calls for those fixture source files and invoked the production migration entry points directly.
| Call path |
Source files |
Source reads through fs.readFileSync |
Duration |
rewriteStandaloneProject |
5,000 |
25,000 |
486 ms |
rewriteMonorepo (2 × 2,500 files) |
5,000 |
45,000 |
734 ms |
Environment: macOS, Node.js 25.9.0, with the filesystem cache pre-warmed.
The per-file read counts are deterministic: exactly 5 reads per file for the standalone project and exactly 9 reads per file for each monorepo workspace package. Every detection result was false, so each check took its complete miss path.
The timings are included only to illustrate the approximate impact and will vary between machines.
Observed call paths
sourceTreeMatches recursively traverses directories with readdirSync and reads every TS/JS file with an eligible extension using readFileSync. It returns early only after finding a match, so a complete miss is the worst-case path.
Standalone
Within a single rewriteStandaloneProject call:
collectProviderSourceModes scans once for each of the two current opt-in providers: 2 passes;
usesVitestBrowserMode performs another scan: 1 pass;
sourceTreeReferencesRetainedVitestModule scans the source again: 1 pass;
- when no webdriverio dependency causes a short circuit,
usesWebdriverioProvider performs another scan: 1 pass.
This results in 5 reads per source file.
providerSourceModes already contains the webdriverio source-detection result, but the standalone path subsequently calls usesWebdriverioProvider anyway.
In addition, sourceTreeReferencesRetainedVitestModule first performs a separate recursive directory traversal to discover tsconfig files and reads any matching tsconfig files it finds. As a result, the package directory tree is traversed 6 times on a complete standalone miss, with 5 of those passes reading TS/JS source files.
Monorepo workspace package
rewriteMonorepo first computes workspace-level signals. For every workspace package on the complete miss path:
workspaceUsesWebdriverio: 1 source scan;
workspaceUsesVitestDirectly:
- retained Vitest module: 1 scan;
- browser mode: 1 scan;
collectInjectedProviderNames:
- two provider scans: 2 scans.
The workspace phase therefore reads each source file 5 times.
The same package is then processed by rewriteMonorepoProject, which recomputes:
- browser mode: 1 scan;
- retained Vitest module: 1 scan;
- source modes for two providers: 2 scans.
The package phase adds another 4 reads, for a total of 9 reads per source file in each workspace package.
The monorepo root package is not subsequently passed through rewriteMonorepoProject, so source files belonging directly to the root package are generally read 5 times rather than 9.
The two retained-module checks also each add a separate directory-tree traversal for locating tsconfig files. A workspace package therefore undergoes 11 related full directory-tree traversals on the complete miss path, 9 of which read TS/JS source files.
Reusing scan results
In the reproduction projects above, no source files were modified between these scans in a way that could affect the results.
In the general monorepo path, however, rewriteMonorepoProject performs configuration merging before the package-level scans. That step may create or modify vite.config.ts. Reusing results between the workspace and package phases would therefore require confirming that these configuration changes cannot affect the relevant signals, or retaining the two phases while consolidating the scans performed within each phase.
Regardless of whether results can be cached across phases, each individual phase currently performs separate full traversals for several closely related boolean signals.
Impact
The complete miss path remains linear in the number of files, but it has a fixed synchronous I/O amplification:
- standalone package: 5 reads per eligible source file;
- monorepo workspace package: 9 reads per eligible source file.
These directory traversals and file reads are synchronous and directly block vp migrate. The cost becomes noticeable in projects containing thousands or tens of thousands of source files.
Ideally, browser mode, retained Vitest module references, and provider signals could be collected during a single source-tree traversal, with the resulting scan data shared by callers within the same migration phase.
Summary
During the full project rewrite path of
vp migrate, the same package source tree is traversed synchronously multiple times to collect Vitest browser/provider signals.Under the following full-miss conditions:
the Vitest signal-detection phase reads every eligible TS/JS file through Node.js
fs.readFileSync:These counts cover only the JavaScript-side Vitest signal scans. They do not represent all file I/O performed during migration. For example, the later Rust/NAPI import rewriter also traverses and reads source files, but those reads are not visible to an
fs.readFileSyncprobe.Local reproduction
I created temporary test projects containing many small
.tsfiles with no Vitest browser/provider references. Their package.json files also contained no dependencies that would cause the checks to return early.I then monitored
fs.readFileSynccalls for those fixture source files and invoked the production migration entry points directly.fs.readFileSyncrewriteStandaloneProjectrewriteMonorepo(2 × 2,500 files)Environment: macOS, Node.js 25.9.0, with the filesystem cache pre-warmed.
The per-file read counts are deterministic: exactly 5 reads per file for the standalone project and exactly 9 reads per file for each monorepo workspace package. Every detection result was
false, so each check took its complete miss path.The timings are included only to illustrate the approximate impact and will vary between machines.
Observed call paths
sourceTreeMatchesrecursively traverses directories withreaddirSyncand reads every TS/JS file with an eligible extension usingreadFileSync. It returns early only after finding a match, so a complete miss is the worst-case path.Standalone
Within a single
rewriteStandaloneProjectcall:collectProviderSourceModesscans once for each of the two current opt-in providers: 2 passes;usesVitestBrowserModeperforms another scan: 1 pass;sourceTreeReferencesRetainedVitestModulescans the source again: 1 pass;usesWebdriverioProviderperforms another scan: 1 pass.This results in 5 reads per source file.
providerSourceModesalready contains the webdriverio source-detection result, but the standalone path subsequently callsusesWebdriverioProvideranyway.In addition,
sourceTreeReferencesRetainedVitestModulefirst performs a separate recursive directory traversal to discover tsconfig files and reads any matching tsconfig files it finds. As a result, the package directory tree is traversed 6 times on a complete standalone miss, with 5 of those passes reading TS/JS source files.Monorepo workspace package
rewriteMonorepofirst computes workspace-level signals. For every workspace package on the complete miss path:workspaceUsesWebdriverio: 1 source scan;workspaceUsesVitestDirectly:collectInjectedProviderNames:The workspace phase therefore reads each source file 5 times.
The same package is then processed by
rewriteMonorepoProject, which recomputes:The package phase adds another 4 reads, for a total of 9 reads per source file in each workspace package.
The monorepo root package is not subsequently passed through
rewriteMonorepoProject, so source files belonging directly to the root package are generally read 5 times rather than 9.The two retained-module checks also each add a separate directory-tree traversal for locating tsconfig files. A workspace package therefore undergoes 11 related full directory-tree traversals on the complete miss path, 9 of which read TS/JS source files.
Reusing scan results
In the reproduction projects above, no source files were modified between these scans in a way that could affect the results.
In the general monorepo path, however,
rewriteMonorepoProjectperforms configuration merging before the package-level scans. That step may create or modifyvite.config.ts. Reusing results between the workspace and package phases would therefore require confirming that these configuration changes cannot affect the relevant signals, or retaining the two phases while consolidating the scans performed within each phase.Regardless of whether results can be cached across phases, each individual phase currently performs separate full traversals for several closely related boolean signals.
Impact
The complete miss path remains linear in the number of files, but it has a fixed synchronous I/O amplification:
These directory traversals and file reads are synchronous and directly block
vp migrate. The cost becomes noticeable in projects containing thousands or tens of thousands of source files.Ideally, browser mode, retained Vitest module references, and provider signals could be collected during a single source-tree traversal, with the resulting scan data shared by callers within the same migration phase.