Skip to content

fix(@angular/build): key the Vitest fsModuleCache on the served build output - #34444

Open
jv3n wants to merge 1 commit into
angular:mainfrom
jv3n:fix/unit-test-vitest-fs-module-cache
Open

jv3n wants to merge 1 commit into
angular:mainfrom
jv3n:fix/unit-test-vitest-fs-module-cache

Conversation

@jv3n

@jv3n jv3n commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

PR Type

  • Bugfix

What is the current behavior?

Issue Number: #34443

The unit-test builder forces fsModuleCache: false (f0a74e8), because Vitest keys its module cache on the file found on disk while a test entry point is served from its bundle: changing a file imported by a spec did not change the key, and the cache served a stale bundle. As a result the cache can never be used, even when enabled in the runnerConfig.

What is the new behavior?

The angular:test-in-memory-provider plugin defines a cache key generator in configureVitest: for a module it serves, the key includes the content hash of the build output file and of its source map, so it changes whenever the served code changes. Modules served from a disk file bail out of the cache; modules the plugin does not serve keep Vitest's default key. fsModuleCache is no longer forced off, and the output path computation is shared between load() and the key generator.

The generator is registered through defineCacheKeyGenerator on Vitest 5, and through experimental_defineCacheKeyGenerator on Vitest 4.1, which only has that name. Vitest 4.0 has no file system module cache, so no generator is registered there and nothing changes.

With deterministic test bundles (#34337), an unchanged build gives the same keys, so the cache is reused across runs.

Tests:

  • the stale-bundle test from 5e0fd4f still checks that changing an imported file is picked up, now with the cache directory created;
  • a new test checks that a second run without changes writes no new cache entry.

How Vitest builds the key

From Vitest 5.0.5, packages/vitest/src/node/cache/fsModuleCache.ts (generateCachePath, trimmed):

let hashString = ''

for (const generator of this.fsCacheKeyGenerators.get(vitestConfig) || []) {
  const result = generator({ environment, id, sourceCode: fileContent })
  if (typeof result === 'string') {
    hashString += result
  }
  if (result === false) {
    this.saveMemoryCache(environment, id, null)
    return null
  }
}
// ...then hashed together with the environment config and `fileContent`

fileContent is the file read from disk for id. For a spec, that is the .spec.ts source, while the module actually served is its bundle, which also contains the application code the spec imports. That is the stale-bundle case behind f0a74e8. The generator's return value maps to:

Returned Effect Used for
a string appended to the key a module served from memory: <output hash>:<source map hash>
false the module is never cached a module served from a disk file (origin: 'disk' has no hash)
undefined Vitest's default key a module the plugin does not serve

Example

A project opting in through its runnerConfig:

// vitest.config.mts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    fsModuleCache: true,
    fsModuleCachePath: './.vitest-cache',
  },
});
// src/app/value.ts
export const VALUE = 'initial';

// src/app/app.component.spec.ts
import { expect, test } from 'vitest';
import { VALUE } from './value';

test('uses the value from the imported module', () => {
  expect(VALUE).toBe('initial');
});
  • Changing value.ts without touching the spec: the spec's bundle hash changes, so does its key, and the new code runs. Before f0a74e8 this served the old bundle.
  • Running again with nothing changed: same bundles, same hashes, same keys, so the entries from the previous run are read back and no new entry is written.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Measured on an application with 255 spec files (numbers in #34443): Vitest duration from ~21.4 s to ~18.4–20 s with a warm cache. Changing a file imported by a spec, without touching the spec, makes that spec fail as expected; reverting it reuses the original cache entry. Watch mode is unaffected: invalidateFile already drops the memoized cache key of the invalidated modules.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request enables and configures Vitest's file system module cache (fsModuleCache) for Angular unit tests. It removes the hardcoded fsModuleCache: false setting and introduces a custom cache key generator via context.defineCacheKeyGenerator to ensure that in-memory built artifacts and test entry points are cached correctly based on their output hashes rather than their disk paths. Additionally, the loading logic has been refactored to use a new getOutputPath helper, and corresponding integration tests have been updated and added to verify that the cache is successfully created and reused across unchanged runs. There are no review comments, so I have no feedback to provide.

@jv3n
jv3n force-pushed the fix/unit-test-vitest-fs-module-cache branch from b2d7f32 to 9f1aabc Compare October 20, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unit-test builder (Vitest): key fsModuleCache on the served build output instead of forcing it off

1 participant