diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index 9884c29c8..afe13b463 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -95,16 +95,24 @@ export function createResolverFunctions(kwargs: { containingFile: string, reusedNames: string[] | undefined, redirectedReference: TSCommon.ResolvedProjectReference | undefined, - optionsOnlyWithNewerTsVersions: TSCommon.CompilerOptions + optionsOnlyWithNewerTsVersions: TSCommon.CompilerOptions, + containingSourceFile?: TSCommon.SourceFile ): (TSCommon.ResolvedModule | undefined)[] => { - return moduleNames.map((moduleName) => { + return moduleNames.map((moduleName, i) => { + const mode = containingSourceFile + ? (ts as any as TSInternal).getModeForResolutionAtIndex?.( + containingSourceFile, + i + ) + : undefined; const { resolvedModule } = ts.resolveModuleName( moduleName, containingFile, config.options, host, moduleResolutionCache, - redirectedReference + redirectedReference, + mode ); if (resolvedModule) { fixupResolvedModule(resolvedModule); @@ -117,12 +125,14 @@ export function createResolverFunctions(kwargs: { const getResolvedModuleWithFailedLookupLocationsFromCache: TSCommon.LanguageServiceHost['getResolvedModuleWithFailedLookupLocationsFromCache'] = ( moduleName, - containingFile + containingFile, + resolutionMode?: TSCommon.ModuleKind.CommonJS | TSCommon.ModuleKind.ESNext ): TSCommon.ResolvedModuleWithFailedLookupLocations | undefined => { const ret = ts.resolveModuleNameFromCache( moduleName, containingFile, - moduleResolutionCache + moduleResolutionCache, + resolutionMode ); if (ret && ret.resolvedModule) { fixupResolvedModule(ret.resolvedModule); diff --git a/src/test/module-node/1778.spec.ts b/src/test/module-node/1778.spec.ts new file mode 100644 index 000000000..43d65e0f7 --- /dev/null +++ b/src/test/module-node/1778.spec.ts @@ -0,0 +1,38 @@ +import { createExec } from '../exec-helpers'; +import { + ctxTsNode, + nodeSupportsEsmHooks, + TEST_DIR, + tsSupportsStableNodeNextNode16, + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, + nodeSupportsSpawningChildProcess, +} from '../helpers'; +import { context, expect } from '../testlib'; +import { join } from 'path'; + +const exec = createExec({ + cwd: TEST_DIR, +}); + +const test = context(ctxTsNode); + +test.suite( + 'Issue #1778: typechecker resolver should take importer\'s module type -- cjs or esm -- into account when resolving package.json "exports"', + (test) => { + test.runIf( + nodeSupportsEsmHooks && + nodeSupportsSpawningChildProcess && + tsSupportsStableNodeNextNode16 + ); + test('test', async () => { + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`, + { + cwd: join(TEST_DIR, '1778'), + } + ); + expect(err).toBe(null); + expect(stdout).toBe('{ esm: true }\n'); + }); + } +); diff --git a/src/test/module-node.spec.ts b/src/test/module-node/module-node.spec.ts similarity index 98% rename from src/test/module-node.spec.ts rename to src/test/module-node/module-node.spec.ts index 043dc36a4..7ebb3fd4c 100644 --- a/src/test/module-node.spec.ts +++ b/src/test/module-node/module-node.spec.ts @@ -1,15 +1,15 @@ -import { expect, context } from './testlib'; +import { expect, context } from '../testlib'; import { CMD_TS_NODE_WITHOUT_PROJECT_FLAG, isOneOf, nodeSupportsImportingTransformedCjsFromEsm, resetNodeEnvironment, tsSupportsStableNodeNextNode16, -} from './helpers'; +} from '../helpers'; import * as Path from 'path'; -import { ctxTsNode } from './helpers'; -import { exec } from './exec-helpers'; -import { file, project, ProjectAPI as ProjectAPI } from './fs-helpers'; +import { ctxTsNode } from '../helpers'; +import { exec } from '../exec-helpers'; +import { file, project, ProjectAPI as ProjectAPI } from '../fs-helpers'; const test = context(ctxTsNode); test.beforeEach(async () => { diff --git a/src/ts-compiler-types.ts b/src/ts-compiler-types.ts index 2f961b853..9077d4f99 100644 --- a/src/ts-compiler-types.ts +++ b/src/ts-compiler-types.ts @@ -32,16 +32,7 @@ export interface TSCommon { createModuleResolutionCache: typeof _ts.createModuleResolutionCache; resolveModuleName: typeof _ts.resolveModuleName; resolveModuleNameFromCache: typeof _ts.resolveModuleNameFromCache; - // Changed in TS 4.7 - resolveTypeReferenceDirective( - typeReferenceDirectiveName: string, - containingFile: string | undefined, - options: _ts.CompilerOptions, - host: _ts.ModuleResolutionHost, - redirectedReference?: _ts.ResolvedProjectReference, - cache?: _ts.TypeReferenceDirectiveResolutionCache, - resolutionMode?: _ts.SourceFile['impliedNodeFormat'] - ): _ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations; + resolveTypeReferenceDirective: typeof _ts.resolveTypeReferenceDirective; createIncrementalCompilerHost: typeof _ts.createIncrementalCompilerHost; createSourceFile: typeof _ts.createSourceFile; getDefaultLibFileName: typeof _ts.getDefaultLibFileName; @@ -52,16 +43,7 @@ export interface TSCommon { ModuleResolutionKind: typeof _ts.ModuleResolutionKind; } export namespace TSCommon { - export interface LanguageServiceHost extends _ts.LanguageServiceHost { - // Modified in 4.7 - resolveTypeReferenceDirectives?( - typeDirectiveNames: string[] | _ts.FileReference[], - containingFile: string, - redirectedReference: _ts.ResolvedProjectReference | undefined, - options: _ts.CompilerOptions, - containingFileMode?: _ts.SourceFile['impliedNodeFormat'] | undefined - ): (_ts.ResolvedTypeReferenceDirective | undefined)[]; - } + export interface LanguageServiceHost extends _ts.LanguageServiceHost {} export type ModuleResolutionHost = _ts.ModuleResolutionHost; export type ParsedCommandLine = _ts.ParsedCommandLine; export type ResolvedModule = _ts.ResolvedModule; @@ -79,6 +61,12 @@ export namespace TSCommon { ? typeof _ts.ModuleKind['Node16'] : 100; }; + // Can't figure out how to re-export an enum + // `export import ... =` complains that _ts is type-only import + export namespace ModuleKind { + export type CommonJS = _ts.ModuleKind.CommonJS; + export type ESNext = _ts.ModuleKind.ESNext; + } } /** @@ -129,6 +117,11 @@ export interface TSInternal { basePath: string, usage: 'files' | 'directories' | 'exclude' ): string | undefined; + // Added in TS 4.7 + getModeForResolutionAtIndex?( + file: TSInternal.SourceFileImportsList, + index: number + ): _ts.SourceFile['impliedNodeFormat']; } /** @internal */ export namespace TSInternal { @@ -139,4 +132,8 @@ export namespace TSInternal { getCurrentDirectory(): string; useCaseSensitiveFileNames: boolean; } + // Note: is only a partial declaration, TS sources declare other fields + export interface SourceFileImportsList { + impliedNodeFormat?: TSCommon.SourceFile['impliedNodeFormat']; + } } diff --git a/tests/1778/index.ts b/tests/1778/index.ts new file mode 100644 index 000000000..ace4327b7 --- /dev/null +++ b/tests/1778/index.ts @@ -0,0 +1,6 @@ +import foo from 'foo'; + +// This file is ESM, so if typechecker's resolver is working correctly, will +// resolve to the foo's package.json "exports" mapping for "default", not "require" +const bar: { esm: true } = foo; +console.log(bar); diff --git a/tests/1778/node_modules/foo/cjs/index.d.ts b/tests/1778/node_modules/foo/cjs/index.d.ts new file mode 100644 index 000000000..a84bc1bc3 --- /dev/null +++ b/tests/1778/node_modules/foo/cjs/index.d.ts @@ -0,0 +1,2 @@ +declare const foo: {cjs: true} +export default foo diff --git a/tests/1778/node_modules/foo/cjs/index.js b/tests/1778/node_modules/foo/cjs/index.js new file mode 100644 index 000000000..9a4be3c24 --- /dev/null +++ b/tests/1778/node_modules/foo/cjs/index.js @@ -0,0 +1 @@ +module.exports = {cjs: true} diff --git a/tests/1778/node_modules/foo/cjs/package.json b/tests/1778/node_modules/foo/cjs/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/1778/node_modules/foo/cjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/1778/node_modules/foo/esm/index.d.ts b/tests/1778/node_modules/foo/esm/index.d.ts new file mode 100644 index 000000000..1da8fb3af --- /dev/null +++ b/tests/1778/node_modules/foo/esm/index.d.ts @@ -0,0 +1,2 @@ +declare const foo: {esm: true} +export default foo diff --git a/tests/1778/node_modules/foo/esm/index.js b/tests/1778/node_modules/foo/esm/index.js new file mode 100644 index 000000000..2b573c6c7 --- /dev/null +++ b/tests/1778/node_modules/foo/esm/index.js @@ -0,0 +1 @@ +export default {esm: true} diff --git a/tests/1778/node_modules/foo/package.json b/tests/1778/node_modules/foo/package.json new file mode 100644 index 000000000..e12814a53 --- /dev/null +++ b/tests/1778/node_modules/foo/package.json @@ -0,0 +1,9 @@ +{ + "type": "module", + "exports": { + ".": { + "require": "./cjs/index.js", + "default": "./esm/index.js" + } + } +} diff --git a/tests/1778/package.json b/tests/1778/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/1778/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/1778/tsconfig.json b/tests/1778/tsconfig.json new file mode 100644 index 000000000..1ec416b41 --- /dev/null +++ b/tests/1778/tsconfig.json @@ -0,0 +1,9 @@ +{ + "ts-node": { + "esm": true + }, + "compilerOptions": { + "module": "NodeNext", + "noEmit": true + } +}