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 4 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
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);
});
});
5 changes: 5 additions & 0 deletions e2e/custom-haste-map/__tests__/hasteExampleHelper.js
@@ -0,0 +1,5 @@
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
23 changes: 22 additions & 1 deletion packages/jest-haste-map/src/ModuleMap.ts
Expand Up @@ -29,7 +29,28 @@ export type SerializableModuleMap = {
rootDir: Config.Path;
};

export default class ModuleMap {
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 default class ModuleMap implements IModuleMap<SerializableModuleMap> {
static DuplicateHasteCandidatesError: typeof DuplicateHasteCandidatesError;
private readonly _raw: RawModuleMap;
private json: SerializableModuleMap | undefined;
Expand Down
33 changes: 31 additions & 2 deletions packages/jest-haste-map/src/index.ts
Expand Up @@ -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');
Expand Down Expand Up @@ -60,6 +60,7 @@ type Options = {
extensions: Array<string>;
forceNodeFilesystemAPI?: boolean;
hasteImplModulePath?: string;
hasteMapModulePath?: string;
ignorePattern?: HasteRegExp;
maxWorkers: number;
mocksPattern?: string;
Expand Down Expand Up @@ -132,6 +133,15 @@ function invariant(condition: unknown, message?: string): asserts condition {
}
}

export type HasteMapStatic<S = SerializableModuleMap> = {
getCacheFilePath(
tmpdir: Config.Path,
name: string,
...extra: Array<string>
): string;
getModuleMapFromJSON(json: S): IModuleMap<S>;
};

/**
* HasteMap is a JavaScript implementation of Facebook's haste module system.
*
Expand Down Expand Up @@ -219,7 +229,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 this;
DiZy marked this conversation as resolved.
Show resolved Hide resolved
}

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 +349,10 @@ export default class HasteMap extends EventEmitter {
);
}

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

getCacheFilePath(): string {
return this._cachePath;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/jest-haste-map/src/types.ts
Expand Up @@ -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
DiZy marked this conversation as resolved.
Show resolved Hide resolved
import type ModuleMap from './ModuleMap';
// eslint-disable-next-line import/no-duplicates
import type {IModuleMap} from './ModuleMap';

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

Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/jest-resolve/src/index.ts
Expand Up @@ -9,9 +9,9 @@

import * as path from 'path';
import chalk = require('chalk');
import type {IModuleMap} from 'jest-haste-map/src/ModuleMap';
DiZy marked this conversation as resolved.
Show resolved Hide resolved
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';
Expand Down Expand Up @@ -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<string, string>;
private readonly _moduleNameCache: Map<string, Config.Path>;
private readonly _modulePathCache: Map<string, Array<Config.Path>>;
private readonly _supportsNativePlatform: boolean;

constructor(moduleMap: ModuleMap, options: ResolverConfig) {
constructor(moduleMap: IModuleMap, options: ResolverConfig) {
this._options = {
defaultPlatform: options.defaultPlatform,
extensions: options.extensions,
Expand Down
6 changes: 4 additions & 2 deletions packages/jest-runner/src/testWorker.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
}
}
Expand Down