Skip to content

Commit 4c80036

Browse files
authored
Enable path completions for node12/nodenext (microsoft#47836)
* Enable path completions for node12/nodenext * Explicitly pull path completions from export maps when available * Explicitly handle pattern exports by stopping up to the star
1 parent 3945e5c commit 4c80036

File tree

3 files changed

+140
-4
lines changed

3 files changed

+140
-4
lines changed

‎src/services/stringCompletions.ts

+68-4
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,20 @@ namespace ts.Completions.StringCompletions {
368368
}
369369
}
370370

371+
function isEmitResolutionKindUsingNodeModules(compilerOptions: CompilerOptions): boolean {
372+
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ||
373+
getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 ||
374+
getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
375+
}
376+
377+
function isEmitModuleResolutionRespectingExportMaps(compilerOptions: CompilerOptions) {
378+
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 ||
379+
getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
380+
}
381+
371382
function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[][] {
372383
const extensions = getSupportedExtensions(compilerOptions);
373-
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ?
384+
return isEmitResolutionKindUsingNodeModules(compilerOptions) ?
374385
getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) :
375386
extensions;
376387
}
@@ -549,7 +560,7 @@ namespace ts.Completions.StringCompletions {
549560

550561
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
551562

552-
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) {
563+
if (isEmitResolutionKindUsingNodeModules(compilerOptions)) {
553564
// If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
554565
// (But do if we didn't find anything, e.g. 'package.json' missing.)
555566
let foundGlobal = false;
@@ -562,12 +573,65 @@ namespace ts.Completions.StringCompletions {
562573
}
563574
}
564575
if (!foundGlobal) {
565-
forEachAncestorDirectory(scriptPath, ancestor => {
576+
let ancestorLookup: (directory: string) => void | undefined = ancestor => {
566577
const nodeModules = combinePaths(ancestor, "node_modules");
567578
if (tryDirectoryExists(host, nodeModules)) {
568579
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result);
569580
}
570-
});
581+
};
582+
if (fragmentDirectory && isEmitModuleResolutionRespectingExportMaps(compilerOptions)) {
583+
const nodeModulesDirectoryLookup = ancestorLookup;
584+
ancestorLookup = ancestor => {
585+
const components = getPathComponents(fragment);
586+
components.shift(); // shift off empty root
587+
let packagePath = components.shift();
588+
if (!packagePath) {
589+
return nodeModulesDirectoryLookup(ancestor);
590+
}
591+
if (startsWith(packagePath, "@")) {
592+
const subName = components.shift();
593+
if (!subName) {
594+
return nodeModulesDirectoryLookup(ancestor);
595+
}
596+
packagePath = combinePaths(packagePath, subName);
597+
}
598+
const packageFile = combinePaths(ancestor, "node_modules", packagePath, "package.json");
599+
if (tryFileExists(host, packageFile)) {
600+
const packageJson = readJson(packageFile, host as { readFile: (filename: string) => string | undefined });
601+
const exports = (packageJson as any).exports;
602+
if (exports) {
603+
if (typeof exports !== "object" || exports === null) { // eslint-disable-line no-null/no-null
604+
return; // null exports or entrypoint only, no sub-modules available
605+
}
606+
const keys = getOwnKeys(exports);
607+
const fragmentSubpath = components.join("/");
608+
const processedKeys = mapDefined(keys, k => {
609+
if (k === ".") return undefined;
610+
if (!startsWith(k, "./")) return undefined;
611+
const subpath = k.substring(2);
612+
if (!startsWith(subpath, fragmentSubpath)) return undefined;
613+
// subpath is a valid export (barring conditions, which we don't currently check here)
614+
if (!stringContains(subpath, "*")) {
615+
return subpath;
616+
}
617+
// pattern export - only return everything up to the `*`, so the user can autocomplete, then
618+
// keep filling in the pattern (we could speculatively return a list of options by hitting disk,
619+
// but conditions will make that somewhat awkward, as each condition may have a different set of possible
620+
// options for the `*`.
621+
return subpath.slice(0, subpath.indexOf("*"));
622+
});
623+
forEach(processedKeys, k => {
624+
if (k) {
625+
result.push(nameAndKind(k, ScriptElementKind.externalModuleName, /*extension*/ undefined));
626+
}
627+
});
628+
return;
629+
}
630+
}
631+
return nodeModulesDirectoryLookup(ancestor);
632+
};
633+
}
634+
forEachAncestorDirectory(scriptPath, ancestorLookup);
571635
}
572636
}
573637

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[
2+
{
3+
"marker": {
4+
"fileName": "/src/foo.ts",
5+
"position": 30,
6+
"name": ""
7+
},
8+
"completionList": {
9+
"isGlobalCompletion": false,
10+
"isMemberCompletion": false,
11+
"isNewIdentifierLocation": true,
12+
"entries": [
13+
{
14+
"name": "dependency",
15+
"kind": "external module name",
16+
"kindModifiers": "",
17+
"sortText": "11",
18+
"displayParts": [
19+
{
20+
"text": "dependency",
21+
"kind": "text"
22+
}
23+
],
24+
"tags": []
25+
}
26+
]
27+
}
28+
}
29+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/// <reference path="../fourslash.ts" />
2+
3+
// @Filename: /node_modules/dependency/package.json
4+
//// {
5+
//// "type": "module",
6+
//// "name": "dependency",
7+
//// "version": "1.0.0",
8+
//// "exports": {
9+
//// ".": {
10+
//// "types": "./lib/index.d.ts"
11+
//// },
12+
//// "./lol": {
13+
//// "types": "./lib/lol.d.ts"
14+
//// },
15+
//// "./dir/*": "./lib/*"
16+
//// }
17+
//// }
18+
19+
// @Filename: /node_modules/dependency/lib/index.d.ts
20+
//// export function fooFromIndex(): void;
21+
22+
// @Filename: /node_modules/dependency/lib/lol.d.ts
23+
//// export function fooFromLol(): void;
24+
25+
// @Filename: /package.json
26+
//// {
27+
//// "type": "module",
28+
//// "dependencies": {
29+
//// "dependency": "^1.0.0"
30+
//// }
31+
//// }
32+
33+
// @Filename: /tsconfig.json
34+
//// { "compilerOptions": { "module": "nodenext" }, "files": ["./src/foo.ts"] }
35+
36+
// @Filename: /src/foo.ts
37+
//// import { fooFromIndex } from "/**/";
38+
39+
verify.baselineCompletions();
40+
edit.insert("dependency/");
41+
verify.completions({ exact: ["lol", "dir/"], isNewIdentifierLocation: true });
42+
edit.insert("l");
43+
verify.completions({ exact: ["lol"], isNewIdentifierLocation: true });

0 commit comments

Comments
 (0)