Resolve correct link path for tsconfig.extends (#141062)

* fix(ts-features-extension): resolve correct path for `extends` of tsconfig
fixes #131643

* always provide link. add command to resolve the link path on click

* cleanup
just make the code cleaner

* revert `yarn.lock` changes

* pretending eslint

* use `vscode.open`

* don't add `.json` to path if it's already here
this change better conforms the TS resolving algorithm (see the reference)

* style: move `resolveNodeModulesPath` to top level

* don't show falsy errors on absolute paths

* improve resolveNodeModulesPath impl
- fixed a bug with infinite loop
- check for module existence once per level
This commit is contained in:
Vitaly 2022-02-03 20:37:00 +03:00 committed by GitHub
parent 6e81cb0464
commit c134702cc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 95 additions and 14 deletions

View file

@ -37,10 +37,11 @@
"Programming Languages"
],
"dependencies": {
"@vscode/extension-telemetry": "0.4.6",
"jsonc-parser": "^2.2.1",
"semver": "5.5.1",
"@vscode/extension-telemetry": "0.4.6",
"vscode-nls": "^5.0.0"
"vscode-nls": "^5.0.0",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
"@types/node": "14.x",

View file

@ -4,9 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import * as jsonc from 'jsonc-parser';
import { basename, dirname, join } from 'path';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import { basename, dirname, join, posix } from 'path';
import { coalesce, flatten } from '../utils/arrays';
import { exists } from '../utils/fs';
import { Utils } from 'vscode-uri';
function mapChildren<R>(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] {
return node && node.type === 'array' && node.children
@ -14,6 +17,14 @@ function mapChildren<R>(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R):
: [];
}
const openExtendsLinkCommandId = '_typescript.openExtendsLink';
type OpenExtendsLinkCommandArgs = {
resourceUri: vscode.Uri
extendsValue: string
};
const localize = nls.loadMessageBundle();
class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
public provideDocumentLinks(
@ -38,21 +49,18 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
return undefined;
}
if (extendsNode.value.startsWith('.')) {
return new vscode.DocumentLink(
this.getRange(document, extendsNode),
vscode.Uri.file(join(dirname(document.uri.fsPath), extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json')))
);
}
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!workspaceFolder) {
const extendsValue: string = extendsNode.value;
if (extendsValue.startsWith('/')) {
return undefined;
}
const args: OpenExtendsLinkCommandArgs = {
resourceUri: document.uri,
extendsValue: extendsValue
};
return new vscode.DocumentLink(
this.getRange(document, extendsNode),
vscode.Uri.joinPath(workspaceFolder.uri, 'node_modules', extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json'))
vscode.Uri.parse(`command:${openExtendsLinkCommandId}?${JSON.stringify(args)}`)
);
}
@ -110,6 +118,67 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
}
}
const resolveNodeModulesPath = async (baseDirUri: vscode.Uri, pathCandidates: string[]): Promise<vscode.Uri | undefined> => {
let currentUri = baseDirUri;
const baseCandidate = pathCandidates[0];
const sepIndex = baseCandidate.startsWith('@') ? 2 : 1;
const moduleBasePath = baseCandidate.split(posix.sep).slice(0, sepIndex).join(posix.sep);
while (true) {
const moduleAbsoluteUrl = vscode.Uri.joinPath(currentUri, 'node_modules', moduleBasePath);
let moduleStat: vscode.FileStat | undefined;
try {
moduleStat = await vscode.workspace.fs.stat(moduleAbsoluteUrl);
} catch (err) { }
if (moduleStat && (moduleStat.type & vscode.FileType.Directory)) {
for (const uriCandidate of pathCandidates
.map((relativePath) => relativePath.split(posix.sep).slice(sepIndex).join(posix.sep))
// skip empty paths within module
.filter(Boolean)
.map((relativeModulePath) => vscode.Uri.joinPath(moduleAbsoluteUrl, relativeModulePath))
) {
if (await exists(uriCandidate)) {
return uriCandidate;
}
}
// Continue to loocking for potentially another version
}
const oldUri = currentUri;
currentUri = vscode.Uri.joinPath(currentUri, '..');
// Can't go next. Reached the system root
if (oldUri.path === currentUri.path) {
return;
}
}
};
// Reference: https://github.com/microsoft/TypeScript/blob/febfd442cdba343771f478cf433b0892f213ad2f/src/compiler/commandLineParser.ts#L3005
/**
* @returns Returns undefined in case of lack of result while trying to resolve from node_modules
*/
const getTsconfigPath = async (baseDirUri: vscode.Uri, extendsValue: string): Promise<vscode.Uri | undefined> => {
// Don't take into account a case, where tsconfig might be resolved from the root (see the reference)
// e.g. C:/projects/shared-tsconfig/tsconfig.json (note that C: prefix is optional)
const isRelativePath = ['./', '../'].some(str => extendsValue.startsWith(str));
if (isRelativePath) {
const absolutePath = vscode.Uri.joinPath(baseDirUri, extendsValue);
if (await exists(absolutePath) || absolutePath.path.endsWith('.json')) {
return absolutePath;
}
return absolutePath.with({
path: `${absolutePath.path}.json`
});
}
// Otherwise resolve like a module
return resolveNodeModulesPath(baseDirUri, [
extendsValue,
...extendsValue.endsWith('.json') ? [] : [`${extendsValue}.json`],
`${extendsValue}/tsconfig.json`,
]);
};
export function register() {
const patterns: vscode.GlobPattern[] = [
'**/[jt]sconfig.json',
@ -122,5 +191,16 @@ export function register() {
languages.map(language =>
patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern }))));
return vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider());
return vscode.Disposable.from(
vscode.commands.registerCommand(openExtendsLinkCommandId, async ({ resourceUri, extendsValue, }: OpenExtendsLinkCommandArgs) => {
const tsconfigPath = await getTsconfigPath(Utils.dirname(resourceUri), extendsValue);
if (tsconfigPath === undefined) {
vscode.window.showErrorMessage(localize('openTsconfigExtendsModuleFail', "Failed to resolve {0} as module", extendsValue));
return;
}
// Will suggest to create a .json variant if it doesn't exist yet (but only for relative paths)
await vscode.commands.executeCommand('vscode.open', tsconfigPath);
}),
vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider()),
);
}