Skip to content

Commit 1bdec3e

Browse files
petebacondarwinjasonaden
authored andcommitted
feat(ivy): ngcc - implement CommonJsDependencyHost (angular#30200)
PR Close angular#30200
1 parent 620cd5c commit 1bdec3e

File tree

7 files changed

+305
-13
lines changed

7 files changed

+305
-13
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as ts from 'typescript';
9+
10+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
11+
import {FileSystem} from '../file_system/file_system';
12+
import {isRequireCall} from '../host/commonjs_host';
13+
14+
import {DependencyHost, DependencyInfo} from './dependency_host';
15+
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';
16+
17+
/**
18+
* Helper functions for computing dependencies.
19+
*/
20+
export class CommonJsDependencyHost implements DependencyHost {
21+
constructor(private fs: FileSystem, private moduleResolver: ModuleResolver) {}
22+
23+
/**
24+
* Find all the dependencies for the entry-point at the given path.
25+
*
26+
* @param entryPointPath The absolute path to the JavaScript file that represents an entry-point.
27+
* @returns Information about the dependencies of the entry-point, including those that were
28+
* missing or deep imports into other entry-points.
29+
*/
30+
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo {
31+
const dependencies = new Set<AbsoluteFsPath>();
32+
const missing = new Set<AbsoluteFsPath|PathSegment>();
33+
const deepImports = new Set<AbsoluteFsPath>();
34+
const alreadySeen = new Set<AbsoluteFsPath>();
35+
this.recursivelyFindDependencies(
36+
entryPointPath, dependencies, missing, deepImports, alreadySeen);
37+
return {dependencies, missing, deepImports};
38+
}
39+
40+
/**
41+
* Compute the dependencies of the given file.
42+
*
43+
* @param file An absolute path to the file whose dependencies we want to get.
44+
* @param dependencies A set that will have the absolute paths of resolved entry points added to
45+
* it.
46+
* @param missing A set that will have the dependencies that could not be found added to it.
47+
* @param deepImports A set that will have the import paths that exist but cannot be mapped to
48+
* entry-points, i.e. deep-imports.
49+
* @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck
50+
* in a
51+
* circular dependency loop.
52+
*/
53+
private recursivelyFindDependencies(
54+
file: AbsoluteFsPath, dependencies: Set<AbsoluteFsPath>, missing: Set<string>,
55+
deepImports: Set<AbsoluteFsPath>, alreadySeen: Set<AbsoluteFsPath>): void {
56+
const fromContents = this.fs.readFile(file);
57+
if (!this.hasRequireCalls(fromContents)) {
58+
// Avoid parsing the source file as there are no require calls.
59+
return;
60+
}
61+
62+
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
63+
const sf =
64+
ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
65+
66+
for (const statement of sf.statements) {
67+
const declarations =
68+
ts.isVariableStatement(statement) ? statement.declarationList.declarations : [];
69+
for (const declaration of declarations) {
70+
if (declaration.initializer && isRequireCall(declaration.initializer)) {
71+
const importPath = declaration.initializer.arguments[0].text;
72+
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
73+
if (resolvedModule) {
74+
if (resolvedModule instanceof ResolvedRelativeModule) {
75+
const internalDependency = resolvedModule.modulePath;
76+
if (!alreadySeen.has(internalDependency)) {
77+
alreadySeen.add(internalDependency);
78+
this.recursivelyFindDependencies(
79+
internalDependency, dependencies, missing, deepImports, alreadySeen);
80+
}
81+
} else {
82+
if (resolvedModule instanceof ResolvedDeepImport) {
83+
deepImports.add(resolvedModule.importPath);
84+
} else {
85+
dependencies.add(resolvedModule.entryPointPath);
86+
}
87+
}
88+
} else {
89+
missing.add(importPath);
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
/**
97+
* Check whether a source file needs to be parsed for imports.
98+
* This is a performance short-circuit, which saves us from creating
99+
* a TypeScript AST unnecessarily.
100+
*
101+
* @param source The content of the source file to check.
102+
*
103+
* @returns false if there are definitely no require calls
104+
* in this file, true otherwise.
105+
*/
106+
hasRequireCalls(source: string): boolean { return /require\(['"]/.test(source); }
107+
}

packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
9+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
1010

1111
export interface DependencyHost {
1212
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo;
1313
}
1414

1515
export interface DependencyInfo {
1616
dependencies: Set<AbsoluteFsPath>;
17-
missing: Set<string>;
18-
deepImports: Set<string>;
17+
missing: Set<AbsoluteFsPath|PathSegment>;
18+
deepImports: Set<AbsoluteFsPath>;
1919
}

packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import * as ts from 'typescript';
99

10-
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
10+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
1111
import {FileSystem} from '../file_system/file_system';
1212
import {DependencyHost, DependencyInfo} from './dependency_host';
1313
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';
@@ -28,8 +28,8 @@ export class EsmDependencyHost implements DependencyHost {
2828
*/
2929
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo {
3030
const dependencies = new Set<AbsoluteFsPath>();
31-
const missing = new Set<string>();
32-
const deepImports = new Set<string>();
31+
const missing = new Set<AbsoluteFsPath|PathSegment>();
32+
const deepImports = new Set<AbsoluteFsPath>();
3333
const alreadySeen = new Set<AbsoluteFsPath>();
3434
this.recursivelyFindDependencies(
3535
entryPointPath, dependencies, missing, deepImports, alreadySeen);

packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import * as ts from 'typescript';
99

10-
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
10+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
1111
import {FileSystem} from '../file_system/file_system';
1212
import {getImportsOfUmdModule, parseStatementForUmdModule} from '../host/umd_host';
1313

@@ -31,8 +31,8 @@ export class UmdDependencyHost implements DependencyHost {
3131
*/
3232
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo {
3333
const dependencies = new Set<AbsoluteFsPath>();
34-
const missing = new Set<string>();
35-
const deepImports = new Set<string>();
34+
const missing = new Set<AbsoluteFsPath|PathSegment>();
35+
const deepImports = new Set<AbsoluteFsPath>();
3636
const alreadySeen = new Set<AbsoluteFsPath>();
3737
this.recursivelyFindDependencies(
3838
entryPointPath, dependencies, missing, deepImports, alreadySeen);
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as ts from 'typescript';
9+
10+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
11+
import {CommonJsDependencyHost} from '../../src/dependencies/commonjs_dependency_host';
12+
import {ModuleResolver} from '../../src/dependencies/module_resolver';
13+
import {MockFileSystem} from '../helpers/mock_file_system';
14+
15+
const _ = AbsoluteFsPath.from;
16+
17+
describe('CommonJsDependencyHost', () => {
18+
let host: CommonJsDependencyHost;
19+
beforeEach(() => {
20+
const fs = createMockFileSystem();
21+
host = new CommonJsDependencyHost(fs, new ModuleResolver(fs));
22+
});
23+
24+
describe('getDependencies()', () => {
25+
it('should not generate a TS AST if the source does not contain any require calls', () => {
26+
spyOn(ts, 'createSourceFile');
27+
host.findDependencies(_('/no/imports/or/re-exports/index.js'));
28+
expect(ts.createSourceFile).not.toHaveBeenCalled();
29+
});
30+
31+
it('should resolve all the external imports of the source file', () => {
32+
const {dependencies, missing, deepImports} =
33+
host.findDependencies(_('/external/imports/index.js'));
34+
expect(dependencies.size).toBe(2);
35+
expect(missing.size).toBe(0);
36+
expect(deepImports.size).toBe(0);
37+
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
38+
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
39+
});
40+
41+
it('should resolve all the external re-exports of the source file', () => {
42+
const {dependencies, missing, deepImports} =
43+
host.findDependencies(_('/external/re-exports/index.js'));
44+
expect(dependencies.size).toBe(2);
45+
expect(missing.size).toBe(0);
46+
expect(deepImports.size).toBe(0);
47+
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
48+
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
49+
});
50+
51+
it('should capture missing external imports', () => {
52+
const {dependencies, missing, deepImports} =
53+
host.findDependencies(_('/external/imports-missing/index.js'));
54+
55+
expect(dependencies.size).toBe(1);
56+
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
57+
expect(missing.size).toBe(1);
58+
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true);
59+
expect(deepImports.size).toBe(0);
60+
});
61+
62+
it('should not register deep imports as missing', () => {
63+
// This scenario verifies the behavior of the dependency analysis when an external import
64+
// is found that does not map to an entry-point but still exists on disk, i.e. a deep import.
65+
// Such deep imports are captured for diagnostics purposes.
66+
const {dependencies, missing, deepImports} =
67+
host.findDependencies(_('/external/deep-import/index.js'));
68+
69+
expect(dependencies.size).toBe(0);
70+
expect(missing.size).toBe(0);
71+
expect(deepImports.size).toBe(1);
72+
expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true);
73+
});
74+
75+
it('should recurse into internal dependencies', () => {
76+
const {dependencies, missing, deepImports} =
77+
host.findDependencies(_('/internal/outer/index.js'));
78+
79+
expect(dependencies.size).toBe(1);
80+
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
81+
expect(missing.size).toBe(0);
82+
expect(deepImports.size).toBe(0);
83+
});
84+
85+
it('should handle circular internal dependencies', () => {
86+
const {dependencies, missing, deepImports} =
87+
host.findDependencies(_('/internal/circular_a/index.js'));
88+
expect(dependencies.size).toBe(2);
89+
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
90+
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
91+
expect(missing.size).toBe(0);
92+
expect(deepImports.size).toBe(0);
93+
});
94+
95+
it('should support `paths` alias mappings when resolving modules', () => {
96+
const fs = createMockFileSystem();
97+
host = new CommonJsDependencyHost(fs, new ModuleResolver(fs, {
98+
baseUrl: '/dist',
99+
paths: {
100+
'@app/*': ['*'],
101+
'@lib/*/test': ['lib/*/test'],
102+
}
103+
}));
104+
const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js'));
105+
expect(dependencies.size).toBe(4);
106+
expect(dependencies.has(_('/dist/components'))).toBe(true);
107+
expect(dependencies.has(_('/dist/shared'))).toBe(true);
108+
expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true);
109+
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
110+
expect(missing.size).toBe(0);
111+
expect(deepImports.size).toBe(0);
112+
});
113+
});
114+
115+
function createMockFileSystem() {
116+
return new MockFileSystem({
117+
'/no/imports/or/re-exports/index.js': '// some text but no import-like statements',
118+
'/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}',
119+
'/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA',
120+
'/external/imports/index.js': commonJs(['lib_1', 'lib_1/sub_1']),
121+
'/external/imports/package.json': '{"esm2015": "./index.js"}',
122+
'/external/imports/index.metadata.json': 'MOCK METADATA',
123+
'/external/re-exports/index.js':
124+
commonJs(['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']),
125+
'/external/re-exports/package.json': '{"esm2015": "./index.js"}',
126+
'/external/re-exports/index.metadata.json': 'MOCK METADATA',
127+
'/external/imports-missing/index.js': commonJs(['lib_1', 'missing']),
128+
'/external/imports-missing/package.json': '{"esm2015": "./index.js"}',
129+
'/external/imports-missing/index.metadata.json': 'MOCK METADATA',
130+
'/external/deep-import/index.js': commonJs(['lib_1/deep/import']),
131+
'/external/deep-import/package.json': '{"esm2015": "./index.js"}',
132+
'/external/deep-import/index.metadata.json': 'MOCK METADATA',
133+
'/internal/outer/index.js': commonJs(['../inner']),
134+
'/internal/outer/package.json': '{"esm2015": "./index.js"}',
135+
'/internal/outer/index.metadata.json': 'MOCK METADATA',
136+
'/internal/inner/index.js': commonJs(['lib_1/sub_1'], ['X']),
137+
'/internal/circular_a/index.js': commonJs(['../circular_b', 'lib_1/sub_1'], ['Y']),
138+
'/internal/circular_b/index.js': commonJs(['../circular_a', 'lib_1'], ['X']),
139+
'/internal/circular_a/package.json': '{"esm2015": "./index.js"}',
140+
'/internal/circular_a/index.metadata.json': 'MOCK METADATA',
141+
'/re-directed/index.js': commonJs(['lib_1/sub_2']),
142+
'/re-directed/package.json': '{"esm2015": "./index.js"}',
143+
'/re-directed/index.metadata.json': 'MOCK METADATA',
144+
'/path-alias/index.js':
145+
commonJs(['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']),
146+
'/path-alias/package.json': '{"esm2015": "./index.js"}',
147+
'/path-alias/index.metadata.json': 'MOCK METADATA',
148+
'/node_modules/lib_1/index.d.ts': 'export declare class X {}',
149+
'/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
150+
'/node_modules/lib_1/index.metadata.json': 'MOCK METADATA',
151+
'/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}',
152+
'/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}',
153+
'/node_modules/lib_1/sub_1/package.json':
154+
'{"esm2015": "./index.js", "typings": "./index.d.ts"}',
155+
'/node_modules/lib_1/sub_1/index.metadata.json': 'MOCK METADATA',
156+
'/node_modules/lib_1/sub_2.d.ts': `export * from './sub_2/sub_2';`,
157+
'/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`,
158+
'/node_modules/lib_1/sub_2/package.json':
159+
'{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}',
160+
'/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA',
161+
'/dist/components/index.d.ts': `export declare class MyComponent {};`,
162+
'/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
163+
'/dist/components/index.metadata.json': 'MOCK METADATA',
164+
'/dist/shared/index.d.ts': `import {X} from 'lib_1';\nexport declare class Service {}`,
165+
'/dist/shared/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
166+
'/dist/shared/index.metadata.json': 'MOCK METADATA',
167+
'/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`,
168+
'/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}',
169+
'/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA',
170+
});
171+
}
172+
});
173+
174+
function commonJs(importPaths: string[], exportNames: string[] = []) {
175+
const commonJsRequires =
176+
importPaths
177+
.map(
178+
p =>
179+
`var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`)
180+
.join('\n');
181+
const exportStatements =
182+
exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
183+
return `${commonJsRequires}
184+
${exportStatements}`;
185+
}

packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import * as ts from 'typescript';
99

10-
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
10+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
1111
import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host';
1212
import {ModuleResolver} from '../../src/dependencies/module_resolver';
1313
import {MockFileSystem} from '../helpers/mock_file_system';
@@ -56,7 +56,7 @@ describe('EsmDependencyHost', () => {
5656
expect(dependencies.size).toBe(1);
5757
expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true);
5858
expect(missing.size).toBe(1);
59-
expect(missing.has('missing')).toBe(true);
59+
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true);
6060
expect(deepImports.size).toBe(0);
6161
});
6262

packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import * as ts from 'typescript';
99

10-
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
10+
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
1111
import {ModuleResolver} from '../../src/dependencies/module_resolver';
1212
import {UmdDependencyHost} from '../../src/dependencies/umd_dependency_host';
1313
import {MockFileSystem} from '../helpers/mock_file_system';
@@ -55,7 +55,7 @@ describe('UmdDependencyHost', () => {
5555
expect(dependencies.size).toBe(1);
5656
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
5757
expect(missing.size).toBe(1);
58-
expect(missing.has('missing')).toBe(true);
58+
expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true);
5959
expect(deepImports.size).toBe(0);
6060
});
6161

0 commit comments

Comments
 (0)