From 4fa3a0ba8da8ff13282235d1eb107457038a27ff Mon Sep 17 00:00:00 2001 From: David Zilburg Date: Thu, 20 May 2021 08:40:22 -0400 Subject: [PATCH] feat: custom haste (#11107) --- CHANGELOG.md | 1 + docs/Configuration.md | 2 + e2e/__tests__/customHaste.test.ts | 30 ++++ .../__tests__/hasteExample.test.js | 18 +++ .../__tests__/hasteExampleHelper.js | 15 ++ e2e/custom-haste-map/hasteMap.js | 139 ++++++++++++++++++ e2e/custom-haste-map/package.json | 7 + packages/jest-config/src/ValidConfig.ts | 1 + packages/jest-haste-map/src/ModuleMap.ts | 15 +- packages/jest-haste-map/src/index.ts | 27 +++- packages/jest-haste-map/src/types.ts | 45 ++++++ packages/jest-resolve/src/index.ts | 6 +- packages/jest-runner/src/testWorker.ts | 6 +- .../src/__tests__/Runtime-statics.test.js | 4 +- packages/jest-runtime/src/index.ts | 8 +- .../src/__tests__/test_sequencer.test.js | 2 + packages/jest-test-sequencer/src/index.ts | 3 +- .../jest-transform/src/ScriptTransformer.ts | 3 +- .../src/__tests__/ScriptTransformer.test.ts | 7 +- packages/jest-types/src/Config.ts | 2 + 20 files changed, 314 insertions(+), 27 deletions(-) create mode 100644 e2e/__tests__/customHaste.test.ts create mode 100644 e2e/custom-haste-map/__tests__/hasteExample.test.js create mode 100644 e2e/custom-haste-map/__tests__/hasteExampleHelper.js create mode 100644 e2e/custom-haste-map/hasteMap.js create mode 100644 e2e/custom-haste-map/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a910aad15429..a5be224c3b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-config, jest-haste-map, jest-resolve, jest-runner, jest-runtime, jest-test-sequencer, jest-transform, jest-types]` [**BREAKING**] Add custom HasteMap class implementation config option ([#11107](https://github.com/facebook/jest/pull/11107)) - `[babel-jest]` Add async transformation ([#11192](https://github.com/facebook/jest/pull/11192)) - `[jest-changed-files]` Use '--' to separate paths from revisions ([#11160](https://github.com/facebook/jest/pull/11160)) - `[jest-circus]` [**BREAKING**] Fail tests when multiple `done()` calls are made ([#10624](https://github.com/facebook/jest/pull/10624)) diff --git a/docs/Configuration.md b/docs/Configuration.md index b2ba31e0d322..5d4c0eee8ca2 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -514,6 +514,8 @@ type HasteConfig = { platforms?: Array; /** Whether to throw on error on module collision. */ throwOnModuleCollision?: boolean; + /** Custom HasteMap module */ + hasteMapModulePath?: string; }; ``` diff --git a/e2e/__tests__/customHaste.test.ts b/e2e/__tests__/customHaste.test.ts new file mode 100644 index 000000000000..4085381b79c4 --- /dev/null +++ b/e2e/__tests__/customHaste.test.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import runJest from '../runJest'; + +describe('Custom Haste Integration', () => { + test('valid test with fake module resolutions', () => { + const config = { + haste: { + hasteMapModulePath: path.resolve( + __dirname, + '..', + 'custom-haste-map/hasteMap.js', + ), + }, + }; + + const {exitCode} = runJest('custom-haste-map', [ + '--config', + JSON.stringify(config), + 'hasteExample.test.js', + ]); + expect(exitCode).toBe(0); + }); +}); diff --git a/e2e/custom-haste-map/__tests__/hasteExample.test.js b/e2e/custom-haste-map/__tests__/hasteExample.test.js new file mode 100644 index 000000000000..03942bad0799 --- /dev/null +++ b/e2e/custom-haste-map/__tests__/hasteExample.test.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const add = require('fakeModuleName'); + +describe('Custom Haste', () => { + test('adds ok', () => { + expect(true).toBe(true); + expect(add(1, 2)).toBe(3); + }); +}); diff --git a/e2e/custom-haste-map/__tests__/hasteExampleHelper.js b/e2e/custom-haste-map/__tests__/hasteExampleHelper.js new file mode 100644 index 000000000000..6a583e26591b --- /dev/null +++ b/e2e/custom-haste-map/__tests__/hasteExampleHelper.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +function add(a, b) { + return a + b; +} + +module.exports = add; diff --git a/e2e/custom-haste-map/hasteMap.js b/e2e/custom-haste-map/hasteMap.js new file mode 100644 index 000000000000..a7a5bcfb99e4 --- /dev/null +++ b/e2e/custom-haste-map/hasteMap.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const path = require('path'); +const fakeFile = { + file: path.resolve(__dirname, '__tests__/hasteExampleHelper.js'), + moduleName: 'fakeModuleName', + sha1: 'fakeSha1', +}; + +const fakeJSON = 'fakeJSON'; + +const testPath = path.resolve(__dirname, '__tests__/hasteExample.test.js'); + +const allFiles = [fakeFile.file, testPath]; + +class HasteFS { + getModuleName(file) { + if (file === fakeFile.file) { + return fakeFile.moduleName; + } + return null; + } + + getSize(file) { + return null; + } + + getDependencies(file) { + if (file === testPath) { + return fakeFile.file; + } + return []; + } + + getSha1(file) { + if (file === fakeFile.file) { + return fakeFile.sha1; + } + return null; + } + + exists(file) { + return allFiles.includes(file); + } + + getAllFiles() { + return allFiles; + } + + getFileIterator() { + return allFiles; + } + + getAbsoluteFileIterator() { + return allFiles; + } + + matchFiles(pattern) { + if (!(pattern instanceof RegExp)) { + pattern = new RegExp(pattern); + } + const files = []; + for (const file of this.getAbsoluteFileIterator()) { + if (pattern.test(file)) { + files.push(file); + } + } + return files; + } + + matchFilesWithGlob(globs, root) { + return []; + } +} + +class ModuleMap { + getModule(name, platform, supportsNativePlatform, type) { + if (name === fakeFile.moduleName) { + return fakeFile.file; + } + return null; + } + + getPackage() { + return null; + } + + getMockModule() { + return undefined; + } + + getRawModuleMap() { + return {}; + } + + toJSON() { + return fakeJSON; + } +} + +class HasteMap { + constructor(options) { + this._cachePath = HasteMap.getCacheFilePath( + options.cacheDirectory, + options.name, + ); + } + + async build() { + return { + hasteFS: new HasteFS(), + moduleMap: new ModuleMap(), + }; + } + + static getCacheFilePath(tmpdir, name) { + return path.join(tmpdir, name); + } + + getCacheFilePath() { + return this._cachePath; + } + + static getModuleMapFromJSON(json) { + if (json === fakeJSON) { + return new ModuleMap(); + } + throw new Error('Failed to parse serialized module map'); + } +} + +module.exports = HasteMap; diff --git a/e2e/custom-haste-map/package.json b/e2e/custom-haste-map/package.json new file mode 100644 index 000000000000..9adbf08c90b5 --- /dev/null +++ b/e2e/custom-haste-map/package.json @@ -0,0 +1,7 @@ +{ + "jest": { + "haste": { + "hasteMapModulePath": "/hasteMap.js" + } + } +} diff --git a/packages/jest-config/src/ValidConfig.ts b/packages/jest-config/src/ValidConfig.ts index 9fc46708c47e..24fa9cffbe5c 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -61,6 +61,7 @@ const initialOptions: Config.InitialOptions = { enableSymlinks: false, forceNodeFilesystemAPI: false, hasteImplModulePath: '/haste_impl.js', + hasteMapModulePath: '', platforms: ['ios', 'android'], throwOnModuleCollision: false, }, diff --git a/packages/jest-haste-map/src/ModuleMap.ts b/packages/jest-haste-map/src/ModuleMap.ts index 1501e4df5df4..81ca36bc9709 100644 --- a/packages/jest-haste-map/src/ModuleMap.ts +++ b/packages/jest-haste-map/src/ModuleMap.ts @@ -11,25 +11,16 @@ import * as fastPath from './lib/fast_path'; import type { DuplicatesSet, HTypeValue, - MockData, - ModuleMapData, + IModuleMap, ModuleMetaData, RawModuleMap, + SerializableModuleMap, } from './types'; const EMPTY_OBJ: Record = {}; const EMPTY_MAP = new Map(); -type ValueType = T extends Map ? V : never; - -export type SerializableModuleMap = { - duplicates: ReadonlyArray<[string, [string, [string, [string, number]]]]>; - map: ReadonlyArray<[string, ValueType]>; - mocks: ReadonlyArray<[string, ValueType]>; - rootDir: Config.Path; -}; - -export default class ModuleMap { +export default class ModuleMap implements IModuleMap { static DuplicateHasteCandidatesError: typeof DuplicateHasteCandidatesError; private readonly _raw: RawModuleMap; private json: SerializableModuleMap | undefined; diff --git a/packages/jest-haste-map/src/index.ts b/packages/jest-haste-map/src/index.ts index 5969c12df484..213e623a4cfe 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -32,12 +32,14 @@ import type { EventsQueue, FileData, FileMetaData, + HasteMapStatic, HasteRegExp, InternalHasteMap, HasteMap as InternalHasteMapObject, MockData, ModuleMapData, ModuleMetaData, + SerializableModuleMap, WorkerMetadata, } from './types'; import FSEventsWatcher = require('./watchers/FSEventsWatcher'); @@ -60,6 +62,7 @@ type Options = { extensions: Array; forceNodeFilesystemAPI?: boolean; hasteImplModulePath?: string; + hasteMapModulePath?: string; ignorePattern?: HasteRegExp; maxWorkers: number; mocksPattern?: string; @@ -106,7 +109,8 @@ type Watcher = { type WorkerInterface = {worker: typeof worker; getSha1: typeof getSha1}; export {default as ModuleMap} from './ModuleMap'; -export type {SerializableModuleMap} from './ModuleMap'; +export type {SerializableModuleMap} from './types'; +export type {IModuleMap} from './types'; export type {default as FS} from './HasteFS'; export type {ChangeEvent, HasteMap as HasteMapObject} from './types'; @@ -219,7 +223,22 @@ export default class HasteMap extends EventEmitter { private _watchers: Array; private _worker: WorkerInterface | null; - constructor(options: Options) { + static getStatic(config: Config.ProjectConfig): HasteMapStatic { + if (config.haste.hasteMapModulePath) { + return require(config.haste.hasteMapModulePath); + } + return HasteMap; + } + + static create(options: Options): HasteMap { + if (options.hasteMapModulePath) { + const CustomHasteMap = require(options.hasteMapModulePath); + return new CustomHasteMap(options); + } + return new HasteMap(options); + } + + private constructor(options: Options) { super(); this._options = { cacheDirectory: options.cacheDirectory || tmpdir(), @@ -324,6 +343,10 @@ export default class HasteMap extends EventEmitter { ); } + static getModuleMapFromJSON(json: SerializableModuleMap): HasteModuleMap { + return HasteModuleMap.fromJSON(json); + } + getCacheFilePath(): string { return this._cachePath; } diff --git a/packages/jest-haste-map/src/types.ts b/packages/jest-haste-map/src/types.ts index 106f209d9c68..27eec1928d4b 100644 --- a/packages/jest-haste-map/src/types.ts +++ b/packages/jest-haste-map/src/types.ts @@ -10,6 +10,45 @@ import type {Config} from '@jest/types'; import type HasteFS from './HasteFS'; import type ModuleMap from './ModuleMap'; +type ValueType = T extends Map ? V : never; + +export type SerializableModuleMap = { + duplicates: ReadonlyArray<[string, [string, [string, [string, number]]]]>; + map: ReadonlyArray<[string, ValueType]>; + mocks: ReadonlyArray<[string, ValueType]>; + rootDir: Config.Path; +}; + +export interface IModuleMap { + getModule( + name: string, + platform?: string | null, + supportsNativePlatform?: boolean | null, + type?: HTypeValue | null, + ): Config.Path | null; + + getPackage( + name: string, + platform: string | null | undefined, + _supportsNativePlatform: boolean | null, + ): Config.Path | null; + + getMockModule(name: string): Config.Path | undefined; + + getRawModuleMap(): RawModuleMap; + + toJSON(): S; +} + +export type HasteMapStatic = { + getCacheFilePath( + tmpdir: Config.Path, + name: string, + ...extra: Array + ): string; + getModuleMapFromJSON(json: S): IModuleMap; +}; + export type IgnoreMatcher = (item: string) => boolean; export type WorkerMessage = { @@ -71,6 +110,12 @@ export type InternalHasteMap = { mocks: MockData; }; +export type IHasteMap = { + hasteFS: HasteFS; + moduleMap: IModuleMap; + __hasteMapForTest?: InternalHasteMap | null; +}; + export type HasteMap = { hasteFS: HasteFS; moduleMap: ModuleMap; diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index 2a76b92238b7..a492cf198978 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import chalk = require('chalk'); import slash = require('slash'); import type {Config} from '@jest/types'; -import type {ModuleMap} from 'jest-haste-map'; +import type {IModuleMap} from 'jest-haste-map'; import {tryRealpath} from 'jest-util'; import ModuleNotFoundError from './ModuleNotFoundError'; import defaultResolver, {clearDefaultResolverCache} from './defaultResolver'; @@ -50,13 +50,13 @@ const nodePaths = NODE_PATH export default class Resolver { private readonly _options: ResolverConfig; - private readonly _moduleMap: ModuleMap; + private readonly _moduleMap: IModuleMap; private readonly _moduleIDCache: Map; private readonly _moduleNameCache: Map; private readonly _modulePathCache: Map>; private readonly _supportsNativePlatform: boolean; - constructor(moduleMap: ModuleMap, options: ResolverConfig) { + constructor(moduleMap: IModuleMap, options: ResolverConfig) { this._options = { defaultPlatform: options.defaultPlatform, extensions: options.extensions, diff --git a/packages/jest-runner/src/testWorker.ts b/packages/jest-runner/src/testWorker.ts index 919eeccd9c8f..37a0a6881644 100644 --- a/packages/jest-runner/src/testWorker.ts +++ b/packages/jest-runner/src/testWorker.ts @@ -9,7 +9,7 @@ import exit = require('exit'); import type {SerializableError, TestResult} from '@jest/test-result'; import type {Config} from '@jest/types'; -import {ModuleMap, SerializableModuleMap} from 'jest-haste-map'; +import HasteMap, {SerializableModuleMap} from 'jest-haste-map'; import {separateMessageFromStack} from 'jest-message-util'; import type Resolver from 'jest-resolve'; import Runtime from 'jest-runtime'; @@ -74,7 +74,9 @@ export function setup(setupData: { config, serializableModuleMap, } of setupData.serializableResolvers) { - const moduleMap = ModuleMap.fromJSON(serializableModuleMap); + const moduleMap = HasteMap.getStatic(config).getModuleMapFromJSON( + serializableModuleMap, + ); resolvers.set(config.name, Runtime.createResolver(config, moduleMap)); } } diff --git a/packages/jest-runtime/src/__tests__/Runtime-statics.test.js b/packages/jest-runtime/src/__tests__/Runtime-statics.test.js index 34d640f1cd97..5252fc5bd736 100644 --- a/packages/jest-runtime/src/__tests__/Runtime-statics.test.js +++ b/packages/jest-runtime/src/__tests__/Runtime-statics.test.js @@ -26,7 +26,7 @@ describe('Runtime statics', () => { test('Runtime.createHasteMap passes correct ignore files to HasteMap', () => { Runtime.createHasteMap(projectConfig, options); - expect(HasteMap).toBeCalledWith( + expect(HasteMap.create).toBeCalledWith( expect.objectContaining({ ignorePattern: /\/root\/ignore-1|\/root\/ignore-2/, }), @@ -35,7 +35,7 @@ describe('Runtime statics', () => { test('Runtime.createHasteMap passes correct ignore files to HasteMap in watch mode', () => { Runtime.createHasteMap(projectConfig, {...options, watch: true}); - expect(HasteMap).toBeCalledWith( + expect(HasteMap.create).toBeCalledWith( expect.objectContaining({ ignorePattern: /\/root\/ignore-1|\/root\/ignore-2|\/watch-root\/ignore-1/, diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 759b66bb39b5..e3de7866eae9 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -42,7 +42,8 @@ import { shouldInstrument, } from '@jest/transform'; import type {Config, Global} from '@jest/types'; -import HasteMap, {ModuleMap} from 'jest-haste-map'; +import type {IModuleMap} from 'jest-haste-map'; +import HasteMap from 'jest-haste-map'; import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; import type {MockFunctionMetadata, ModuleMocker} from 'jest-mock'; import {escapePathForRegex} from 'jest-regex-util'; @@ -314,7 +315,7 @@ export default class Runtime { ? new RegExp(ignorePatternParts.join('|')) : undefined; - return new HasteMap({ + return HasteMap.create({ cacheDirectory: config.cacheDirectory, computeSha1: config.haste.computeSha1, console: options?.console, @@ -323,6 +324,7 @@ export default class Runtime { extensions: [Snapshot.EXTENSION].concat(config.moduleFileExtensions), forceNodeFilesystemAPI: config.haste.forceNodeFilesystemAPI, hasteImplModulePath: config.haste.hasteImplModulePath, + hasteMapModulePath: config.haste.hasteMapModulePath, ignorePattern, maxWorkers: options?.maxWorkers || 1, mocksPattern: escapePathForRegex(path.sep + '__mocks__' + path.sep), @@ -340,7 +342,7 @@ export default class Runtime { static createResolver( config: Config.ProjectConfig, - moduleMap: ModuleMap, + moduleMap: IModuleMap, ): Resolver { return new Resolver(moduleMap, { defaultPlatform: config.haste.defaultPlatform, diff --git a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js index 65653fd6ee54..7950bc02a324 100644 --- a/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js +++ b/packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js @@ -23,6 +23,7 @@ const context = { config: { cache: true, cacheDirectory: '/cache', + haste: {}, name: 'test', }, hasteFS: { @@ -34,6 +35,7 @@ const secondContext = { config: { cache: true, cacheDirectory: '/cache2', + haste: {}, name: 'test2', }, hasteFS: { diff --git a/packages/jest-test-sequencer/src/index.ts b/packages/jest-test-sequencer/src/index.ts index 8904c8428399..8038771a3047 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -36,7 +36,8 @@ export default class TestSequencer { _getCachePath(context: Context): string { const {config} = context; - return HasteMap.getCacheFilePath( + const HasteMapClass = HasteMap.getStatic(config); + return HasteMapClass.getCacheFilePath( config.cacheDirectory, 'perf-cache-' + config.name, ); diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index dca4aa3492ba..8fb433899e59 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -194,7 +194,8 @@ class ScriptTransformer { filename: Config.Path, cacheKey: string, ): Config.Path { - const baseCacheDir = HasteMap.getCacheFilePath( + const HasteMapClass = HasteMap.getStatic(this._config); + const baseCacheDir = HasteMapClass.getCacheFilePath( this._config.cacheDirectory, 'jest-transform-cache-' + this._config.name, VERSION, diff --git a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts index 29edad4dd816..94ade78a4a2c 100644 --- a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts +++ b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts @@ -38,7 +38,12 @@ jest }, })) .mock('jest-haste-map', () => ({ - getCacheFilePath: (cacheDir: string, baseDir: string) => cacheDir + baseDir, + getStatic() { + return { + getCacheFilePath: (cacheDir: string, baseDir: string) => + cacheDir + baseDir, + }; + }, })) .mock('jest-util', () => ({ ...jest.requireActual('jest-util'), diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 756fc26e7f67..a2ffd2773c0c 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -36,6 +36,8 @@ export type HasteConfig = { platforms?: Array; /** Whether to throw on error on module collision. */ throwOnModuleCollision?: boolean; + /** Custom HasteMap module */ + hasteMapModulePath?: string; }; export type CoverageReporterName = keyof ReportOptions;