diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af6cc2ac6ad..2611f1bbae31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 9a78de751fe5..061f79bcb254 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -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)" `; @@ -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)" `; diff --git a/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap b/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap index 62cd96c39c42..34f55ffd75e3 100644 --- a/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap +++ b/e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap @@ -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. (node_modules/discord.js/src/index.js:21:12) at Object.require (__tests__/test.js:10:1)" `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index a6c967952cb4..98867dc0fce7 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -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)" `; diff --git a/packages/jest-resolve/package.json b/packages/jest-resolve/package.json index 13b19daff0d6..d0bec260ddbb 100644 --- a/packages/jest-resolve/package.json +++ b/packages/jest-resolve/package.json @@ -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:^", diff --git a/packages/jest-resolve/src/__mocks__/imports/foo-import/internal.cjs b/packages/jest-resolve/src/__mocks__/imports/foo-import/internal.cjs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/main.js b/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/main.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/package.json b/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/package.json new file mode 100644 index 000000000000..c52a84e69b16 --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "external-foo", + "main": "main.js" +} diff --git a/packages/jest-resolve/src/__mocks__/imports/foo-import/package.json b/packages/jest-resolve/src/__mocks__/imports/foo-import/package.json new file mode 100644 index 000000000000..85e20d103e78 --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/imports/foo-import/package.json @@ -0,0 +1,9 @@ +{ + "name": "foo-import", + "imports": { + "#nested": { + "require": "./internal.cjs", + "default": "external-foo" + } + } +} diff --git a/packages/jest-resolve/src/__mocks__/imports/package.json b/packages/jest-resolve/src/__mocks__/imports/package.json new file mode 100644 index 000000000000..6fedff11360b --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/imports/package.json @@ -0,0 +1,3 @@ +{ + "name": "imports" +} diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index bad6fe4a3cd5..d9c69dbc13e1 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -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', () => { diff --git a/packages/jest-resolve/src/defaultResolver.ts b/packages/jest-resolve/src/defaultResolver.ts index 03be868ebc1a..bffa2ad9177f 100644 --- a/packages/jest-resolve/src/defaultResolver.ts +++ b/packages/jest-resolve/src/defaultResolver.ts @@ -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 { @@ -83,7 +84,7 @@ export type ResolverOptions = { }; type UpstreamResolveOptionsWithConditions = UpstreamResolveOptions & - Pick; + ResolverOptions; export type SyncResolver = (path: string, options: ResolverOptions) => string; export type AsyncResolver = ( @@ -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()}`; } @@ -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); diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index 9318b3b4059c..d95a363fd1f7 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -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; } } @@ -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; } } diff --git a/yarn.lock b/yarn.lock index b5e4cf38be16..4706e2ebf74a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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