fix(ngcc): correctly detect dependencies in CommonJS (#34528)

Previously, `CommonJsDependencyHost.collectDependencies()` would only
find dependencies via imports of the form `var foo = require('...');` or
`var foo = require('...'), bar = require('...');` However, CommonJS
files can have imports in many different forms. By failing to recognize
other forms of imports, the associated dependencies were missed, which
in turn resulted in entry-points being compiled out-of-order and failing
due to that.

While we cannot easily capture all different types of imports, this
commit enhances `CommonJsDependencyHost` to recognize the following
common forms of imports:

- Imports in property assignments. E.g.:
  `exports.foo = require('...');` or
  `module.exports = {foo: require('...')};`

- Imports for side-effects only. E.g.:
  `require('...');`

- Star re-exports (with both emitted and imported heleprs). E.g.:
  `__export(require('...'));` or
  `tslib_1.__exportStar(require('...'), exports);`

PR Close #34528
This commit is contained in:
George Kalpakas
2019-12-05 21:02:57 +02:00
committed by atscott
parent f48c81c1ca
commit ff02ddfd9b
2 changed files with 296 additions and 35 deletions

View File

@ -7,7 +7,7 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {isRequireCall} from '../host/commonjs_umd_utils';
import {RequireCall, isReexportStatement, isRequireCall} from '../host/commonjs_umd_utils';
import {DependencyHostBase} from './dependency_host';
import {ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';
@ -40,33 +40,72 @@ export class CommonJsDependencyHost extends DependencyHostBase {
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
const sf =
ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
const requireCalls: RequireCall[] = [];
for (const statement of sf.statements) {
const declarations =
ts.isVariableStatement(statement) ? statement.declarationList.declarations : [];
for (const declaration of declarations) {
if (declaration.initializer && isRequireCall(declaration.initializer)) {
const importPath = declaration.initializer.arguments[0].text;
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
if (resolvedModule) {
if (resolvedModule instanceof ResolvedRelativeModule) {
const internalDependency = resolvedModule.modulePath;
if (!alreadySeen.has(internalDependency)) {
alreadySeen.add(internalDependency);
this.recursivelyCollectDependencies(
internalDependency, dependencies, missing, deepImports, alreadySeen);
}
} else {
if (resolvedModule instanceof ResolvedDeepImport) {
deepImports.add(resolvedModule.importPath);
} else {
dependencies.add(resolvedModule.entryPointPath);
}
}
} else {
missing.add(importPath);
for (const stmt of sf.statements) {
if (ts.isVariableStatement(stmt)) {
// Regular import(s):
// `var foo = require('...')` or `var foo = require('...'), bar = require('...')`
const declarations = stmt.declarationList.declarations;
for (const declaration of declarations) {
if ((declaration.initializer !== undefined) && isRequireCall(declaration.initializer)) {
requireCalls.push(declaration.initializer);
}
}
} else if (ts.isExpressionStatement(stmt)) {
if (isRequireCall(stmt.expression)) {
// Import for the side-effects only:
// `require('...')`
requireCalls.push(stmt.expression);
} else if (isReexportStatement(stmt)) {
// Re-export in one of the following formats:
// - `__export(require('...'))`
// - `__export(<identifier>)`
// - `tslib_1.__exportStar(require('...'), exports)`
// - `tslib_1.__exportStar(<identifier>, exports)`
const firstExportArg = stmt.expression.arguments[0];
if (isRequireCall(firstExportArg)) {
// Re-export with `require()` call:
// `__export(require('...'))` or `tslib_1.__exportStar(require('...'), exports)`
requireCalls.push(firstExportArg);
}
} else if (
ts.isBinaryExpression(stmt.expression) &&
(stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken)) {
if (isRequireCall(stmt.expression.right)) {
// Import with assignment. E.g.:
// `exports.foo = require('...')`
requireCalls.push(stmt.expression.right);
} else if (ts.isObjectLiteralExpression(stmt.expression.right)) {
// Import in object literal. E.g.:
// `module.exports = {foo: require('...')}`
stmt.expression.right.properties.forEach(prop => {
if (ts.isPropertyAssignment(prop) && isRequireCall(prop.initializer)) {
requireCalls.push(prop.initializer);
}
});
}
}
}
}
const importPaths = new Set(requireCalls.map(call => call.arguments[0].text));
for (const importPath of importPaths) {
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
if (resolvedModule === null) {
missing.add(importPath);
} else if (resolvedModule instanceof ResolvedRelativeModule) {
const internalDependency = resolvedModule.modulePath;
if (!alreadySeen.has(internalDependency)) {
alreadySeen.add(internalDependency);
this.recursivelyCollectDependencies(
internalDependency, dependencies, missing, deepImports, alreadySeen);
}
} else if (resolvedModule instanceof ResolvedDeepImport) {
deepImports.add(resolvedModule.importPath);
} else {
dependencies.add(resolvedModule.entryPointPath);
}
}
}