fix(core): resolve subpath patterns in package exports correctly when constructing graph (#30511)
## Current Behavior
When a package has a subpath pattern like the following:
```json
{
"exports": {
"./*": {
"types": "./dist/lib/*/index.d.ts",
"import": "./dist/lib/*/index.js",
"default": "./dist/lib/*/index.js"
}
}
}
```
When constructing the graph the project is not picked as a dependency of
others projects that import from the package using a path that matches
that subpath pattern. This is currently happening because the current
resolution is wrongly using `minimatch` to match those patterns instead
of the [Node.js spec for resolving subpath
patterns](https://nodejs.org/docs/latest-v22.x/api/esm.html#resolution-algorithm-specification).
## Expected Behavior
Subpath patterns should be processed after the [Node.js
spec](https://nodejs.org/docs/latest-v22.x/api/esm.html#resolution-algorithm-specification)
and the graph should pick up dependencies when used.
## Related Issue(s)
Fixes #30342
This commit is contained in:
parent
2dbff35de9
commit
b3c6d2d417
@ -8,7 +8,7 @@ A node describing a project in a workspace
|
||||
|
||||
- [data](../../devkit/documents/ProjectGraphProjectNode#data): ProjectConfiguration & Object
|
||||
- [name](../../devkit/documents/ProjectGraphProjectNode#name): string
|
||||
- [type](../../devkit/documents/ProjectGraphProjectNode#type): "app" | "e2e" | "lib"
|
||||
- [type](../../devkit/documents/ProjectGraphProjectNode#type): "lib" | "app" | "e2e"
|
||||
|
||||
## Properties
|
||||
|
||||
@ -28,4 +28,4 @@ Additional metadata about a project
|
||||
|
||||
### type
|
||||
|
||||
• **type**: `"app"` \| `"e2e"` \| `"lib"`
|
||||
• **type**: `"lib"` \| `"app"` \| `"e2e"`
|
||||
|
||||
@ -154,6 +154,30 @@ describe('Graph - TS solution setup', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
// wildcard exports with trailing values
|
||||
createPackage('pkg15', {
|
||||
sourceFilePaths: ['src/features/some-file.ts'],
|
||||
packageJsonEntryFields: {
|
||||
exports: {
|
||||
'./features/*.js': {
|
||||
types: './dist/src/features/*.d.ts',
|
||||
default: './dist/src/features/*.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// wildcard exports with no leading or trailing values
|
||||
createPackage('pkg16', {
|
||||
sourceFilePaths: ['src/features/subpath/extra-nested/index.ts'],
|
||||
packageJsonEntryFields: {
|
||||
exports: {
|
||||
'./*': {
|
||||
types: './dist/src/features/*/index.d.ts',
|
||||
default: './dist/src/features/*/index.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// project outside of the package manager workspaces
|
||||
createPackage('lib1', { root: 'libs/lib1' });
|
||||
|
||||
@ -173,6 +197,8 @@ describe('Graph - TS solution setup', () => {
|
||||
json.dependencies['@proj/pkg10'] = 'workspace:*';
|
||||
json.dependencies['@proj/pkg11'] = 'workspace:*';
|
||||
json.dependencies['@proj/pkg13'] = 'workspace:*';
|
||||
json.dependencies['@proj/pkg15'] = 'workspace:*';
|
||||
json.dependencies['@proj/pkg16'] = 'workspace:*';
|
||||
return json;
|
||||
});
|
||||
}
|
||||
@ -207,6 +233,8 @@ describe('Graph - TS solution setup', () => {
|
||||
{ path: '../pkg12' },
|
||||
{ path: '../pkg13' },
|
||||
{ path: '../pkg14' },
|
||||
{ path: '../pkg15' },
|
||||
{ path: '../pkg16' },
|
||||
];
|
||||
return json;
|
||||
});
|
||||
@ -226,13 +254,15 @@ describe('Graph - TS solution setup', () => {
|
||||
import { util1 } from '@proj/pkg11/utils/util1';
|
||||
import { pkg12 } from '@proj/pkg12/feature1';
|
||||
import { pkg13 } from '@proj/pkg14';
|
||||
import { some_file } from '@proj/pkg15/features/some-file.js';
|
||||
import { pkg16 } from '@proj/pkg16/subpath/extra-nested';
|
||||
// this is an invalid import that doesn't match any TS path alias and
|
||||
// it's not included in the package manager workspaces, it should not
|
||||
// be picked up as a dependency
|
||||
import { lib1 } from '@proj/lib1';
|
||||
|
||||
// use the correct imports, leave out the invalid ones so it's easier to remove them later
|
||||
export const pkgParent = pkg2 + pkg4 + pkg5 + pkg6 + pkg7 + pkg8 + pkg9 + pkg10 + util1 + pkg13;
|
||||
export const pkgParent = pkg2 + pkg4 + pkg5 + pkg6 + pkg7 + pkg8 + pkg9 + pkg10 + util1 + pkg13 + some_file + pkg16;
|
||||
`
|
||||
);
|
||||
|
||||
@ -253,6 +283,8 @@ describe('Graph - TS solution setup', () => {
|
||||
'@proj/pkg10',
|
||||
'@proj/pkg11',
|
||||
'@proj/pkg13',
|
||||
'@proj/pkg15',
|
||||
'@proj/pkg16',
|
||||
]);
|
||||
|
||||
// assert build fails due to the invalid imports
|
||||
@ -331,7 +363,7 @@ describe('Graph - TS solution setup', () => {
|
||||
|
||||
const sourceFilePaths = options?.sourceFilePaths ?? ['src/index.ts'];
|
||||
for (const sourceFilePath of sourceFilePaths) {
|
||||
const fileName = basename(sourceFilePath, '.ts');
|
||||
const fileName = basename(sourceFilePath, '.ts').replace(/-/g, '_');
|
||||
createFile(
|
||||
`${root}/${sourceFilePath}`,
|
||||
`export const ${
|
||||
|
||||
@ -1129,6 +1129,10 @@ describe('TargetProjectLocator', () => {
|
||||
${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
|
||||
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
|
||||
${{ './*': './dist/*.js' }} | ${'@org/pkg1/subpath'}
|
||||
${{ './*': './dist/*.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
||||
${{ './*': './dist/foo/*/index.js' }} | ${'@org/pkg1/foo/subpath'}
|
||||
${{ './*': './dist/foo/*/index.js' }} | ${'@org/pkg1/foo/subpath/extra-path'}
|
||||
${{ './features/*.js': './dist/features/*.js' }} | ${'@org/pkg1/features/some-file.js'}
|
||||
${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1'}
|
||||
`(
|
||||
'should find "$importPath" as "pkg1" project when exports="$exports"',
|
||||
@ -1168,6 +1172,10 @@ describe('TargetProjectLocator', () => {
|
||||
${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
|
||||
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
|
||||
${{ './*': './dist/*.js' }} | ${'@org/pkg1/subpath'}
|
||||
${{ './*': './dist/*.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
||||
${{ './*': './dist/foo/*/index.js' }} | ${'@org/pkg1/foo/subpath'}
|
||||
${{ './*': './dist/foo/*/index.js' }} | ${'@org/pkg1/foo/subpath/extra-path'}
|
||||
${{ './features/*.js': './dist/features/*.js' }} | ${'@org/pkg1/features/some-file.js'}
|
||||
${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1'}
|
||||
`(
|
||||
'should not find "$importPath" as "pkg1" project when exports="$exports" and isInPackageManagerWorkspaces is false',
|
||||
@ -1206,8 +1214,8 @@ describe('TargetProjectLocator', () => {
|
||||
${undefined} | ${'@org/pkg1'}
|
||||
${{}} | ${'@org/pkg1'}
|
||||
${{ '.': 'dist/index.js' }} | ${'@org/pkg1/subpath'}
|
||||
${{ './subpath/*': 'dist/subpath/*.js' }} | ${'@org/pkg1/foo'}
|
||||
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
||||
${{ './*': './dist/*.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
||||
${{ './feature': null }} | ${'@org/pkg1/feature'}
|
||||
${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1/subpath'}
|
||||
`(
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { minimatch } from 'minimatch';
|
||||
import { join } from 'node:path/posix';
|
||||
import type { ProjectGraphProjectNode } from '../../../config/project-graph';
|
||||
import type { ProjectConfiguration } from '../../../config/workspace-json-project-json';
|
||||
@ -79,6 +78,8 @@ export function getWorkspacePackagesMetadata<
|
||||
};
|
||||
}
|
||||
|
||||
// adapted from PACKAGE_IMPORTS_EXPORTS_RESOLVE at
|
||||
// https://nodejs.org/docs/latest-v22.x/api/esm.html#resolution-algorithm-specification
|
||||
export function matchImportToWildcardEntryPointsToProjectMap<
|
||||
T extends ProjectGraphProjectNode | ProjectConfiguration
|
||||
>(
|
||||
@ -89,9 +90,33 @@ export function matchImportToWildcardEntryPointsToProjectMap<
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchingPair = Object.entries(wildcardEntryPointsToProjectMap).find(
|
||||
([key]) => minimatch(importPath, key)
|
||||
const entryPoint = Object.keys(wildcardEntryPointsToProjectMap).find(
|
||||
(key) => {
|
||||
const segments = key.split('*');
|
||||
if (segments.length > 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const patternBase = segments[0];
|
||||
if (patternBase === importPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!importPath.startsWith(patternBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const patternTrailer = segments[1];
|
||||
if (
|
||||
patternTrailer?.length > 0 &&
|
||||
(!importPath.endsWith(patternTrailer) || importPath.length < key.length)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
return matchingPair?.[1];
|
||||
return entryPoint ? wildcardEntryPointsToProjectMap[entryPoint] : null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user