Skip to content

Commit 04f4c28

Browse files
authored
Fix #1778: typechecker resolver should take importer's module type -- cjs or esm -- into account when resolving package.json "exports" (#1782)
* Add failing test * fix typo in failing test * fix bug * fix test runIf filter * fix * fix * lint
1 parent b3dd3f2 commit 04f4c28

File tree

13 files changed

+111
-30
lines changed

13 files changed

+111
-30
lines changed

src/resolver-functions.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,24 @@ export function createResolverFunctions(kwargs: {
9595
containingFile: string,
9696
reusedNames: string[] | undefined,
9797
redirectedReference: TSCommon.ResolvedProjectReference | undefined,
98-
optionsOnlyWithNewerTsVersions: TSCommon.CompilerOptions
98+
optionsOnlyWithNewerTsVersions: TSCommon.CompilerOptions,
99+
containingSourceFile?: TSCommon.SourceFile
99100
): (TSCommon.ResolvedModule | undefined)[] => {
100-
return moduleNames.map((moduleName) => {
101+
return moduleNames.map((moduleName, i) => {
102+
const mode = containingSourceFile
103+
? (ts as any as TSInternal).getModeForResolutionAtIndex?.(
104+
containingSourceFile,
105+
i
106+
)
107+
: undefined;
101108
const { resolvedModule } = ts.resolveModuleName(
102109
moduleName,
103110
containingFile,
104111
config.options,
105112
host,
106113
moduleResolutionCache,
107-
redirectedReference
114+
redirectedReference,
115+
mode
108116
);
109117
if (resolvedModule) {
110118
fixupResolvedModule(resolvedModule);
@@ -117,12 +125,14 @@ export function createResolverFunctions(kwargs: {
117125
const getResolvedModuleWithFailedLookupLocationsFromCache: TSCommon.LanguageServiceHost['getResolvedModuleWithFailedLookupLocationsFromCache'] =
118126
(
119127
moduleName,
120-
containingFile
128+
containingFile,
129+
resolutionMode?: TSCommon.ModuleKind.CommonJS | TSCommon.ModuleKind.ESNext
121130
): TSCommon.ResolvedModuleWithFailedLookupLocations | undefined => {
122131
const ret = ts.resolveModuleNameFromCache(
123132
moduleName,
124133
containingFile,
125-
moduleResolutionCache
134+
moduleResolutionCache,
135+
resolutionMode
126136
);
127137
if (ret && ret.resolvedModule) {
128138
fixupResolvedModule(ret.resolvedModule);

src/test/module-node/1778.spec.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { createExec } from '../exec-helpers';
2+
import {
3+
ctxTsNode,
4+
nodeSupportsEsmHooks,
5+
TEST_DIR,
6+
tsSupportsStableNodeNextNode16,
7+
CMD_TS_NODE_WITHOUT_PROJECT_FLAG,
8+
nodeSupportsSpawningChildProcess,
9+
} from '../helpers';
10+
import { context, expect } from '../testlib';
11+
import { join } from 'path';
12+
13+
const exec = createExec({
14+
cwd: TEST_DIR,
15+
});
16+
17+
const test = context(ctxTsNode);
18+
19+
test.suite(
20+
'Issue #1778: typechecker resolver should take importer\'s module type -- cjs or esm -- into account when resolving package.json "exports"',
21+
(test) => {
22+
test.runIf(
23+
nodeSupportsEsmHooks &&
24+
nodeSupportsSpawningChildProcess &&
25+
tsSupportsStableNodeNextNode16
26+
);
27+
test('test', async () => {
28+
const { err, stdout } = await exec(
29+
`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`,
30+
{
31+
cwd: join(TEST_DIR, '1778'),
32+
}
33+
);
34+
expect(err).toBe(null);
35+
expect(stdout).toBe('{ esm: true }\n');
36+
});
37+
}
38+
);

src/test/module-node.spec.ts renamed to src/test/module-node/module-node.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { expect, context } from './testlib';
1+
import { expect, context } from '../testlib';
22
import {
33
CMD_TS_NODE_WITHOUT_PROJECT_FLAG,
44
isOneOf,
55
nodeSupportsImportingTransformedCjsFromEsm,
66
resetNodeEnvironment,
77
tsSupportsStableNodeNextNode16,
8-
} from './helpers';
8+
} from '../helpers';
99
import * as Path from 'path';
10-
import { ctxTsNode } from './helpers';
11-
import { exec } from './exec-helpers';
12-
import { file, project, ProjectAPI as ProjectAPI } from './fs-helpers';
10+
import { ctxTsNode } from '../helpers';
11+
import { exec } from '../exec-helpers';
12+
import { file, project, ProjectAPI as ProjectAPI } from '../fs-helpers';
1313

1414
const test = context(ctxTsNode);
1515
test.beforeEach(async () => {

src/ts-compiler-types.ts

+17-20
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,7 @@ export interface TSCommon {
3232
createModuleResolutionCache: typeof _ts.createModuleResolutionCache;
3333
resolveModuleName: typeof _ts.resolveModuleName;
3434
resolveModuleNameFromCache: typeof _ts.resolveModuleNameFromCache;
35-
// Changed in TS 4.7
36-
resolveTypeReferenceDirective(
37-
typeReferenceDirectiveName: string,
38-
containingFile: string | undefined,
39-
options: _ts.CompilerOptions,
40-
host: _ts.ModuleResolutionHost,
41-
redirectedReference?: _ts.ResolvedProjectReference,
42-
cache?: _ts.TypeReferenceDirectiveResolutionCache,
43-
resolutionMode?: _ts.SourceFile['impliedNodeFormat']
44-
): _ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations;
35+
resolveTypeReferenceDirective: typeof _ts.resolveTypeReferenceDirective;
4536
createIncrementalCompilerHost: typeof _ts.createIncrementalCompilerHost;
4637
createSourceFile: typeof _ts.createSourceFile;
4738
getDefaultLibFileName: typeof _ts.getDefaultLibFileName;
@@ -52,16 +43,7 @@ export interface TSCommon {
5243
ModuleResolutionKind: typeof _ts.ModuleResolutionKind;
5344
}
5445
export namespace TSCommon {
55-
export interface LanguageServiceHost extends _ts.LanguageServiceHost {
56-
// Modified in 4.7
57-
resolveTypeReferenceDirectives?(
58-
typeDirectiveNames: string[] | _ts.FileReference[],
59-
containingFile: string,
60-
redirectedReference: _ts.ResolvedProjectReference | undefined,
61-
options: _ts.CompilerOptions,
62-
containingFileMode?: _ts.SourceFile['impliedNodeFormat'] | undefined
63-
): (_ts.ResolvedTypeReferenceDirective | undefined)[];
64-
}
46+
export interface LanguageServiceHost extends _ts.LanguageServiceHost {}
6547
export type ModuleResolutionHost = _ts.ModuleResolutionHost;
6648
export type ParsedCommandLine = _ts.ParsedCommandLine;
6749
export type ResolvedModule = _ts.ResolvedModule;
@@ -79,6 +61,12 @@ export namespace TSCommon {
7961
? typeof _ts.ModuleKind['Node16']
8062
: 100;
8163
};
64+
// Can't figure out how to re-export an enum
65+
// `export import ... =` complains that _ts is type-only import
66+
export namespace ModuleKind {
67+
export type CommonJS = _ts.ModuleKind.CommonJS;
68+
export type ESNext = _ts.ModuleKind.ESNext;
69+
}
8270
}
8371

8472
/**
@@ -129,6 +117,11 @@ export interface TSInternal {
129117
basePath: string,
130118
usage: 'files' | 'directories' | 'exclude'
131119
): string | undefined;
120+
// Added in TS 4.7
121+
getModeForResolutionAtIndex?(
122+
file: TSInternal.SourceFileImportsList,
123+
index: number
124+
): _ts.SourceFile['impliedNodeFormat'];
132125
}
133126
/** @internal */
134127
export namespace TSInternal {
@@ -139,4 +132,8 @@ export namespace TSInternal {
139132
getCurrentDirectory(): string;
140133
useCaseSensitiveFileNames: boolean;
141134
}
135+
// Note: is only a partial declaration, TS sources declare other fields
136+
export interface SourceFileImportsList {
137+
impliedNodeFormat?: TSCommon.SourceFile['impliedNodeFormat'];
138+
}
142139
}

tests/1778/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import foo from 'foo';
2+
3+
// This file is ESM, so if typechecker's resolver is working correctly, will
4+
// resolve to the foo's package.json "exports" mapping for "default", not "require"
5+
const bar: { esm: true } = foo;
6+
console.log(bar);

tests/1778/node_modules/foo/cjs/index.d.ts

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/1778/node_modules/foo/cjs/index.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/1778/node_modules/foo/cjs/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/1778/node_modules/foo/esm/index.d.ts

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/1778/node_modules/foo/esm/index.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/1778/node_modules/foo/package.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/1778/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}

tests/1778/tsconfig.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"ts-node": {
3+
"esm": true
4+
},
5+
"compilerOptions": {
6+
"module": "NodeNext",
7+
"noEmit": true
8+
}
9+
}

0 commit comments

Comments
 (0)