From b30f4db14dcfb98ef03e21cd1f1a691384ae5565 Mon Sep 17 00:00:00 2001 From: David Zilburg Date: Mon, 10 May 2021 13:05:48 -0400 Subject: [PATCH] Custom haste map module config --- e2e/__tests__/customHaste.test.ts | 30 ++++ .../__tests__/hasteExample.test.js | 18 +++ .../__tests__/hasteExampleHelper.js | 5 + 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 | 23 ++- packages/jest-haste-map/src/index.ts | 16 +- packages/jest-haste-map/src/types.ts | 9 ++ packages/jest-resolve/src/index.ts | 6 +- packages/jest-runner/src/testWorker.ts | 7 +- packages/jest-runtime/src/index.ts | 11 +- packages/jest-test-sequencer/src/index.ts | 7 +- .../jest-transform/src/ScriptTransformer.ts | 7 +- packages/jest-types/src/Config.ts | 2 + 15 files changed, 274 insertions(+), 14 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/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..aa666c97a732 --- /dev/null +++ b/e2e/custom-haste-map/__tests__/hasteExampleHelper.js @@ -0,0 +1,5 @@ +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 d655914a100e..a0789cb3aeaa 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -58,6 +58,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..7a53cf8e8f73 100644 --- a/packages/jest-haste-map/src/ModuleMap.ts +++ b/packages/jest-haste-map/src/ModuleMap.ts @@ -29,7 +29,28 @@ export type SerializableModuleMap = { rootDir: Config.Path; }; -export default class ModuleMap { +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 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..0968773dfbf9 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -18,7 +18,7 @@ import {escapePathForRegex} from 'jest-regex-util'; import serializer from 'jest-serializer'; import {Worker} from 'jest-worker'; import HasteFS from './HasteFS'; -import HasteModuleMap from './ModuleMap'; +import HasteModuleMap, {IModuleMap, SerializableModuleMap} from './ModuleMap'; import H from './constants'; import nodeCrawl = require('./crawlers/node'); import watchmanCrawl = require('./crawlers/watchman'); @@ -132,6 +132,16 @@ function invariant(condition: unknown, message?: string): asserts condition { } } +export type HasteMapStatic = { + new (options: Options): HasteMap; + getCacheFilePath( + tmpdir: Config.Path, + name: string, + ...extra: Array + ): string; + getModuleMapFromJSON(json: S): IModuleMap; +}; + /** * HasteMap is a JavaScript implementation of Facebook's haste module system. * @@ -324,6 +334,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..d239d6f3293e 100644 --- a/packages/jest-haste-map/src/types.ts +++ b/packages/jest-haste-map/src/types.ts @@ -8,7 +8,10 @@ import type {Stats} from 'graceful-fs'; import type {Config} from '@jest/types'; import type HasteFS from './HasteFS'; +// eslint-disable-next-line import/no-duplicates import type ModuleMap from './ModuleMap'; +// eslint-disable-next-line import/no-duplicates +import type {IModuleMap} from './ModuleMap'; export type IgnoreMatcher = (item: string) => boolean; @@ -71,6 +74,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..5f8ec04803f7 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -9,9 +9,9 @@ import * as path from 'path'; import chalk = require('chalk'); +import type {IModuleMap} from 'jest-haste-map/src/ModuleMap'; import slash = require('slash'); import type {Config} from '@jest/types'; -import type {ModuleMap} 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..6c4d9c60f190 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, {HasteMapStatic, 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,10 @@ export function setup(setupData: { config, serializableModuleMap, } of setupData.serializableResolvers) { - const moduleMap = ModuleMap.fromJSON(serializableModuleMap); + const HasteMapClass = config.haste.hasteMapModulePath + ? (require(config.haste.hasteMapModulePath) as HasteMapStatic) + : HasteMap; + const moduleMap = HasteMapClass.getModuleMapFromJSON(serializableModuleMap); resolvers.set(config.name, Runtime.createResolver(config, moduleMap)); } } diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 0838e25d5b5c..be3228dc3f03 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -21,6 +21,7 @@ import { import {parse as parseCjs} from 'cjs-module-lexer'; import {CoverageInstrumenter, V8Coverage} from 'collect-v8-coverage'; import * as fs from 'graceful-fs'; +import type {IModuleMap} from 'jest-haste-map/src/ModuleMap'; import stripBOM = require('strip-bom'); import type { Jest, @@ -42,7 +43,7 @@ import { shouldInstrument, } from '@jest/transform'; import type {Config, Global} from '@jest/types'; -import HasteMap, {ModuleMap} from 'jest-haste-map'; +import HasteMap, {HasteMapStatic} 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,11 @@ export default class Runtime { ? new RegExp(ignorePatternParts.join('|')) : undefined; - return new HasteMap({ + const HasteMapClass = config.haste.hasteMapModulePath + ? (require(config.haste.hasteMapModulePath) as HasteMapStatic) + : HasteMap; + + return new HasteMapClass({ cacheDirectory: config.cacheDirectory, computeSha1: config.haste.computeSha1, console: options?.console, @@ -340,7 +345,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/index.ts b/packages/jest-test-sequencer/src/index.ts index 8904c8428399..3e32e88f70e6 100644 --- a/packages/jest-test-sequencer/src/index.ts +++ b/packages/jest-test-sequencer/src/index.ts @@ -7,7 +7,7 @@ import * as fs from 'graceful-fs'; import type {AggregatedResult} from '@jest/test-result'; -import HasteMap from 'jest-haste-map'; +import HasteMap, {HasteMapStatic} from 'jest-haste-map'; import type {Test} from 'jest-runner'; import type {Context} from 'jest-runtime'; @@ -36,7 +36,10 @@ export default class TestSequencer { _getCachePath(context: Context): string { const {config} = context; - return HasteMap.getCacheFilePath( + const HasteMapClass = config.haste.hasteMapModulePath + ? (require(config.haste.hasteMapModulePath) as HasteMapStatic) + : HasteMap; + 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 0cf2de288a88..868a9999162a 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -17,7 +17,7 @@ import {addHook} from 'pirates'; import slash = require('slash'); import {sync as writeFileAtomic} from 'write-file-atomic'; import type {Config} from '@jest/types'; -import HasteMap from 'jest-haste-map'; +import HasteMap, {HasteMapStatic} from 'jest-haste-map'; import { createDirectory, isPromise, @@ -194,7 +194,10 @@ class ScriptTransformer { filename: Config.Path, cacheKey: string, ): Config.Path { - const baseCacheDir = HasteMap.getCacheFilePath( + const HasteMapClass = this._config.haste.hasteMapModulePath + ? (require(this._config.haste.hasteMapModulePath) as HasteMapStatic) + : HasteMap; + const baseCacheDir = HasteMapClass.getCacheFilePath( this._config.cacheDirectory, 'jest-transform-cache-' + this._config.name, VERSION, diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 49113fa9ce34..766b65ae4349 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 */ + hasteMapModulePath?: string; }; export type CoverageReporterName = keyof ReportOptions;