feat(linter): support path wildcards in enforce-module-boundaries autofix (#18316)

This commit is contained in:
Igor Loskutov 2023-07-31 21:18:00 +07:00 committed by GitHub
parent dcefa4a6ad
commit 9c7ded0b1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 57 additions and 6 deletions

View File

@ -332,7 +332,7 @@ export default createESLintRule<Options, MessageIds>({
fix(fixer) {
// imp has form of @myorg/someproject/some/path
const indexTsPaths = getBarrelEntryPointByImportScope(imp);
if (indexTsPaths && indexTsPaths.length > 0) {
if (indexTsPaths.length > 0) {
const specifiers = (node as any).specifiers;
if (!specifiers || specifiers.length === 0) {
return;
@ -359,13 +359,22 @@ export default createESLintRule<Options, MessageIds>({
dirname(importPath)
);
// the string we receive from elsewhere might not have a leading './' here despite still being a relative path
// we'd like to ensure it's a normalized relative form starting from ./ or ../
const ensureRelativeForm = (path: string): string =>
path.startsWith('./') || path.startsWith('../')
? path
: `./${path}`;
// if the string is empty, it's the current file
const importPathResolved =
relativePath === ''
? `./${basename(importPath)}`
: joinPathFragments(
relativePath,
basename(importPath)
: ensureRelativeForm(
joinPathFragments(
relativePath,
basename(importPath)
)
);
importsToRemap.push({

View File

@ -27,9 +27,38 @@ function tryReadBaseJson() {
*/
export function getBarrelEntryPointByImportScope(
importScope: string
): string[] | null {
): string[] {
const tryPaths = (
paths: Record<string, string[]>,
importScope: string
): string[] => {
// TODO check and warn that the entries of paths[importScope] have no wildcards; that'd be user misconfiguration
if (paths[importScope]) return paths[importScope];
// accommodate wildcards (it's not glob) https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
const result = new Set<string>(); // set ensures there are no duplicates
for (const [alias, targets] of Object.entries(paths)) {
if (!alias.endsWith('*')) {
continue;
}
const strippedAlias = alias.slice(0, -1); // remove asterisk
if (!importScope.startsWith(strippedAlias)) {
continue;
}
const dynamicPart = importScope.slice(strippedAlias.length);
targets.forEach((target) => {
result.add(target.replace('*', dynamicPart)); // add interpolated value
});
// we found the entry for importScope; an import scope not supposed and has no sense having > 1 Aliases; TODO warn on duplicated entries
break;
}
return Array.from(result);
};
const tsConfigBase = tryReadBaseJson();
return tsConfigBase?.compilerOptions?.paths[importScope] || null;
if (!tsConfigBase?.compilerOptions?.paths) return [];
return tryPaths(tsConfigBase.compilerOptions.paths, importScope);
}
export function getBarrelEntryPointProjectNode(
@ -90,7 +119,20 @@ export function getRelativeImportPath(exportedMember, filePath, basePath) {
} else {
return;
}
} else if (
!lstatSync(filePath, {
throwIfNoEntry: false,
}) /*not folder, but probably not full file with an extension either*/
) {
// try to find an extension that exists
const ext = ['.ts', '.tsx', '.js', '.jsx'].find((ext) =>
lstatSync(filePath + ext, { throwIfNoEntry: false })
);
if (ext) {
filePath += ext;
}
}
const fileContent = readFileSync(filePath, 'utf8');
// use the TypeScript AST to find the path to the file where exportedMember is defined