Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom haste #11107

Merged
merged 9 commits into from May 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions docs/Configuration.md
Expand Up @@ -514,6 +514,8 @@ type HasteConfig = {
platforms?: Array<string>;
/** Whether to throw on error on module collision. */
throwOnModuleCollision?: boolean;
/** Custom HasteMap module */
hasteMapModulePath?: string;
};
```

Expand Down
30 changes: 30 additions & 0 deletions 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);
});
});
18 changes: 18 additions & 0 deletions 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);
});
});
15 changes: 15 additions & 0 deletions 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;
139 changes: 139 additions & 0 deletions 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;
7 changes: 7 additions & 0 deletions e2e/custom-haste-map/package.json
@@ -0,0 +1,7 @@
{
"jest": {
"haste": {
"hasteMapModulePath": "<rootDir>/hasteMap.js"
}
}
}
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.ts
Expand Up @@ -58,6 +58,7 @@ const initialOptions: Config.InitialOptions = {
enableSymlinks: false,
forceNodeFilesystemAPI: false,
hasteImplModulePath: '<rootDir>/haste_impl.js',
hasteMapModulePath: '',
platforms: ['ios', 'android'],
throwOnModuleCollision: false,
},
Expand Down
15 changes: 3 additions & 12 deletions packages/jest-haste-map/src/ModuleMap.ts
Expand Up @@ -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<string, ModuleMetaData> = {};
const EMPTY_MAP = new Map();

type ValueType<T> = T extends Map<string, infer V> ? V : never;

export type SerializableModuleMap = {
duplicates: ReadonlyArray<[string, [string, [string, [string, number]]]]>;
map: ReadonlyArray<[string, ValueType<ModuleMapData>]>;
mocks: ReadonlyArray<[string, ValueType<MockData>]>;
rootDir: Config.Path;
};

export default class ModuleMap {
export default class ModuleMap implements IModuleMap<SerializableModuleMap> {
static DuplicateHasteCandidatesError: typeof DuplicateHasteCandidatesError;
private readonly _raw: RawModuleMap;
private json: SerializableModuleMap | undefined;
Expand Down
27 changes: 25 additions & 2 deletions packages/jest-haste-map/src/index.ts
Expand Up @@ -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');
Expand All @@ -60,6 +62,7 @@ type Options = {
extensions: Array<string>;
forceNodeFilesystemAPI?: boolean;
hasteImplModulePath?: string;
hasteMapModulePath?: string;
ignorePattern?: HasteRegExp;
maxWorkers: number;
mocksPattern?: string;
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -219,7 +223,22 @@ export default class HasteMap extends EventEmitter {
private _watchers: Array<Watcher>;
private _worker: WorkerInterface | null;

constructor(options: Options) {
static getStatic(config: Config.ProjectConfig): HasteMapStatic {
Copy link
Member

Choose a reason for hiding this comment

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

can we make this async so we can use import(config.haste.hasteMapModulePath) in the future? same for create

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing to async complicates some call sites such as the setup function in testWorker.ts

Copy link
Member

Choose a reason for hiding this comment

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

right, but it needs to be async in order to use ESM. so we either need to make it so now, or make it async whenever somebody wants to write these modules using ESM (such as a WASM implementation)

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(),
Expand Down Expand Up @@ -324,6 +343,10 @@ export default class HasteMap extends EventEmitter {
);
}

static getModuleMapFromJSON(json: SerializableModuleMap): HasteModuleMap {
return HasteModuleMap.fromJSON(json);
}

getCacheFilePath(): string {
return this._cachePath;
}
Expand Down
45 changes: 45 additions & 0 deletions packages/jest-haste-map/src/types.ts
Expand Up @@ -10,6 +10,45 @@ import type {Config} from '@jest/types';
import type HasteFS from './HasteFS';
import type ModuleMap from './ModuleMap';

type ValueType<T> = T extends Map<string, infer V> ? V : never;

export type SerializableModuleMap = {
duplicates: ReadonlyArray<[string, [string, [string, [string, number]]]]>;
map: ReadonlyArray<[string, ValueType<ModuleMapData>]>;
mocks: ReadonlyArray<[string, ValueType<MockData>]>;
rootDir: Config.Path;
};

export interface IModuleMap<S = SerializableModuleMap> {
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<S = SerializableModuleMap> = {
getCacheFilePath(
tmpdir: Config.Path,
name: string,
...extra: Array<string>
): string;
getModuleMapFromJSON(json: S): IModuleMap<S>;
};

export type IgnoreMatcher = (item: string) => boolean;

export type WorkerMessage = {
Expand Down Expand Up @@ -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;
Expand Down