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
|
- [data](../../devkit/documents/ProjectGraphProjectNode#data): ProjectConfiguration & Object
|
||||||
- [name](../../devkit/documents/ProjectGraphProjectNode#name): string
|
- [name](../../devkit/documents/ProjectGraphProjectNode#name): string
|
||||||
- [type](../../devkit/documents/ProjectGraphProjectNode#type): "app" | "e2e" | "lib"
|
- [type](../../devkit/documents/ProjectGraphProjectNode#type): "lib" | "app" | "e2e"
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
@ -28,4 +28,4 @@ Additional metadata about a project
|
|||||||
|
|
||||||
### type
|
### 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
|
// project outside of the package manager workspaces
|
||||||
createPackage('lib1', { root: 'libs/lib1' });
|
createPackage('lib1', { root: 'libs/lib1' });
|
||||||
|
|
||||||
@ -173,6 +197,8 @@ describe('Graph - TS solution setup', () => {
|
|||||||
json.dependencies['@proj/pkg10'] = 'workspace:*';
|
json.dependencies['@proj/pkg10'] = 'workspace:*';
|
||||||
json.dependencies['@proj/pkg11'] = 'workspace:*';
|
json.dependencies['@proj/pkg11'] = 'workspace:*';
|
||||||
json.dependencies['@proj/pkg13'] = 'workspace:*';
|
json.dependencies['@proj/pkg13'] = 'workspace:*';
|
||||||
|
json.dependencies['@proj/pkg15'] = 'workspace:*';
|
||||||
|
json.dependencies['@proj/pkg16'] = 'workspace:*';
|
||||||
return json;
|
return json;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -207,6 +233,8 @@ describe('Graph - TS solution setup', () => {
|
|||||||
{ path: '../pkg12' },
|
{ path: '../pkg12' },
|
||||||
{ path: '../pkg13' },
|
{ path: '../pkg13' },
|
||||||
{ path: '../pkg14' },
|
{ path: '../pkg14' },
|
||||||
|
{ path: '../pkg15' },
|
||||||
|
{ path: '../pkg16' },
|
||||||
];
|
];
|
||||||
return json;
|
return json;
|
||||||
});
|
});
|
||||||
@ -226,13 +254,15 @@ describe('Graph - TS solution setup', () => {
|
|||||||
import { util1 } from '@proj/pkg11/utils/util1';
|
import { util1 } from '@proj/pkg11/utils/util1';
|
||||||
import { pkg12 } from '@proj/pkg12/feature1';
|
import { pkg12 } from '@proj/pkg12/feature1';
|
||||||
import { pkg13 } from '@proj/pkg14';
|
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
|
// 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
|
// it's not included in the package manager workspaces, it should not
|
||||||
// be picked up as a dependency
|
// be picked up as a dependency
|
||||||
import { lib1 } from '@proj/lib1';
|
import { lib1 } from '@proj/lib1';
|
||||||
|
|
||||||
// use the correct imports, leave out the invalid ones so it's easier to remove them later
|
// 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/pkg10',
|
||||||
'@proj/pkg11',
|
'@proj/pkg11',
|
||||||
'@proj/pkg13',
|
'@proj/pkg13',
|
||||||
|
'@proj/pkg15',
|
||||||
|
'@proj/pkg16',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// assert build fails due to the invalid imports
|
// assert build fails due to the invalid imports
|
||||||
@ -331,7 +363,7 @@ describe('Graph - TS solution setup', () => {
|
|||||||
|
|
||||||
const sourceFilePaths = options?.sourceFilePaths ?? ['src/index.ts'];
|
const sourceFilePaths = options?.sourceFilePaths ?? ['src/index.ts'];
|
||||||
for (const sourceFilePath of sourceFilePaths) {
|
for (const sourceFilePath of sourceFilePaths) {
|
||||||
const fileName = basename(sourceFilePath, '.ts');
|
const fileName = basename(sourceFilePath, '.ts').replace(/-/g, '_');
|
||||||
createFile(
|
createFile(
|
||||||
`${root}/${sourceFilePath}`,
|
`${root}/${sourceFilePath}`,
|
||||||
`export const ${
|
`export const ${
|
||||||
|
|||||||
@ -1129,6 +1129,10 @@ describe('TargetProjectLocator', () => {
|
|||||||
${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
|
${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
|
||||||
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
|
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
|
||||||
${{ './*': './dist/*.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'}
|
${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1'}
|
||||||
`(
|
`(
|
||||||
'should find "$importPath" as "pkg1" project when exports="$exports"',
|
'should find "$importPath" as "pkg1" project when exports="$exports"',
|
||||||
@ -1168,6 +1172,10 @@ describe('TargetProjectLocator', () => {
|
|||||||
${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
|
${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
|
||||||
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
|
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
|
||||||
${{ './*': './dist/*.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'}
|
${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1'}
|
||||||
`(
|
`(
|
||||||
'should not find "$importPath" as "pkg1" project when exports="$exports" and isInPackageManagerWorkspaces is false',
|
'should not find "$importPath" as "pkg1" project when exports="$exports" and isInPackageManagerWorkspaces is false',
|
||||||
@ -1206,8 +1214,8 @@ describe('TargetProjectLocator', () => {
|
|||||||
${undefined} | ${'@org/pkg1'}
|
${undefined} | ${'@org/pkg1'}
|
||||||
${{}} | ${'@org/pkg1'}
|
${{}} | ${'@org/pkg1'}
|
||||||
${{ '.': 'dist/index.js' }} | ${'@org/pkg1/subpath'}
|
${{ '.': 'dist/index.js' }} | ${'@org/pkg1/subpath'}
|
||||||
|
${{ './subpath/*': 'dist/subpath/*.js' }} | ${'@org/pkg1/foo'}
|
||||||
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
||||||
${{ './*': './dist/*.js' }} | ${'@org/pkg1/subpath/extra-path'}
|
|
||||||
${{ './feature': null }} | ${'@org/pkg1/feature'}
|
${{ './feature': null }} | ${'@org/pkg1/feature'}
|
||||||
${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1/subpath'}
|
${{ 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 { join } from 'node:path/posix';
|
||||||
import type { ProjectGraphProjectNode } from '../../../config/project-graph';
|
import type { ProjectGraphProjectNode } from '../../../config/project-graph';
|
||||||
import type { ProjectConfiguration } from '../../../config/workspace-json-project-json';
|
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<
|
export function matchImportToWildcardEntryPointsToProjectMap<
|
||||||
T extends ProjectGraphProjectNode | ProjectConfiguration
|
T extends ProjectGraphProjectNode | ProjectConfiguration
|
||||||
>(
|
>(
|
||||||
@ -89,9 +90,33 @@ export function matchImportToWildcardEntryPointsToProjectMap<
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchingPair = Object.entries(wildcardEntryPointsToProjectMap).find(
|
const entryPoint = Object.keys(wildcardEntryPointsToProjectMap).find(
|
||||||
([key]) => minimatch(importPath, key)
|
(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