diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c7faac2588..d620b35ffb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - `[jest-resolve]` Pass custom cached `realpath` function to `resolve` ([#9873](https://github.com/facebook/jest/pull/9873)) - `[jest-runtime]` Add `teardown` method to clear any caches when tests complete ([#9906](https://github.com/facebook/jest/pull/9906)) - `[jest-runtime]` Do not pass files required internally through transformation when loading them ([#9900](https://github.com/facebook/jest/pull/9900)) +- `[jest-runtime]` Use `Map`s instead of object literals as cache holders ([#9901](https://github.com/facebook/jest/pull/9901)) ## 25.4.0 diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index 00faa0e5f535..19937efafd7b 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -28,6 +28,7 @@ type FindNodeModuleConfig = { throwIfNotFound?: boolean; }; +// TODO: replace with a Map in Jest 26 type BooleanObject = Record; namespace Resolver { diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 737258df6c92..69e1c22de49e 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -83,10 +83,17 @@ type InitialModule = Partial & type ModuleRegistry = Map; type ResolveOptions = Parameters[1]; -type BooleanObject = Record; -type CacheFS = {[path: string]: string}; - -type RequireCache = {[key: string]: Module}; +type StringMap = Map; +type BooleanMap = Map; + +const fromEntries: typeof Object.fromEntries = + Object.fromEntries ?? + function fromEntries(iterable: Iterable<[string, T]>) { + return [...iterable].reduce>((obj, [key, val]) => { + obj[key] = val; + return obj; + }, {}); + }; namespace Runtime { export type Context = JestContext; @@ -122,18 +129,19 @@ const runtimeSupportsVmModules = typeof SyntheticModule === 'function'; /* eslint-disable-next-line no-redeclare */ class Runtime { - private _cacheFS: CacheFS; + private _cacheFS: StringMap; private _config: Config.ProjectConfig; private _coverageOptions: ShouldInstrumentOptions; private _currentlyExecutingModulePath: string; private _environment: JestEnvironment; - private _explicitShouldMock: BooleanObject; + private _explicitShouldMock: BooleanMap; private _internalModuleRegistry: ModuleRegistry; private _isCurrentlyExecutingManualMock: string | null; - private _mockFactories: Record unknown>; - private _mockMetaDataCache: { - [key: string]: jestMock.MockFunctionMetadata>; - }; + private _mockFactories: Map unknown>; + private _mockMetaDataCache: Map< + string, + jestMock.MockFunctionMetadata> + >; private _mockRegistry: Map; private _isolatedMockRegistry: Map | null; private _moduleMocker: typeof jestMock; @@ -142,16 +150,16 @@ class Runtime { private _esmoduleRegistry: Map>; private _resolver: Resolver; private _shouldAutoMock: boolean; - private _shouldMockModuleCache: BooleanObject; - private _shouldUnmockTransitiveDependenciesCache: BooleanObject; - private _sourceMapRegistry: SourceMapRegistry; + private _shouldMockModuleCache: BooleanMap; + private _shouldUnmockTransitiveDependenciesCache: BooleanMap; + private _sourceMapRegistry: StringMap; private _scriptTransformer: ScriptTransformer; private _fileTransforms: Map; private _v8CoverageInstrumenter: CoverageInstrumenter | undefined; private _v8CoverageResult: V8Coverage | undefined; - private _transitiveShouldMock: BooleanObject; + private _transitiveShouldMock: BooleanMap; private _unmockList: RegExp | undefined; - private _virtualMocks: BooleanObject; + private _virtualMocks: BooleanMap; private _moduleImplementation?: typeof nativeModule.Module; private jestObjectCaches: Map; private _hasWarnedAboutRequireCacheModification = false; @@ -160,10 +168,10 @@ class Runtime { config: Config.ProjectConfig, environment: JestEnvironment, resolver: Resolver, - cacheFS?: CacheFS, + cacheFS: Record = {}, coverageOptions?: ShouldInstrumentOptions, ) { - this._cacheFS = cacheFS || Object.create(null); + this._cacheFS = new Map(Object.entries(cacheFS)); this._config = config; this._coverageOptions = coverageOptions || { changedFiles: undefined, @@ -175,10 +183,10 @@ class Runtime { }; this._currentlyExecutingModulePath = ''; this._environment = environment; - this._explicitShouldMock = Object.create(null); + this._explicitShouldMock = new Map(); this._internalModuleRegistry = new Map(); this._isCurrentlyExecutingManualMock = null; - this._mockFactories = Object.create(null); + this._mockFactories = new Map(); this._mockRegistry = new Map(); // during setup, this cannot be null (and it's fine to explode if it is) this._moduleMocker = this._environment.moduleMocker!; @@ -189,15 +197,15 @@ class Runtime { this._resolver = resolver; this._scriptTransformer = new ScriptTransformer(config); this._shouldAutoMock = config.automock; - this._sourceMapRegistry = Object.create(null); + this._sourceMapRegistry = new Map(); this._fileTransforms = new Map(); - this._virtualMocks = Object.create(null); + this._virtualMocks = new Map(); this.jestObjectCaches = new Map(); - this._mockMetaDataCache = Object.create(null); - this._shouldMockModuleCache = Object.create(null); - this._shouldUnmockTransitiveDependenciesCache = Object.create(null); - this._transitiveShouldMock = Object.create(null); + this._mockMetaDataCache = new Map(); + this._shouldMockModuleCache = new Map(); + this._shouldUnmockTransitiveDependenciesCache = new Map(); + this._transitiveShouldMock = new Map(); this._unmockList = unmockRegExpCache.get(config); if (!this._unmockList && config.unmockedModulePathPatterns) { @@ -208,13 +216,11 @@ class Runtime { } if (config.automock) { + const virtualMocks = fromEntries(this._virtualMocks); config.setupFiles.forEach(filePath => { if (filePath && filePath.includes(NODE_MODULES)) { - const moduleID = this._resolver.getModuleID( - this._virtualMocks, - filePath, - ); - this._transitiveShouldMock[moduleID] = false; + const moduleID = this._resolver.getModuleID(virtualMocks, filePath); + this._transitiveShouldMock.set(moduleID, false); } }); } @@ -459,7 +465,7 @@ class Runtime { isRequireActual?: boolean | null, ): T { const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); @@ -476,7 +482,7 @@ class Runtime { !moduleResource && manualMock && manualMock !== this._isCurrentlyExecutingManualMock && - this._explicitShouldMock[moduleID] !== false + this._explicitShouldMock.get(moduleID) !== false ) { modulePath = manualMock; } @@ -547,7 +553,7 @@ class Runtime { requireMock(from: Config.Path, moduleName: string): T { const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); @@ -563,8 +569,9 @@ class Runtime { const mockRegistry = this._isolatedMockRegistry || this._mockRegistry; - if (moduleID in this._mockFactories) { - const module = this._mockFactories[moduleID](); + if (this._mockFactories.has(moduleID)) { + // has check above makes this ok + const module = this._mockFactories.get(moduleID)!(); mockRegistry.set(moduleID, module); return module as T; } @@ -804,7 +811,7 @@ class Runtime { } getSourceMaps(): SourceMapRegistry { - return this._sourceMapRegistry; + return fromEntries(this._sourceMapRegistry); } setMock( @@ -813,17 +820,18 @@ class Runtime { mockFactory: () => unknown, options?: {virtual?: boolean}, ): void { - if (options && options.virtual) { + if (options?.virtual) { const mockPath = this._resolver.getModulePath(from, moduleName); - this._virtualMocks[mockPath] = true; + + this._virtualMocks.set(mockPath, true); } const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); - this._explicitShouldMock[moduleID] = true; - this._mockFactories[moduleID] = mockFactory; + this._explicitShouldMock.set(moduleID, true); + this._mockFactories.set(moduleID, mockFactory); } restoreAllMocks(): void { @@ -844,20 +852,23 @@ class Runtime { this.resetModules(); this._internalModuleRegistry.clear(); - this._mockFactories = {}; - this._mockMetaDataCache = {}; - this._shouldMockModuleCache = {}; - this._shouldUnmockTransitiveDependenciesCache = {}; - this._transitiveShouldMock = {}; - this._virtualMocks = {}; - this._cacheFS = {}; - - this._sourceMapRegistry = {}; + this._mockFactories.clear(); + this._mockMetaDataCache.clear(); + this._shouldMockModuleCache.clear(); + this._shouldUnmockTransitiveDependenciesCache.clear(); + this._explicitShouldMock.clear(); + this._transitiveShouldMock.clear(); + this._virtualMocks.clear(); + this._cacheFS.clear(); + this._unmockList = undefined; + + this._sourceMapRegistry.clear(); this._fileTransforms.clear(); this.jestObjectCaches.clear(); this._v8CoverageResult = []; + this._v8CoverageInstrumenter = undefined; this._moduleImplementation = undefined; } @@ -1074,7 +1085,7 @@ class Runtime { this._fileTransforms.set(filename, transformedFile); if (transformedFile.sourceMapPath) { - this._sourceMapRegistry[filename] = transformedFile.sourceMapPath; + this._sourceMapRegistry.set(filename, transformedFile.sourceMapPath); } return transformedFile.code; } @@ -1198,12 +1209,14 @@ class Runtime { const modulePath = this._resolver.resolveStubModuleName(from, moduleName) || this._resolveModule(from, moduleName); - if (!(modulePath in this._mockMetaDataCache)) { + if (!this._mockMetaDataCache.has(modulePath)) { // This allows us to handle circular dependencies while generating an // automock - this._mockMetaDataCache[modulePath] = - this._moduleMocker.getMetadata({}) || {}; + this._mockMetaDataCache.set( + modulePath, + this._moduleMocker.getMetadata({}) || {}, + ); // In order to avoid it being possible for automocking to potentially // cause side-effects within the module environment, we need to execute @@ -1227,36 +1240,39 @@ class Runtime { `See: https://jestjs.io/docs/manual-mocks.html#content`, ); } - this._mockMetaDataCache[modulePath] = mockMetadata; + this._mockMetaDataCache.set(modulePath, mockMetadata); } return this._moduleMocker.generateFromMetadata( - this._mockMetaDataCache[modulePath], + // added above if missing + this._mockMetaDataCache.get(modulePath)!, ); } - private _shouldMock(from: Config.Path, moduleName: string) { + private _shouldMock(from: Config.Path, moduleName: string): boolean { const explicitShouldMock = this._explicitShouldMock; const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); const key = from + path.delimiter + moduleID; - if (moduleID in explicitShouldMock) { - return explicitShouldMock[moduleID]; + if (explicitShouldMock.has(moduleID)) { + // guaranteed by `has` above + return explicitShouldMock.get(moduleID)!; } if ( !this._shouldAutoMock || this._resolver.isCoreModule(moduleName) || - this._shouldUnmockTransitiveDependenciesCache[key] + this._shouldUnmockTransitiveDependenciesCache.get(key) ) { return false; } - if (moduleID in this._shouldMockModuleCache) { - return this._shouldMockModuleCache[moduleID]; + if (this._shouldMockModuleCache.has(moduleID)) { + // guaranteed by `has` above + return this._shouldMockModuleCache.get(moduleID)!; } let modulePath; @@ -1265,35 +1281,35 @@ class Runtime { } catch (e) { const manualMock = this._resolver.getMockModule(from, moduleName); if (manualMock) { - this._shouldMockModuleCache[moduleID] = true; + this._shouldMockModuleCache.set(moduleID, true); return true; } throw e; } if (this._unmockList && this._unmockList.test(modulePath)) { - this._shouldMockModuleCache[moduleID] = false; + this._shouldMockModuleCache.set(moduleID, false); return false; } // transitive unmocking for package managers that store flat packages (npm3) const currentModuleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, ); if ( - this._transitiveShouldMock[currentModuleID] === false || + this._transitiveShouldMock.get(currentModuleID) === false || (from.includes(NODE_MODULES) && modulePath.includes(NODE_MODULES) && ((this._unmockList && this._unmockList.test(from)) || - explicitShouldMock[currentModuleID] === false)) + explicitShouldMock.get(currentModuleID) === false)) ) { - this._transitiveShouldMock[moduleID] = false; - this._shouldUnmockTransitiveDependenciesCache[key] = true; + this._transitiveShouldMock.set(moduleID, false); + this._shouldUnmockTransitiveDependenciesCache.set(key, true); return false; } - - return (this._shouldMockModuleCache[moduleID] = true); + this._shouldMockModuleCache.set(moduleID, true); + return true; } private _createRequireImplementation( @@ -1328,7 +1344,7 @@ class Runtime { } return true; }; - return new Proxy(Object.create(null), { + return new Proxy(Object.create(null), { defineProperty: notPermittedMethod, deleteProperty: notPermittedMethod, get: (_target, key) => @@ -1374,21 +1390,21 @@ class Runtime { }; const unmock = (moduleName: string) => { const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); - this._explicitShouldMock[moduleID] = false; + this._explicitShouldMock.set(moduleID, false); return jestObject; }; const deepUnmock = (moduleName: string) => { const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); - this._explicitShouldMock[moduleID] = false; - this._transitiveShouldMock[moduleID] = false; + this._explicitShouldMock.set(moduleID, false); + this._transitiveShouldMock.set(moduleID, false); return jestObject; }; const mock: Jest['mock'] = (moduleName, mockFactory, options) => { @@ -1397,11 +1413,11 @@ class Runtime { } const moduleID = this._resolver.getModuleID( - this._virtualMocks, + fromEntries(this._virtualMocks), from, moduleName, ); - this._explicitShouldMock[moduleID] = true; + this._explicitShouldMock.set(moduleID, true); return jestObject; }; const setMockFactory = ( @@ -1640,12 +1656,12 @@ class Runtime { } private readFile(filename: Config.Path): string { - let source = this._cacheFS[filename]; + let source = this._cacheFS.get(filename); if (!source) { source = fs.readFileSync(filename, 'utf8'); - this._cacheFS[filename] = source; + this._cacheFS.set(filename, source); } return source; diff --git a/tsconfig.json b/tsconfig.json index 2301b5e98b8b..a513c83fe2c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "target": "es2017", "module": "commonjs", - "lib": ["dom", "es2017"], + // Object.fromEntries + "lib": ["dom", "es2017", "es2019.object"], "declaration": true, "composite": true, "emitDeclarationOnly": true,