fix(ivy): let ngcc not consider deep imports as missing dependencies (#27031)
This fixes an issue where packages would be skipped if they contained
e.g. RxJS 5 style imports such as
```
import { observeOn } from 'rxjs/operators/observeOn';
```
Given that no package.json file can be found at the imported path, the
dependency would be reported missing, causing the package to be skipped.
PR Close #27031
			
			
This commit is contained in:
		
							parent
							
								
									99c5db1fb1
								
							
						
					
					
						commit
						97ef8ae9e7
					
				| @ -17,15 +17,15 @@ export class DependencyHost { | |||||||
|   /** |   /** | ||||||
|    * Get a list of the resolved paths to all the dependencies of this entry point. |    * Get a list of the resolved paths to all the dependencies of this entry point. | ||||||
|    * @param from An absolute path to the file whose dependencies we want to get. |    * @param from An absolute path to the file whose dependencies we want to get. | ||||||
|    * @param resolved A set that will have the resolved dependencies added to it. |    * @param resolved A set that will have the absolute paths of resolved entry points added to it. | ||||||
|    * @param missing A set that will have the dependencies that could not be found added to it. |    * @param missing A set that will have the dependencies that could not be found added to it. | ||||||
|  |    * @param deepImports A set that will have the import paths that exist but cannot be mapped to | ||||||
|  |    * entry-points, i.e. deep-imports. | ||||||
|    * @param internal A set that is used to track internal dependencies to prevent getting stuck in a |    * @param internal A set that is used to track internal dependencies to prevent getting stuck in a | ||||||
|    * circular dependency loop. |    * circular dependency loop. | ||||||
|    * @returns an object containing an array of absolute paths to `resolved` depenendencies and an |  | ||||||
|    * array of import specifiers for dependencies that were `missing`. |  | ||||||
|    */ |    */ | ||||||
|   computeDependencies( |   computeDependencies( | ||||||
|       from: string, resolved: Set<string>, missing: Set<string>, |       from: string, resolved: Set<string>, missing: Set<string>, deepImports: Set<string>, | ||||||
|       internal: Set<string> = new Set()): void { |       internal: Set<string> = new Set()): void { | ||||||
|     const fromContents = fs.readFileSync(from, 'utf8'); |     const fromContents = fs.readFileSync(from, 'utf8'); | ||||||
|     if (!this.hasImportOrReeportStatements(fromContents)) { |     if (!this.hasImportOrReeportStatements(fromContents)) { | ||||||
| @ -48,14 +48,22 @@ export class DependencyHost { | |||||||
|             // Avoid circular dependencies
 |             // Avoid circular dependencies
 | ||||||
|             if (!internal.has(internalDependency)) { |             if (!internal.has(internalDependency)) { | ||||||
|               internal.add(internalDependency); |               internal.add(internalDependency); | ||||||
|               this.computeDependencies(internalDependency, resolved, missing, internal); |               this.computeDependencies( | ||||||
|  |                   internalDependency, resolved, missing, deepImports, internal); | ||||||
|             } |             } | ||||||
|           } else { |           } else { | ||||||
|             const externalDependency = this.tryResolveExternal(from, importPath); |             const resolvedEntryPoint = this.tryResolveEntryPoint(from, importPath); | ||||||
|             if (externalDependency !== null) { |             if (resolvedEntryPoint !== null) { | ||||||
|               resolved.add(externalDependency); |               resolved.add(resolvedEntryPoint); | ||||||
|             } else { |             } else { | ||||||
|               missing.add(importPath); |               // If the import could not be resolved as entry point, it either does not exist
 | ||||||
|  |               // at all or is a deep import.
 | ||||||
|  |               const deeplyImportedFile = this.tryResolve(from, importPath); | ||||||
|  |               if (deeplyImportedFile !== null) { | ||||||
|  |                 deepImports.add(importPath); | ||||||
|  |               } else { | ||||||
|  |                 missing.add(importPath); | ||||||
|  |               } | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
| @ -91,9 +99,9 @@ export class DependencyHost { | |||||||
|    * @returns the resolved path to the entry point directory of the import or null |    * @returns the resolved path to the entry point directory of the import or null | ||||||
|    * if it cannot be resolved. |    * if it cannot be resolved. | ||||||
|    */ |    */ | ||||||
|   tryResolveExternal(from: string, to: string): string|null { |   tryResolveEntryPoint(from: string, to: string): string|null { | ||||||
|     const externalDependency = this.tryResolve(from, `${to}/package.json`); |     const entryPoint = this.tryResolve(from, `${to}/package.json`); | ||||||
|     return externalDependency && path.dirname(externalDependency); |     return entryPoint && path.dirname(entryPoint); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|  | |||||||
| @ -87,7 +87,8 @@ export class DependencyResolver { | |||||||
| 
 | 
 | ||||||
|       const dependencies = new Set<string>(); |       const dependencies = new Set<string>(); | ||||||
|       const missing = new Set<string>(); |       const missing = new Set<string>(); | ||||||
|       this.host.computeDependencies(entryPointPath, dependencies, missing); |       const deepImports = new Set<string>(); | ||||||
|  |       this.host.computeDependencies(entryPointPath, dependencies, missing, deepImports); | ||||||
| 
 | 
 | ||||||
|       if (missing.size > 0) { |       if (missing.size > 0) { | ||||||
|         // This entry point has dependencies that are missing
 |         // This entry point has dependencies that are missing
 | ||||||
| @ -109,6 +110,13 @@ export class DependencyResolver { | |||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       if (deepImports.size) { | ||||||
|  |         const imports = Array.from(deepImports).map(i => `'${i}'`).join(', '); | ||||||
|  |         console.warn( | ||||||
|  |             `Entry point '${entryPoint.name}' contains deep imports into ${imports}. ` + | ||||||
|  |             `This is probably not a problem, but may cause the compilation of entry points to be out of order.`); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // The map now only holds entry-points that ngcc cares about and whose dependencies
 |     // The map now only holds entry-points that ngcc cares about and whose dependencies
 | ||||||
|  | |||||||
| @ -27,61 +27,88 @@ describe('DependencyHost', () => { | |||||||
|     it('should not generate a TS AST if the source does not contain any imports or re-exports', |     it('should not generate a TS AST if the source does not contain any imports or re-exports', | ||||||
|        () => { |        () => { | ||||||
|          spyOn(ts, 'createSourceFile'); |          spyOn(ts, 'createSourceFile'); | ||||||
|          host.computeDependencies('/no/imports/or/re-exports.js', new Set(), new Set()); |          host.computeDependencies('/no/imports/or/re-exports.js', new Set(), new Set(), new Set()); | ||||||
|          expect(ts.createSourceFile).not.toHaveBeenCalled(); |          expect(ts.createSourceFile).not.toHaveBeenCalled(); | ||||||
|        }); |        }); | ||||||
| 
 | 
 | ||||||
|     it('should resolve all the external imports of the source file', () => { |     it('should resolve all the external imports of the source file', () => { | ||||||
|       spyOn(host, 'tryResolveExternal') |       spyOn(host, 'tryResolveEntryPoint') | ||||||
|           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); |           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); | ||||||
|       const resolved = new Set(); |       const resolved = new Set(); | ||||||
|       const missing = new Set(); |       const missing = new Set(); | ||||||
|       host.computeDependencies('/external/imports.js', resolved, missing); |       const deepImports = new Set(); | ||||||
|  |       host.computeDependencies('/external/imports.js', resolved, missing, deepImports); | ||||||
|       expect(resolved.size).toBe(2); |       expect(resolved.size).toBe(2); | ||||||
|       expect(resolved.has('RESOLVED/path/to/x')); |       expect(resolved.has('RESOLVED/path/to/x')).toBe(true); | ||||||
|       expect(resolved.has('RESOLVED/path/to/y')); |       expect(resolved.has('RESOLVED/path/to/y')).toBe(true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should resolve all the external re-exports of the source file', () => { |     it('should resolve all the external re-exports of the source file', () => { | ||||||
|       spyOn(host, 'tryResolveExternal') |       spyOn(host, 'tryResolveEntryPoint') | ||||||
|           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); |           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); | ||||||
|       const resolved = new Set(); |       const resolved = new Set(); | ||||||
|       const missing = new Set(); |       const missing = new Set(); | ||||||
|       host.computeDependencies('/external/re-exports.js', resolved, missing); |       const deepImports = new Set(); | ||||||
|  |       host.computeDependencies('/external/re-exports.js', resolved, missing, deepImports); | ||||||
|       expect(resolved.size).toBe(2); |       expect(resolved.size).toBe(2); | ||||||
|       expect(resolved.has('RESOLVED/path/to/x')); |       expect(resolved.has('RESOLVED/path/to/x')).toBe(true); | ||||||
|       expect(resolved.has('RESOLVED/path/to/y')); |       expect(resolved.has('RESOLVED/path/to/y')).toBe(true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should capture missing external imports', () => { |     it('should capture missing external imports', () => { | ||||||
|       spyOn(host, 'tryResolveExternal') |       spyOn(host, 'tryResolveEntryPoint') | ||||||
|           .and.callFake( |           .and.callFake( | ||||||
|               (from: string, importPath: string) => |               (from: string, importPath: string) => | ||||||
|                   importPath === 'missing' ? null : `RESOLVED/${importPath}`); |                   importPath === 'missing' ? null : `RESOLVED/${importPath}`); | ||||||
|  |       spyOn(host, 'tryResolve').and.callFake(() => null); | ||||||
|       const resolved = new Set(); |       const resolved = new Set(); | ||||||
|       const missing = new Set(); |       const missing = new Set(); | ||||||
|       host.computeDependencies('/external/imports-missing.js', resolved, missing); |       const deepImports = new Set(); | ||||||
|  |       host.computeDependencies('/external/imports-missing.js', resolved, missing, deepImports); | ||||||
|       expect(resolved.size).toBe(1); |       expect(resolved.size).toBe(1); | ||||||
|       expect(resolved.has('RESOLVED/path/to/x')); |       expect(resolved.has('RESOLVED/path/to/x')).toBe(true); | ||||||
|       expect(missing.size).toBe(1); |       expect(missing.size).toBe(1); | ||||||
|       expect(missing.has('missing')); |       expect(missing.has('missing')).toBe(true); | ||||||
|  |       expect(deepImports.size).toBe(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should not register deep imports as missing', () => { | ||||||
|  |       // This scenario verifies the behavior of the dependency analysis when an external import
 | ||||||
|  |       // is found that does not map to an entry-point but still exists on disk, i.e. a deep import.
 | ||||||
|  |       // Such deep imports are captured for diagnostics purposes.
 | ||||||
|  |       const tryResolveEntryPoint = (from: string, importPath: string) => | ||||||
|  |           importPath === 'deep/import' ? null : `RESOLVED/${importPath}`; | ||||||
|  |       spyOn(host, 'tryResolveEntryPoint').and.callFake(tryResolveEntryPoint); | ||||||
|  |       spyOn(host, 'tryResolve') | ||||||
|  |           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); | ||||||
|  |       const resolved = new Set(); | ||||||
|  |       const missing = new Set(); | ||||||
|  |       const deepImports = new Set(); | ||||||
|  |       host.computeDependencies('/external/deep-import.js', resolved, missing, deepImports); | ||||||
|  |       expect(resolved.size).toBe(0); | ||||||
|  |       expect(missing.size).toBe(0); | ||||||
|  |       expect(deepImports.size).toBe(1); | ||||||
|  |       expect(deepImports.has('deep/import')).toBe(true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should recurse into internal dependencies', () => { |     it('should recurse into internal dependencies', () => { | ||||||
|       spyOn(host, 'resolveInternal') |       spyOn(host, 'resolveInternal') | ||||||
|           .and.callFake( |           .and.callFake( | ||||||
|               (from: string, importPath: string) => path.join('/internal', importPath + '.js')); |               (from: string, importPath: string) => path.join('/internal', importPath + '.js')); | ||||||
|       spyOn(host, 'tryResolveExternal') |       spyOn(host, 'tryResolveEntryPoint') | ||||||
|           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); |           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); | ||||||
|       const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough(); |       const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough(); | ||||||
|       const resolved = new Set(); |       const resolved = new Set(); | ||||||
|       const missing = new Set(); |       const missing = new Set(); | ||||||
|       host.computeDependencies('/internal/outer.js', resolved, missing); |       const deepImports = new Set(); | ||||||
|       expect(getDependenciesSpy).toHaveBeenCalledWith('/internal/outer.js', resolved, missing); |       host.computeDependencies('/internal/outer.js', resolved, missing, deepImports); | ||||||
|       expect(getDependenciesSpy) |       expect(getDependenciesSpy) | ||||||
|           .toHaveBeenCalledWith('/internal/inner.js', resolved, missing, jasmine.any(Set)); |           .toHaveBeenCalledWith('/internal/outer.js', resolved, missing, deepImports); | ||||||
|  |       expect(getDependenciesSpy) | ||||||
|  |           .toHaveBeenCalledWith( | ||||||
|  |               '/internal/inner.js', resolved, missing, deepImports, jasmine.any(Set)); | ||||||
|       expect(resolved.size).toBe(1); |       expect(resolved.size).toBe(1); | ||||||
|       expect(resolved.has('RESOLVED/path/to/y')); |       expect(resolved.has('RESOLVED/path/to/y')).toBe(true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -89,14 +116,15 @@ describe('DependencyHost', () => { | |||||||
|       spyOn(host, 'resolveInternal') |       spyOn(host, 'resolveInternal') | ||||||
|           .and.callFake( |           .and.callFake( | ||||||
|               (from: string, importPath: string) => path.join('/internal', importPath + '.js')); |               (from: string, importPath: string) => path.join('/internal', importPath + '.js')); | ||||||
|       spyOn(host, 'tryResolveExternal') |       spyOn(host, 'tryResolveEntryPoint') | ||||||
|           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); |           .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); | ||||||
|       const resolved = new Set(); |       const resolved = new Set(); | ||||||
|       const missing = new Set(); |       const missing = new Set(); | ||||||
|       host.computeDependencies('/internal/circular-a.js', resolved, missing); |       const deepImports = new Set(); | ||||||
|  |       host.computeDependencies('/internal/circular-a.js', resolved, missing, deepImports); | ||||||
|       expect(resolved.size).toBe(2); |       expect(resolved.size).toBe(2); | ||||||
|       expect(resolved.has('RESOLVED/path/to/x')); |       expect(resolved.has('RESOLVED/path/to/x')).toBe(true); | ||||||
|       expect(resolved.has('RESOLVED/path/to/y')); |       expect(resolved.has('RESOLVED/path/to/y')).toBe(true); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     function createMockFileSystem() { |     function createMockFileSystem() { | ||||||
| @ -105,6 +133,7 @@ describe('DependencyHost', () => { | |||||||
|         '/external/imports.js': `import {X} from 'path/to/x';\nimport {Y} from 'path/to/y';`, |         '/external/imports.js': `import {X} from 'path/to/x';\nimport {Y} from 'path/to/y';`, | ||||||
|         '/external/re-exports.js': `export {X} from 'path/to/x';\nexport {Y} from 'path/to/y';`, |         '/external/re-exports.js': `export {X} from 'path/to/x';\nexport {Y} from 'path/to/y';`, | ||||||
|         '/external/imports-missing.js': `import {X} from 'path/to/x';\nimport {Y} from 'missing';`, |         '/external/imports-missing.js': `import {X} from 'path/to/x';\nimport {Y} from 'missing';`, | ||||||
|  |         '/external/deep-import.js': `import {Y} from 'deep/import';`, | ||||||
|         '/internal/outer.js': `import {X} from './inner';`, |         '/internal/outer.js': `import {X} from './inner';`, | ||||||
|         '/internal/inner.js': `import {Y} from 'path/to/y';`, |         '/internal/inner.js': `import {Y} from 'path/to/y';`, | ||||||
|         '/internal/circular-a.js': `import {B} from './circular-b'; import {X} from 'path/to/x';`, |         '/internal/circular-a.js': `import {B} from './circular-b'; import {X} from 'path/to/x';`, | ||||||
| @ -131,18 +160,18 @@ describe('DependencyHost', () => { | |||||||
|   describe('tryResolveExternal', () => { |   describe('tryResolveExternal', () => { | ||||||
|     it('should call `tryResolve`, appending `package.json` to the target path', () => { |     it('should call `tryResolve`, appending `package.json` to the target path', () => { | ||||||
|       const tryResolveSpy = spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED'); |       const tryResolveSpy = spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED'); | ||||||
|       host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH'); |       host.tryResolveEntryPoint('SOURCE_PATH', 'TARGET_PATH'); | ||||||
|       expect(tryResolveSpy).toHaveBeenCalledWith('SOURCE_PATH', 'TARGET_PATH/package.json'); |       expect(tryResolveSpy).toHaveBeenCalledWith('SOURCE_PATH', 'TARGET_PATH/package.json'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return the directory containing the result from `tryResolve', () => { |     it('should return the directory containing the result from `tryResolve', () => { | ||||||
|       spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED'); |       spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED'); | ||||||
|       expect(host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH')).toEqual('PATH/TO'); |       expect(host.tryResolveEntryPoint('SOURCE_PATH', 'TARGET_PATH')).toEqual('PATH/TO'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return null if `tryResolve` returns null', () => { |     it('should return null if `tryResolve` returns null', () => { | ||||||
|       spyOn(host, 'tryResolve').and.returnValue(null); |       spyOn(host, 'tryResolve').and.returnValue(null); | ||||||
|       expect(host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH')).toEqual(null); |       expect(host.tryResolveEntryPoint('SOURCE_PATH', 'TARGET_PATH')).toEqual(null); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user