Skip to content

Commit

Permalink
feat: support subpath imports (#13705)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Jan 1, 2023
1 parent abc64a6 commit 74776d8
Show file tree
Hide file tree
Showing 14 changed files with 106 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-resolve]` Support subpath imports ([#13705](https://github.com/facebook/jest/pull/13705))
- `[jest-runtime]` Add `jest.isolateModulesAsync` for scoped module initialization of asynchronous functions ([#13680](https://github.com/facebook/jest/pull/13680))
- `[jest-test-result]` Added `skipped` and `focused` status to `FormattedTestResult` ([#13700](https://github.com/facebook/jest/pull/13700))

Expand Down
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Expand Up @@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = `
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:758:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:760:17)
at Object.require (index.js:10:1)
at Object.require (__tests__/index.js:10:20)"
`;
Expand Down Expand Up @@ -71,7 +71,7 @@ exports[`moduleNameMapper wrong configuration 1`] = `
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:758:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:760:17)
at Object.require (index.js:10:1)
at Object.require (__tests__/index.js:10:20)"
`;
2 changes: 1 addition & 1 deletion e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap
Expand Up @@ -26,7 +26,7 @@ exports[`shows a proper error from deep requires 1`] = `
12 | test('dummy', () => {
13 | expect(1).toBe(1);
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:425:11)
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:427:11)
at Object.<anonymous> (node_modules/discord.js/src/index.js:21:12)
at Object.require (__tests__/test.js:10:1)"
`;
Expand Up @@ -37,7 +37,7 @@ exports[`show error message with matching files 1`] = `
| ^
9 |
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:425:11)
at Resolver._throwModNotFoundError (../../packages/jest-resolve/build/resolver.js:427:11)
at Object.require (index.js:8:18)
at Object.require (__tests__/test.js:8:11)"
`;
1 change: 1 addition & 0 deletions packages/jest-resolve/package.json
Expand Up @@ -17,6 +17,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@okikio/resolve.imports": "^1.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"jest-haste-map": "workspace:^",
Expand Down
Empty file.
Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,9 @@
{
"name": "foo-import",
"imports": {
"#nested": {
"require": "./internal.cjs",
"default": "external-foo"
}
}
}
3 changes: 3 additions & 0 deletions packages/jest-resolve/src/__mocks__/imports/package.json
@@ -0,0 +1,3 @@
{
"name": "imports"
}
38 changes: 38 additions & 0 deletions packages/jest-resolve/src/__tests__/resolve.test.ts
Expand Up @@ -316,6 +316,44 @@ describe('findNodeModule', () => {
expect(result).toBeNull();
});
});

describe('imports', () => {
const importsRoot = path.resolve(__dirname, '../__mocks__/imports');

test('supports internal reference', () => {
const result = Resolver.findNodeModule('#nested', {
basedir: path.resolve(importsRoot, './foo-import/index.cjs'),
conditions: ['require'],
});

expect(result).toEqual(
path.resolve(importsRoot, './foo-import/internal.cjs'),
);
});

test('supports external reference', () => {
const result = Resolver.findNodeModule('#nested', {
basedir: path.resolve(importsRoot, './foo-import/index.js'),
conditions: [],
});

expect(result).toEqual(
path.resolve(
importsRoot,
'./foo-import/node_modules/external-foo/main.js',
),
);
});

test('fails for non-existent mapping', () => {
expect(() => {
Resolver.findNodeModule('#something-else', {
basedir: path.resolve(importsRoot, './foo-import/index.js'),
conditions: [],
});
}).toThrow('Missing "#something-else" import in "foo-import" package');
});
});
});

describe('findNodeModuleAsync', () => {
Expand Down
37 changes: 34 additions & 3 deletions packages/jest-resolve/src/defaultResolver.ts
Expand Up @@ -6,6 +6,7 @@
*/

import {dirname, isAbsolute, resolve as pathResolve} from 'path';
import {resolve as resolveImports} from '@okikio/resolve.imports';
import pnpResolver from 'jest-pnp-resolver';
import {SyncOpts as UpstreamResolveOptions, sync as resolveSync} from 'resolve';
import {
Expand Down Expand Up @@ -83,7 +84,7 @@ export type ResolverOptions = {
};

type UpstreamResolveOptionsWithConditions = UpstreamResolveOptions &
Pick<ResolverOptions, 'basedir' | 'conditions'>;
ResolverOptions;

export type SyncResolver = (path: string, options: ResolverOptions) => string;
export type AsyncResolver = (
Expand Down Expand Up @@ -136,12 +137,42 @@ function getPathInModule(
return path;
}

if (path.startsWith('#')) {
const closestPackageJson = findClosestPackageJson(options.basedir);

if (!closestPackageJson) {
throw new Error(
`Jest: unable to locate closest package.json from ${options.basedir} when resolving import "${path}"`,
);
}

const pkg = readPackageCached(closestPackageJson);

const resolved = resolveImports(
pkg,
path,
createResolveOptions(options.conditions),
);

if (!resolved) {
throw new Error(
'`imports` exists, but no results - this is a bug in Jest. Please report an issue',
);
}

if (resolved.startsWith('.')) {
return pathResolve(dirname(closestPackageJson), resolved);
}

// this is an external module, re-resolve it
return defaultResolver(resolved, options);
}

const segments = path.split('/');

let moduleName = segments.shift();

if (moduleName) {
// TODO: handle `#` here: https://github.com/facebook/jest/issues/12270
if (moduleName.startsWith('@')) {
moduleName = `${moduleName}/${segments.shift()}`;
}
Expand Down Expand Up @@ -213,6 +244,6 @@ function createResolveOptions(
{browser: false, require: true};
}

// if it's a relative import or an absolute path, exports are ignored
// if it's a relative import or an absolute path, imports/exports are ignored
const shouldIgnoreRequestForExports = (path: string) =>
path.startsWith('.') || isAbsolute(path);
6 changes: 4 additions & 2 deletions packages/jest-resolve/src/resolver.ts
Expand Up @@ -131,7 +131,8 @@ export default class Resolver {
rootDir: options.rootDir,
});
} catch (e) {
if (options.throwIfNotFound) {
// we always wanna throw if it's an internal import
if (options.throwIfNotFound || path.startsWith('#')) {
throw e;
}
}
Expand Down Expand Up @@ -174,7 +175,8 @@ export default class Resolver {
});
return result;
} catch (e: unknown) {
if (options.throwIfNotFound) {
// we always wanna throw if it's an internal import
if (options.throwIfNotFound || path.startsWith('#')) {
throw e;
}
}
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Expand Up @@ -3757,6 +3757,13 @@ __metadata:
languageName: node
linkType: hard

"@okikio/resolve.imports@npm:^1.0.0":
version: 1.0.0
resolution: "@okikio/resolve.imports@npm:1.0.0"
checksum: a06d347731b3c47e79125d346dd0172fda3cf20d138249f4ed23c5cc60b32f668f9bbab49698fc061ef5782d236622ff4cd0ce2b2520550273e863f55b687350
languageName: node
linkType: hard

"@pkgr/utils@npm:^2.3.1":
version: 2.3.1
resolution: "@pkgr/utils@npm:2.3.1"
Expand Down Expand Up @@ -12918,6 +12925,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "jest-resolve@workspace:packages/jest-resolve"
dependencies:
"@okikio/resolve.imports": ^1.0.0
"@tsd/typescript": ^4.9.0
"@types/graceful-fs": ^4.1.3
"@types/pnpapi": ^0.0.2
Expand Down

0 comments on commit 74776d8

Please sign in to comment.