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/*'],
|
'@proj/proj1234-child/*': ['libs/proj1234-child/*'],
|
||||||
'#hash-path': ['libs/hash-project/src/index.ts'],
|
'#hash-path': ['libs/hash-project/src/index.ts'],
|
||||||
'parent-path/*': ['libs/parent-path/*'],
|
'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 = {
|
npmProjects = {
|
||||||
'npm:@ng/core': {
|
'npm:@ng/core': {
|
||||||
@ -413,19 +437,33 @@ describe('TargetProjectLocator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to resolve wildcard paths', () => {
|
it('should be able to resolve wildcard paths', () => {
|
||||||
const parentProject = targetProjectLocator.findProjectFromImport(
|
// 'parent-path/*': ['libs/parent-path/*'] => 'libs/parent-path/child-path'
|
||||||
'parent-path',
|
|
||||||
'libs/proj1/index.ts'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(parentProject).toEqual('parent-project');
|
|
||||||
|
|
||||||
const childProject = targetProjectLocator.findProjectFromImport(
|
const childProject = targetProjectLocator.findProjectFromImport(
|
||||||
'parent-path/child-path',
|
'parent-path/child-path',
|
||||||
'libs/proj1/index.ts'
|
'libs/proj1/index.ts'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(childProject).toEqual('child-project');
|
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 #', () => {
|
it('should be able to resolve paths that start with a #', () => {
|
||||||
|
|||||||
@ -30,6 +30,16 @@ import {
|
|||||||
*/
|
*/
|
||||||
type NpmResolutionCache = Map<string, string | null>;
|
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.
|
* 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 npmProjects: Record<string, ProjectGraphExternalNode | null>;
|
||||||
private tsConfig = this.getRootTsConfig();
|
private tsConfig = this.getRootTsConfig();
|
||||||
private paths = this.tsConfig.config?.compilerOptions?.paths;
|
private paths = this.tsConfig.config?.compilerOptions?.paths;
|
||||||
|
private parsedPathPatterns: ParsedPatterns | undefined;
|
||||||
private typescriptResolutionCache = new Map<string, string | null>();
|
private typescriptResolutionCache = new Map<string, string | null>();
|
||||||
private packagesMetadata: {
|
private packagesMetadata: {
|
||||||
entryPointsToProjectMap: Record<string, ProjectGraphProjectNode>;
|
entryPointsToProjectMap: Record<string, ProjectGraphProjectNode>;
|
||||||
@ -81,6 +92,10 @@ export class TargetProjectLocator {
|
|||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, ProjectGraphExternalNode>);
|
}, {} 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
|
// find project using tsconfig paths
|
||||||
const results = this.findPaths(importExpr);
|
const results = this.findMatchingPaths(importExpr);
|
||||||
if (results) {
|
if (results) {
|
||||||
const [path, paths] = 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) {
|
for (let p of paths) {
|
||||||
const r = p.endsWith('/*')
|
const path = matchedStar ? p.replace('*', matchedStar) : p;
|
||||||
? join(dirname(p), relative(path.replace(/\*$/, ''), importExpr))
|
const maybeResolvedProject = this.findProjectOfResolvedModule(path);
|
||||||
: p;
|
|
||||||
const maybeResolvedProject = this.findProjectOfResolvedModule(r);
|
|
||||||
if (maybeResolvedProject) {
|
if (maybeResolvedProject) {
|
||||||
return maybeResolvedProject;
|
return maybeResolvedProject;
|
||||||
}
|
}
|
||||||
@ -237,7 +257,7 @@ export class TargetProjectLocator {
|
|||||||
/**
|
/**
|
||||||
* Return file paths matching the import relative to the repo root
|
* Return file paths matching the import relative to the repo root
|
||||||
* @param normalizedImportExpr
|
* @param normalizedImportExpr
|
||||||
* @returns
|
* @deprecated Use `findMatchingPaths` instead. It will be removed in Nx v22.
|
||||||
*/
|
*/
|
||||||
findPaths(normalizedImportExpr: string): string[] | undefined {
|
findPaths(normalizedImportExpr: string): string[] | undefined {
|
||||||
if (!this.paths) {
|
if (!this.paths) {
|
||||||
@ -258,6 +278,37 @@ export class TargetProjectLocator {
|
|||||||
return undefined;
|
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 {
|
findImportInWorkspaceProjects(importPath: string): string | null {
|
||||||
this.packagesMetadata ??= getWorkspacePackagesMetadata(this.nodes);
|
this.packagesMetadata ??= getWorkspacePackagesMetadata(this.nodes);
|
||||||
|
|
||||||
@ -279,6 +330,40 @@ export class TargetProjectLocator {
|
|||||||
return this.packagesMetadata.packageToProjectMap[dep]?.name;
|
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(
|
private resolveImportWithTypescript(
|
||||||
normalizedImportExpr: string,
|
normalizedImportExpr: string,
|
||||||
filePath: string
|
filePath: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user