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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for Resource Queries #6282

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 3 additions & 3 deletions packages/jest-config/src/normalize.ts
Expand Up @@ -137,12 +137,12 @@ const setupPreset = (
// Force re-evaluation to support multiple projects
try {
if (presetModule) {
delete require.cache[require.resolve(presetModule)];
delete require.cache[require.resolve(presetModule.path)];
}
} catch (e) {}

// @ts-ignore: `presetModule` can be null?
preset = require(presetModule);
preset = require(presetModule.path);
} catch (error) {
if (error instanceof SyntaxError || error instanceof TypeError) {
throw createConfigError(
Expand Down Expand Up @@ -435,7 +435,7 @@ const normalizeReporters = (options: Config.InitialOptionsWithRootDir) => {
` Module name: ${reporterPath}`,
);
}
normalizedReporterConfig[0] = reporter;
normalizedReporterConfig[0] = reporter.path;
}
return normalizedReporterConfig;
});
Expand Down
6 changes: 3 additions & 3 deletions packages/jest-config/src/utils.ts
Expand Up @@ -49,7 +49,7 @@ export const resolve = (
);
}
/// can cast as string since nulls will be thrown
return module as string;
return module?.path as string;
};

export const escapeGlobCharacters = (path: Config.Path): Config.Glob =>
Expand Down Expand Up @@ -139,7 +139,7 @@ export const resolveWithPrefix = (
resolver: resolver || undefined,
});
if (module) {
return module;
return module.path;
}

try {
Expand All @@ -151,7 +151,7 @@ export const resolveWithPrefix = (
resolver: resolver || undefined,
});
if (module) {
return module;
return module.path;
}

try {
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-reporters/src/coverage_worker.ts
Expand Up @@ -37,7 +37,7 @@ export function worker({
}: CoverageWorkerData): CoverageWorkerResult | null {
return generateEmptyCoverage(
fs.readFileSync(path, 'utf8'),
path,
{id: path, path},
globalConfig,
config,
options && options.changedFiles && new Set(options.changedFiles),
Expand Down
8 changes: 4 additions & 4 deletions packages/jest-reporters/src/generateEmptyCoverage.ts
Expand Up @@ -27,7 +27,7 @@ export type CoverageWorkerResult =

export default function (
source: string,
filename: Config.Path,
filename: Config.RPth,
globalConfig: Config.GlobalConfig,
config: Config.ProjectConfig,
changedFiles?: Set<Config.Path>,
Expand All @@ -40,9 +40,9 @@ export default function (
coverageProvider: globalConfig.coverageProvider,
};
let coverageWorkerResult: CoverageWorkerResult | null = null;
if (shouldInstrument(filename, coverageOptions, config)) {
if (shouldInstrument(filename.path, coverageOptions, config)) {
if (coverageOptions.coverageProvider === 'v8') {
const stat = fs.statSync(filename);
const stat = fs.statSync(filename.path);
return {
kind: 'V8Coverage',
result: {
Expand All @@ -60,7 +60,7 @@ export default function (
},
],
scriptId: '0',
url: filename,
url: filename.path,
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-resolve-dependencies/src/index.ts
Expand Up @@ -56,7 +56,7 @@ class DependencyResolver {
file,
dependency,
options,
);
).path;
} catch {
try {
resolvedDependency = this._resolver.getMockModule(file, dependency);
Expand Down
108 changes: 73 additions & 35 deletions packages/jest-resolve/src/index.ts
Expand Up @@ -49,12 +49,18 @@ const nodePaths = NODE_PATH
.map(p => path.resolve(resolvedCwd, p))
: undefined;

function parseOutQuery(pathQithQuery: string) {
const [path, ...queryParts] = pathQithQuery.split('?');
const query = queryParts.join('');
return [path, query];
}

/* eslint-disable-next-line no-redeclare */
class Resolver {
private readonly _options: ResolverConfig;
private readonly _moduleMap: ModuleMap;
private readonly _moduleIDCache: Map<string, string>;
private readonly _moduleNameCache: Map<string, Config.Path>;
private readonly _moduleNameCache: Map<string, Config.RPth>;
private readonly _modulePathCache: Map<string, Array<Config.Path>>;
private readonly _supportsNativePlatform: boolean;

Expand Down Expand Up @@ -105,14 +111,15 @@ class Resolver {
static findNodeModule(
path: Config.Path,
options: FindNodeModuleConfig,
): Config.Path | null {
): Config.RPth | null {
const resolver: typeof defaultResolver = options.resolver
? require(options.resolver)
: defaultResolver;
const paths = options.paths;

try {
return resolver(path, {
const [nativePath, query] = parseOutQuery(path);
const realpath = resolver(nativePath, {
basedir: options.basedir,
browser: options.browser,
defaultResolver,
Expand All @@ -121,6 +128,11 @@ class Resolver {
paths: paths ? (nodePaths || []).concat(paths) : nodePaths,
rootDir: options.rootDir,
});

return {
id: `${realpath}?${query}`,
path: realpath,
};
} catch (e) {
if (options.throwIfNotFound) {
throw e;
Expand All @@ -133,13 +145,12 @@ class Resolver {
dirname: Config.Path,
moduleName: string,
options?: Resolver.ResolveModuleConfig,
): Config.Path | null {
): Config.RPth | null {
const paths = (options && options.paths) || this._options.modulePaths;
const moduleDirectory = this._options.moduleDirectories;
const key = dirname + path.delimiter + moduleName;
const defaultPlatform = this._options.defaultPlatform;
const extensions = this._options.extensions.slice();
let module;

if (this._supportsNativePlatform) {
extensions.unshift(
Expand All @@ -152,6 +163,8 @@ class Resolver {
);
}

const [nativeName, query] = parseOutQuery(moduleName);

// 1. If we have already resolved this module for this directory name,
// return a value from the cache.
const cacheResult = this._moduleNameCache.get(key);
Expand All @@ -160,10 +173,14 @@ class Resolver {
}

// 2. Check if the module is a haste module.
module = this.getModule(moduleName);
if (module) {
this._moduleNameCache.set(key, module);
return module;
const hasteModule = this.getModule(nativeName);
if (hasteModule) {
const resolvedPath = {
id: `haste:${hasteModule}?${query}`,
path: hasteModule,
};
this._moduleNameCache.set(key, resolvedPath);
return resolvedPath;
}

// 3. Check if the module is a node module and resolve it based on
Expand All @@ -189,11 +206,14 @@ class Resolver {

if (!skipResolution) {
// @ts-ignore: the "pnp" version named isn't in DefinitelyTyped
module = resolveNodeModule(moduleName, Boolean(process.versions.pnp));
const nodeModule = resolveNodeModule(
moduleName,
Boolean(process.versions.pnp),
);

if (module) {
this._moduleNameCache.set(key, module);
return module;
if (nodeModule) {
this._moduleNameCache.set(key, nodeModule);
return nodeModule;
}
}

Expand All @@ -202,15 +222,23 @@ class Resolver {
const parts = moduleName.split('/');
const hastePackage = this.getPackage(parts.shift()!);
if (hastePackage) {
const requireResolve = (name: string) => {
const path = require.resolve(name);
return {
id: `haste:${path}?${query}`,
path,
};
};
try {
const module = path.join.apply(
const hastePackageModule = path.join.apply(
path,
[path.dirname(hastePackage)].concat(parts),
);
// try resolving with custom resolver first to support extensions,
// then fallback to require.resolve
const resolvedModule =
resolveNodeModule(module) || require.resolve(module);
resolveNodeModule(hastePackageModule) ||
requireResolve(hastePackageModule);
this._moduleNameCache.set(key, resolvedModule);
return resolvedModule;
} catch (ignoredError) {}
Expand All @@ -223,7 +251,7 @@ class Resolver {
from: Config.Path,
moduleName: string,
options?: Resolver.ResolveModuleConfig,
): Config.Path {
): Config.RPth {
const dirname = path.dirname(from);
const module =
this.resolveStubModuleName(from, moduleName) ||
Expand Down Expand Up @@ -288,7 +316,7 @@ class Resolver {
} else {
const moduleName = this.resolveStubModuleName(from, name);
if (moduleName) {
return this.getModule(moduleName) || moduleName;
return this.getModule(moduleName.path) || moduleName.path;
}
}
return null;
Expand Down Expand Up @@ -373,7 +401,7 @@ class Resolver {
return virtualMocks[virtualMockPath]
? virtualMockPath
: moduleName
? this.resolveModule(from, moduleName)
? this.resolveModule(from, moduleName).path
: from;
}

Expand All @@ -386,7 +414,7 @@ class Resolver {
resolveStubModuleName(
from: Config.Path,
moduleName: string,
): Config.Path | null {
): Config.RPth | null {
const dirname = path.dirname(from);
const paths = this._options.modulePaths;
const extensions = this._options.extensions.slice();
Expand All @@ -407,12 +435,14 @@ class Resolver {
);
}

const [nativeName, query] = parseOutQuery(moduleName);

if (moduleNameMapper) {
for (const {moduleName: mappedModuleName, regex} of moduleNameMapper) {
if (regex.test(moduleName)) {
if (regex.test(nativeName)) {
// Note: once a moduleNameMapper matches the name, it must result
// in a module, or else an error is thrown.
const matches = moduleName.match(regex);
const matches = nativeName.match(regex);
const mapModuleName = matches
? (moduleName: string) =>
moduleName.replace(
Expand All @@ -424,23 +454,31 @@ class Resolver {
const possibleModuleNames = Array.isArray(mappedModuleName)
? mappedModuleName
: [mappedModuleName];
let module: string | null = null;
let module: Config.RPth | null = null;
for (const possibleModuleName of possibleModuleNames) {
const updatedName = mapModuleName(possibleModuleName);

module =
this.getModule(updatedName) ||
Resolver.findNodeModule(updatedName, {
basedir: dirname,
browser: this._options.browser,
extensions,
moduleDirectory,
paths,
resolver,
rootDir: this._options.rootDir,
});

if (module) {
const hasteModule = this.getModule(updatedName);
if (hasteModule) {
module = {
id: `haste:${hasteModule}?${query}`,
path: hasteModule,
};
break;
}

const nodeModule = Resolver.findNodeModule(updatedName, {
basedir: dirname,
browser: this._options.browser,
extensions,
moduleDirectory,
paths,
resolver,
rootDir: this._options.rootDir,
});

if (nodeModule) {
module = nodeModule;
break;
}
}
Expand Down
14 changes: 14 additions & 0 deletions packages/jest-runtime/src/__mocks__/resourceQueryResolver.js
@@ -0,0 +1,14 @@
/**
* 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';

module.exports = function userResolver(path, options) {
const defaultResolver = require('../__tests__/defaultResolver.js');
const [clearPath, query] = path.split('?');
return defaultResolver(clearPath, options) + (query ? '?' + query : '');
};
41 changes: 41 additions & 0 deletions packages/jest-runtime/src/__tests__/runtime_require_module.test.js
Expand Up @@ -252,6 +252,47 @@ describe('Runtime requireModule', () => {
expect(hastePackage.isHastePackage).toBe(true);
}));

it('supports resolving the same path to multiple modules', () =>
createRuntime(__filename, {
// using the default resolver as a custom resolver
resolver: require.resolve('../__mocks__/resourceQueryResolver.js'),
}).then(runtime => {
const moduleNoQuery1 = runtime.requireModule(
runtime.__mockRootPath,
'./moduleForResourceQuery.js',
);
const moduleNoQuery2 = runtime.requireModule(
runtime.__mockRootPath,
'./moduleForResourceQuery.js',
);
expect(moduleNoQuery1.name).toBe('moduleForResourceQuery');
expect(moduleNoQuery1).toBe(moduleNoQuery2);

const moduleWithQueryA = runtime.requireModule(
runtime.__mockRootPath,
'./moduleForResourceQuery.js?a',
);
const moduleWithQueryB = runtime.requireModule(
runtime.__mockRootPath,
'./moduleForResourceQuery.js?b',
);
expect(moduleWithQueryA.name).toBe('moduleForResourceQuery');
expect(moduleWithQueryB.name).toBe('moduleForResourceQuery');
expect(moduleWithQueryA).not.toBe(moduleWithQueryB);

const moduleWithSameQuery1 = runtime.requireModule(
runtime.__mockRootPath,
'./moduleForResourceQuery.js?sameQuery',
);
const moduleWithSameQuery2 = runtime.requireModule(
runtime.__mockRootPath,
'./moduleForResourceQuery.js?sameQuery',
);
expect(moduleWithSameQuery1.name).toBe('moduleForResourceQuery');
expect(moduleWithSameQuery2.name).toBe('moduleForResourceQuery');
expect(moduleWithSameQuery1).toBe(moduleWithSameQuery2);
}));

it('resolves node modules properly when crawling node_modules', () =>
// While we are crawling a node module, we shouldn't put package.json
// files of node modules to resolve to `package.json` but rather resolve
Expand Down