diff --git a/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts b/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts index 35a63a5698..2dcffe8942 100644 --- a/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/entry_point_finder/targeted_entry_point_finder.ts @@ -150,13 +150,35 @@ export class TargetedEntryPointFinder implements EntryPointFinder { break; } } - // If we get here then none of the `basePaths` matched the `entryPointPath`, which - // is somewhat unexpected and means that this entry-point lives completely outside - // any of the `basePaths`. - // All we can do is assume that his entry-point is a primary entry-point to a package. - return entryPointPath; - } + // We couldn't find a `packagePath` using `basePaths` so try to find the nearest `node_modules` + // that contains the `entryPointPath`, if there is one, and use it as a `basePath`. + let packagePath = entryPointPath; + let scopedPackagePath = packagePath; + let containerPath = this.fs.dirname(packagePath); + while (!this.fs.isRoot(containerPath) && !containerPath.endsWith('node_modules')) { + scopedPackagePath = packagePath; + packagePath = containerPath; + containerPath = this.fs.dirname(containerPath); + } + + if (this.fs.exists(join(packagePath, 'package.json'))) { + // The directory directly below `node_modules` is a package - use it + return packagePath; + } else if ( + this.fs.basename(packagePath).startsWith('@') && + this.fs.exists(join(scopedPackagePath, 'package.json'))) { + // The directory directly below the `node_modules` is a scope and the directory directly + // below that is a scoped package - use it + return scopedPackagePath; + } else { + // If we get here then none of the `basePaths` contained the `entryPointPath` and the + // `entryPointPath` contains no `node_modules` that contains a package or a scoped + // package. All we can do is assume that this entry-point is a primary entry-point to a + // package. + return entryPointPath; + } + } /** * Split the given `path` into path segments using an FS independent algorithm. diff --git a/packages/compiler-cli/ngcc/src/entry_point_finder/utils.ts b/packages/compiler-cli/ngcc/src/entry_point_finder/utils.ts index 7a43c237a7..2f8329eae4 100644 --- a/packages/compiler-cli/ngcc/src/entry_point_finder/utils.ts +++ b/packages/compiler-cli/ngcc/src/entry_point_finder/utils.ts @@ -30,7 +30,7 @@ import {PathMappings} from '../utils'; export function getBasePaths( sourceDirectory: AbsoluteFsPath, pathMappings: PathMappings | undefined): AbsoluteFsPath[] { const fs = getFileSystem(); - let basePaths = [sourceDirectory]; + const basePaths = [sourceDirectory]; if (pathMappings) { const baseUrl = resolve(pathMappings.baseUrl); Object.values(pathMappings.paths).forEach(paths => paths.forEach(path => { diff --git a/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts index 63d317fd2d..0bbd51fbfb 100644 --- a/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/entry_point_finder/targeted_entry_point_finder_spec.ts @@ -166,6 +166,81 @@ runInEachFileSystem(() => { ]); }); + it('should handle external node_modules folders (e.g. in a yarn workspace)', () => { + // Note that neither the basePath and targetPath contain each other + const basePath = _Abs('/nested_node_modules/packages/app/node_modules'); + const targetPath = _Abs('/nested_node_modules/node_modules/package/entry-point'); + loadTestFiles([ + ...createPackage(_Abs('/nested_node_modules/node_modules'), 'package'), + ...createPackage(_Abs('/nested_node_modules/node_modules/package'), 'entry-point'), + ]); + const finder = new TargetedEntryPointFinder( + fs, config, logger, resolver, basePath, targetPath, undefined); + const {entryPoints} = finder.findEntryPoints(); + expect(dumpEntryPointPaths(_Abs('/nested_node_modules'), entryPoints)).toEqual([ + ['node_modules/package', 'node_modules/package/entry-point'], + ]); + }); + + it('should handle external node_modules folders (e.g. in a yarn workspace) for dependencies', + () => { + // The application being compiled is at `/project/packages/app` so the basePath sent to + // ngcc is the `node_modules` below it + const basePath = _Abs('/project/packages/app/node_modules'); + // `packages/app` depends upon lib1, which has a private dependency on lib2 in its + // own `node_modules` folder + const lib2 = createPackage( + _Abs('/project/node_modules/lib1/node_modules'), 'lib2', ['lib3/entry-point']); + // `lib2` depends upon `lib3/entry-point` which has been hoisted all the way up to the + // top level `node_modules` + const lib3 = createPackage(_Abs('/project/node_modules'), 'lib3'); + const lib3EntryPoint = createPackage(_Abs('/project/node_modules/lib3'), 'entry-point'); + loadTestFiles([...lib2, ...lib3, ...lib3EntryPoint]); + // The targetPath being processed is `lib2` and we expect it to find the correct + // entry-point info for the `lib3/entry-point` dependency. + const targetPath = _Abs('/project/node_modules/lib1/node_modules/lib2'); + const finder = new TargetedEntryPointFinder( + fs, config, logger, resolver, basePath, targetPath, undefined); + const {entryPoints} = finder.findEntryPoints(); + expect(dumpEntryPointPaths(_Abs('/project/node_modules'), entryPoints)).toEqual([ + ['lib3', 'lib3/entry-point'], + ['lib1/node_modules/lib2', 'lib1/node_modules/lib2'], + ]); + }); + + it('should handle external node_modules folders (e.g. in a yarn workspace) for scoped dependencies', + () => { + // The application being compiled is at `/project/packages/app` so the basePath sent to + // ngcc is the `node_modules` below it + const basePath = _Abs('/project/packages/app/node_modules'); + // `packages/app` depends upon lib1, which has a private dependency on lib2 in its + // own `node_modules` folder + const lib2 = createPackage( + _Abs('/project/node_modules/lib1/node_modules'), 'lib2', + ['@scope/lib3/entry-point']); + // `lib2` depends upon `lib3/entry-point` which has been hoisted all the way up to the + // top level `node_modules` + const lib3 = createPackage(_Abs('/project/node_modules/@scope'), 'lib3'); + const lib3EntryPoint = createPackage( + _Abs('/project/node_modules/@scope/lib3'), 'entry-point', ['lib4/entry-point']); + const lib4 = + createPackage(_Abs('/project/node_modules/@scope/lib3/node_modules'), 'lib4'); + const lib4EntryPoint = createPackage( + _Abs('/project/node_modules/@scope/lib3/node_modules/lib4'), 'entry-point'); + loadTestFiles([...lib2, ...lib3, ...lib3EntryPoint, ...lib4, ...lib4EntryPoint]); + // The targetPath being processed is `lib2` and we expect it to find the correct + // entry-point info for the `lib3/entry-point` dependency. + const targetPath = _Abs('/project/node_modules/lib1/node_modules/lib2'); + const finder = new TargetedEntryPointFinder( + fs, config, logger, resolver, basePath, targetPath, undefined); + const {entryPoints} = finder.findEntryPoints(); + expect(dumpEntryPointPaths(_Abs('/project/node_modules'), entryPoints)).toEqual([ + ['@scope/lib3/node_modules/lib4', '@scope/lib3/node_modules/lib4/entry-point'], + ['@scope/lib3', '@scope/lib3/entry-point'], + ['lib1/node_modules/lib2', 'lib1/node_modules/lib2'], + ]); + }); + it('should handle dependencies via pathMappings', () => { const basePath = _Abs('/path_mapped/node_modules'); const targetPath = _Abs('/path_mapped/node_modules/test');