From 2700ac0bfe73bd368485f120f17065ab6b0d2966 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 1 Jan 2023 12:17:14 +0100 Subject: [PATCH 1/5] feat: support subpath imports --- CHANGELOG.md | 1 + packages/jest-resolve/package.json | 1 + .../src/__mocks__/imports/foo/internal.cjs | 0 .../foo/node_modules/external-foo/main.js | 0 .../node_modules/external-foo/package.json | 4 ++ .../src/__mocks__/imports/foo/package.json | 9 +++++ .../src/__mocks__/imports/package.json | 3 ++ .../src/__tests__/resolve.test.ts | 35 ++++++++++++++++++ packages/jest-resolve/src/defaultResolver.ts | 37 +++++++++++++++++-- yarn.lock | 8 ++++ 10 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 packages/jest-resolve/src/__mocks__/imports/foo/internal.cjs create mode 100644 packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/main.js create mode 100644 packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/package.json create mode 100644 packages/jest-resolve/src/__mocks__/imports/foo/package.json create mode 100644 packages/jest-resolve/src/__mocks__/imports/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af6cc2ac6ad..72210a4c8d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-resolve]` Support subpath imports - `[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/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/internal.cjs b/packages/jest-resolve/src/__mocks__/imports/foo/internal.cjs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/main.js b/packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/main.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/package.json b/packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/package.json new file mode 100644 index 000000000000..c52a84e69b16 --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/imports/foo/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/package.json b/packages/jest-resolve/src/__mocks__/imports/foo/package.json new file mode 100644 index 000000000000..0f33e072b91f --- /dev/null +++ b/packages/jest-resolve/src/__mocks__/imports/foo/package.json @@ -0,0 +1,9 @@ +{ + "name": "foo", + "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..c3e5ce7cdebe 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -316,6 +316,41 @@ 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/index.cjs'), + conditions: ['require'], + }); + + expect(result).toEqual(path.resolve(importsRoot, './foo/internal.cjs')); + }); + + test('supports external reference', () => { + const result = Resolver.findNodeModule('#nested', { + basedir: path.resolve(importsRoot, './foo/index.js'), + conditions: [], + }); + + expect(result).toEqual( + path.resolve(importsRoot, './foo/node_modules/external-foo/main.js'), + ); + }); + + test('fails for non-existent mapping', () => { + expect(() => { + Resolver.findNodeModule('#something-else', { + basedir: path.resolve(importsRoot, './foo/index.js'), + conditions: [], + // this is not the default behaviour (which is to return `null`) + throwIfNotFound: true, + }); + }).toThrow('Missing "#something-else" import in "foo" 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/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 From a1222780f0e346d07043b09879ab5bc9c09aee23 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 1 Jan 2023 12:20:47 +0100 Subject: [PATCH 2/5] link in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72210a4c8d19..2611f1bbae31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[jest-resolve]` Support subpath imports +- `[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)) From 658c944e1819dbd72b8b4b3d8cba554bf5b3184a Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 1 Jan 2023 12:23:28 +0100 Subject: [PATCH 3/5] always throw for internal imports --- packages/jest-resolve/src/__tests__/resolve.test.ts | 2 -- packages/jest-resolve/src/resolver.ts | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index c3e5ce7cdebe..20e0a2b18cbb 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -345,8 +345,6 @@ describe('findNodeModule', () => { Resolver.findNodeModule('#something-else', { basedir: path.resolve(importsRoot, './foo/index.js'), conditions: [], - // this is not the default behaviour (which is to return `null`) - throwIfNotFound: true, }); }).toThrow('Missing "#something-else" import in "foo" package'); }); 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; } } From 0f0c1175f4dffc8cb4a326890a717420710fa5c7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 1 Jan 2023 12:28:11 +0100 Subject: [PATCH 4/5] update snaps --- e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap | 4 ++-- e2e/__tests__/__snapshots__/requireMissingExt.test.ts.snap | 2 +- .../__snapshots__/resolveNoFileExtensions.test.ts.snap | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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)" `; From 630cbb6261069666beb759c3a38558614dd3015e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 1 Jan 2023 12:29:48 +0100 Subject: [PATCH 5/5] avoid name clash --- .../imports/{foo => foo-import}/internal.cjs | 0 .../node_modules/external-foo/main.js | 0 .../node_modules/external-foo/package.json | 0 .../imports/{foo => foo-import}/package.json | 2 +- .../jest-resolve/src/__tests__/resolve.test.ts | 17 +++++++++++------ 5 files changed, 12 insertions(+), 7 deletions(-) rename packages/jest-resolve/src/__mocks__/imports/{foo => foo-import}/internal.cjs (100%) rename packages/jest-resolve/src/__mocks__/imports/{foo => foo-import}/node_modules/external-foo/main.js (100%) rename packages/jest-resolve/src/__mocks__/imports/{foo => foo-import}/node_modules/external-foo/package.json (100%) rename packages/jest-resolve/src/__mocks__/imports/{foo => foo-import}/package.json (82%) diff --git a/packages/jest-resolve/src/__mocks__/imports/foo/internal.cjs b/packages/jest-resolve/src/__mocks__/imports/foo-import/internal.cjs similarity index 100% rename from packages/jest-resolve/src/__mocks__/imports/foo/internal.cjs rename to packages/jest-resolve/src/__mocks__/imports/foo-import/internal.cjs diff --git a/packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/main.js b/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/main.js similarity index 100% rename from packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/main.js rename to packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/main.js diff --git a/packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/package.json b/packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/package.json similarity index 100% rename from packages/jest-resolve/src/__mocks__/imports/foo/node_modules/external-foo/package.json rename to packages/jest-resolve/src/__mocks__/imports/foo-import/node_modules/external-foo/package.json diff --git a/packages/jest-resolve/src/__mocks__/imports/foo/package.json b/packages/jest-resolve/src/__mocks__/imports/foo-import/package.json similarity index 82% rename from packages/jest-resolve/src/__mocks__/imports/foo/package.json rename to packages/jest-resolve/src/__mocks__/imports/foo-import/package.json index 0f33e072b91f..85e20d103e78 100644 --- a/packages/jest-resolve/src/__mocks__/imports/foo/package.json +++ b/packages/jest-resolve/src/__mocks__/imports/foo-import/package.json @@ -1,5 +1,5 @@ { - "name": "foo", + "name": "foo-import", "imports": { "#nested": { "require": "./internal.cjs", diff --git a/packages/jest-resolve/src/__tests__/resolve.test.ts b/packages/jest-resolve/src/__tests__/resolve.test.ts index 20e0a2b18cbb..d9c69dbc13e1 100644 --- a/packages/jest-resolve/src/__tests__/resolve.test.ts +++ b/packages/jest-resolve/src/__tests__/resolve.test.ts @@ -322,31 +322,36 @@ describe('findNodeModule', () => { test('supports internal reference', () => { const result = Resolver.findNodeModule('#nested', { - basedir: path.resolve(importsRoot, './foo/index.cjs'), + basedir: path.resolve(importsRoot, './foo-import/index.cjs'), conditions: ['require'], }); - expect(result).toEqual(path.resolve(importsRoot, './foo/internal.cjs')); + expect(result).toEqual( + path.resolve(importsRoot, './foo-import/internal.cjs'), + ); }); test('supports external reference', () => { const result = Resolver.findNodeModule('#nested', { - basedir: path.resolve(importsRoot, './foo/index.js'), + basedir: path.resolve(importsRoot, './foo-import/index.js'), conditions: [], }); expect(result).toEqual( - path.resolve(importsRoot, './foo/node_modules/external-foo/main.js'), + 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/index.js'), + basedir: path.resolve(importsRoot, './foo-import/index.js'), conditions: [], }); - }).toThrow('Missing "#something-else" import in "foo" package'); + }).toThrow('Missing "#something-else" import in "foo-import" package'); }); }); });