fix(core): update resolution of ts path mappings with patterns in target project locator (#30533)
## Current Behavior The `TargetProjectLocator` logic for matching TS path mapping patterns is incorrect and doesn't handle a few scenarios. ## Expected Behavior The `TargetProjectLocator` logic for matching TS path mapping patterns should match the TS resolution and handle all valid scenarios. ## Related Issue(s) Fixes #30172
This commit is contained in:
parent
9b84926d0b
commit
b911ddbdac
@ -80,6 +80,9 @@ describe('TargetProjectLocator', () => {
|
||||
'@proj/proj1234-child/*': ['libs/proj1234-child/*'],
|
||||
'#hash-path': ['libs/hash-project/src/index.ts'],
|
||||
'parent-path/*': ['libs/parent-path/*'],
|
||||
'@proj/feature-*': ['libs/features/*'],
|
||||
'@proj/*/utils': ['libs/scope/*/utils'],
|
||||
'@proj/*-util': ['libs/utils/*'],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -227,6 +230,27 @@ describe('TargetProjectLocator', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
name: 'users',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/features/users',
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
name: 'admin',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/scope/admin',
|
||||
},
|
||||
},
|
||||
'file-system': {
|
||||
name: 'file-system',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/utils/file-system',
|
||||
},
|
||||
},
|
||||
};
|
||||
npmProjects = {
|
||||
'npm:@ng/core': {
|
||||
@ -413,19 +437,33 @@ describe('TargetProjectLocator', () => {
|
||||
});
|
||||
|
||||
it('should be able to resolve wildcard paths', () => {
|
||||
const parentProject = targetProjectLocator.findProjectFromImport(
|
||||
'parent-path',
|
||||
'libs/proj1/index.ts'
|
||||
);
|
||||
|
||||
expect(parentProject).toEqual('parent-project');
|
||||
|
||||
// 'parent-path/*': ['libs/parent-path/*'] => 'libs/parent-path/child-path'
|
||||
const childProject = targetProjectLocator.findProjectFromImport(
|
||||
'parent-path/child-path',
|
||||
'libs/proj1/index.ts'
|
||||
);
|
||||
|
||||
expect(childProject).toEqual('child-project');
|
||||
|
||||
// '@proj/feature-*': ['libs/features/*'] => 'libs/features/users'
|
||||
const usersProject = targetProjectLocator.findProjectFromImport(
|
||||
'@proj/feature-users',
|
||||
'libs/proj1/index.ts'
|
||||
);
|
||||
expect(usersProject).toEqual('users');
|
||||
|
||||
// '@proj/*/utils': ['libs/scope/*/utils'] => 'libs/scope/admin/utils'
|
||||
const adminProject = targetProjectLocator.findProjectFromImport(
|
||||
'@proj/admin/utils',
|
||||
'libs/proj1/index.ts'
|
||||
);
|
||||
expect(adminProject).toEqual('admin');
|
||||
|
||||
// '@proj/*-util': ['libs/utils/*'] => 'libs/utils/file-system'
|
||||
const fileSystemProject = targetProjectLocator.findProjectFromImport(
|
||||
'@proj/file-system-util',
|
||||
'libs/proj1/index.ts'
|
||||
);
|
||||
expect(fileSystemProject).toEqual('file-system');
|
||||
});
|
||||
|
||||
it('should be able to resolve paths that start with a #', () => {
|
||||
|
||||
@ -30,6 +30,16 @@ import {
|
||||
*/
|
||||
type NpmResolutionCache = Map<string, string | null>;
|
||||
|
||||
type PathPattern = {
|
||||
pattern: string;
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
};
|
||||
type ParsedPatterns = {
|
||||
matchableStrings: Set<string> | undefined;
|
||||
patterns: PathPattern[] | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use a shared cache to avoid repeated npm package resolution work within the TargetProjectLocator.
|
||||
*/
|
||||
@ -47,6 +57,7 @@ export class TargetProjectLocator {
|
||||
private npmProjects: Record<string, ProjectGraphExternalNode | null>;
|
||||
private tsConfig = this.getRootTsConfig();
|
||||
private paths = this.tsConfig.config?.compilerOptions?.paths;
|
||||
private parsedPathPatterns: ParsedPatterns | undefined;
|
||||
private typescriptResolutionCache = new Map<string, string | null>();
|
||||
private packagesMetadata: {
|
||||
entryPointsToProjectMap: Record<string, ProjectGraphProjectNode>;
|
||||
@ -81,6 +92,10 @@ export class TargetProjectLocator {
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, ProjectGraphExternalNode>);
|
||||
|
||||
if (this.tsConfig.config?.compilerOptions?.paths) {
|
||||
this.parsePaths(this.tsConfig.config.compilerOptions.paths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,14 +112,19 @@ export class TargetProjectLocator {
|
||||
}
|
||||
|
||||
// find project using tsconfig paths
|
||||
const results = this.findPaths(importExpr);
|
||||
const results = this.findMatchingPaths(importExpr);
|
||||
if (results) {
|
||||
const [path, paths] = results;
|
||||
const matchedStar =
|
||||
typeof path === 'string'
|
||||
? undefined
|
||||
: importExpr.substring(
|
||||
path.prefix.length,
|
||||
importExpr.length - path.suffix.length
|
||||
);
|
||||
for (let p of paths) {
|
||||
const r = p.endsWith('/*')
|
||||
? join(dirname(p), relative(path.replace(/\*$/, ''), importExpr))
|
||||
: p;
|
||||
const maybeResolvedProject = this.findProjectOfResolvedModule(r);
|
||||
const path = matchedStar ? p.replace('*', matchedStar) : p;
|
||||
const maybeResolvedProject = this.findProjectOfResolvedModule(path);
|
||||
if (maybeResolvedProject) {
|
||||
return maybeResolvedProject;
|
||||
}
|
||||
@ -237,7 +257,7 @@ export class TargetProjectLocator {
|
||||
/**
|
||||
* Return file paths matching the import relative to the repo root
|
||||
* @param normalizedImportExpr
|
||||
* @returns
|
||||
* @deprecated Use `findMatchingPaths` instead. It will be removed in Nx v22.
|
||||
*/
|
||||
findPaths(normalizedImportExpr: string): string[] | undefined {
|
||||
if (!this.paths) {
|
||||
@ -258,6 +278,37 @@ export class TargetProjectLocator {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
findMatchingPaths(
|
||||
importExpr: string
|
||||
): [pattern: string | PathPattern, paths: string[]] | undefined {
|
||||
if (!this.parsedPathPatterns) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { matchableStrings, patterns } = this.parsedPathPatterns;
|
||||
if (matchableStrings.has(importExpr)) {
|
||||
return [importExpr, this.paths[importExpr]];
|
||||
}
|
||||
|
||||
// https://github.com/microsoft/TypeScript/blob/29e6d6689dfb422e4f1395546c1917d07e1f664d/src/compiler/core.ts#L2410
|
||||
let matchedValue: PathPattern | undefined;
|
||||
let longestMatchPrefixLength = -1;
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
const pattern = patterns[i];
|
||||
if (
|
||||
pattern.prefix.length > longestMatchPrefixLength &&
|
||||
this.isPatternMatch(pattern, importExpr)
|
||||
) {
|
||||
longestMatchPrefixLength = pattern.prefix.length;
|
||||
matchedValue = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
return matchedValue
|
||||
? [matchedValue, this.paths[matchedValue.pattern]]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
findImportInWorkspaceProjects(importPath: string): string | null {
|
||||
this.packagesMetadata ??= getWorkspacePackagesMetadata(this.nodes);
|
||||
|
||||
@ -279,6 +330,40 @@ export class TargetProjectLocator {
|
||||
return this.packagesMetadata.packageToProjectMap[dep]?.name;
|
||||
}
|
||||
|
||||
private isPatternMatch(
|
||||
{ prefix, suffix }: PathPattern,
|
||||
candidate: string
|
||||
): boolean {
|
||||
return (
|
||||
candidate.length >= prefix.length + suffix.length &&
|
||||
candidate.startsWith(prefix) &&
|
||||
candidate.endsWith(suffix)
|
||||
);
|
||||
}
|
||||
|
||||
private parsePaths(paths: Record<string, string>): void {
|
||||
this.parsedPathPatterns = {
|
||||
matchableStrings: new Set(),
|
||||
patterns: [],
|
||||
};
|
||||
|
||||
for (const key of Object.keys(paths)) {
|
||||
const parts = key.split('*');
|
||||
if (parts.length > 2) {
|
||||
continue;
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
this.parsedPathPatterns.matchableStrings.add(key);
|
||||
continue;
|
||||
}
|
||||
this.parsedPathPatterns.patterns.push({
|
||||
pattern: key,
|
||||
prefix: parts[0],
|
||||
suffix: parts[1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private resolveImportWithTypescript(
|
||||
normalizedImportExpr: string,
|
||||
filePath: string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user