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:
parent
eb6e1af46d
commit
cfbb1a1e77
|
@ -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,35 +40,74 @@ 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 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 && isRequireCall(declaration.initializer)) {
|
||||
const importPath = declaration.initializer.arguments[0].text;
|
||||
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) {
|
||||
if (resolvedModule instanceof ResolvedRelativeModule) {
|
||||
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) {
|
||||
} else if (resolvedModule instanceof ResolvedDeepImport) {
|
||||
deepImports.add(resolvedModule.importPath);
|
||||
} else {
|
||||
dependencies.add(resolvedModule.entryPointPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
missing.add(importPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -143,6 +143,142 @@ runInEachFileSystem(() => {
|
|||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should recognize imports in a variable declaration list', () => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/test/index.js'),
|
||||
contents: commonJs({
|
||||
varDeclarations: [
|
||||
['lib_1/sub_1', 'lib_1/sub_2'],
|
||||
],
|
||||
}),
|
||||
},
|
||||
{name: _('/test/package.json'), contents: '{"main": "./index.js"}'},
|
||||
{name: _('/test/index.metadata.json'), contents: 'MOCK METADATA'},
|
||||
]);
|
||||
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(_('/test/index.js'), {dependencies, missing, deepImports});
|
||||
|
||||
expect(dependencies.size).toBe(2);
|
||||
expect(missing.size).toBe(0);
|
||||
expect(deepImports.size).toBe(0);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should recognize imports as property assignments (on existing object)', () => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/test/index.js'),
|
||||
contents: commonJs({
|
||||
propAssignment: ['lib_1/sub_1', 'lib_1/sub_2'],
|
||||
}),
|
||||
},
|
||||
{name: _('/test/package.json'), contents: '{"main": "./index.js"}'},
|
||||
{name: _('/test/index.metadata.json'), contents: 'MOCK METADATA'},
|
||||
]);
|
||||
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(_('/test/index.js'), {dependencies, missing, deepImports});
|
||||
|
||||
expect(dependencies.size).toBe(2);
|
||||
expect(missing.size).toBe(0);
|
||||
expect(deepImports.size).toBe(0);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should recognize imports as property assignments (in object literal)', () => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/test/index.js'),
|
||||
contents: commonJs({
|
||||
inObjectLiteral: ['lib_1/sub_1', 'lib_1/sub_2'],
|
||||
}),
|
||||
},
|
||||
{name: _('/test/package.json'), contents: '{"main": "./index.js"}'},
|
||||
{name: _('/test/index.metadata.json'), contents: 'MOCK METADATA'},
|
||||
]);
|
||||
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(_('/test/index.js'), {dependencies, missing, deepImports});
|
||||
|
||||
expect(dependencies.size).toBe(2);
|
||||
expect(missing.size).toBe(0);
|
||||
expect(deepImports.size).toBe(0);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should recognize imports used for their side-effects only', () => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/test/index.js'),
|
||||
contents: commonJs({
|
||||
forSideEffects: ['lib_1/sub_1', 'lib_1/sub_2'],
|
||||
}),
|
||||
},
|
||||
{name: _('/test/package.json'), contents: '{"main": "./index.js"}'},
|
||||
{name: _('/test/index.metadata.json'), contents: 'MOCK METADATA'},
|
||||
]);
|
||||
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(_('/test/index.js'), {dependencies, missing, deepImports});
|
||||
|
||||
expect(dependencies.size).toBe(2);
|
||||
expect(missing.size).toBe(0);
|
||||
expect(deepImports.size).toBe(0);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should recognize star re-exports (with both emitted and imported helpers)', () => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/test/index.js'),
|
||||
contents: commonJs({
|
||||
reExportsWithEmittedHelper: ['lib_1', 'lib_1/sub_1'],
|
||||
reExportsWithImportedHelper: ['lib_1', 'lib_1/sub_2'],
|
||||
}),
|
||||
},
|
||||
{name: _('/test/package.json'), contents: '{"main": "./index.js"}'},
|
||||
{name: _('/test/index.metadata.json'), contents: 'MOCK METADATA'},
|
||||
]);
|
||||
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(_('/test/index.js'), {dependencies, missing, deepImports});
|
||||
|
||||
expect(dependencies.size).toBe(3);
|
||||
expect(missing.size).toBe(0);
|
||||
expect(deepImports.size).toBe(0);
|
||||
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should not get confused by re-exports with a separate `require()` call', () => {
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/test/index.js'),
|
||||
contents: commonJs({
|
||||
reExportsWithoutRequire: ['lib_1', 'lib_1/sub_2'],
|
||||
}),
|
||||
},
|
||||
{name: _('/test/package.json'), contents: '{"main": "./index.js"}'},
|
||||
{name: _('/test/index.metadata.json'), contents: 'MOCK METADATA'},
|
||||
]);
|
||||
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(_('/test/index.js'), {dependencies, missing, deepImports});
|
||||
|
||||
expect(dependencies.size).toBe(2);
|
||||
expect(missing.size).toBe(0);
|
||||
expect(deepImports.size).toBe(0);
|
||||
expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true);
|
||||
expect(dependencies.has(_('/node_modules/lib_1/sub_2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should capture missing external imports', () => {
|
||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||
host.collectDependencies(
|
||||
|
@ -224,16 +360,102 @@ runInEachFileSystem(() => {
|
|||
});
|
||||
});
|
||||
|
||||
function commonJs(importPaths: string[], exportNames: string[] = []) {
|
||||
const commonJsRequires =
|
||||
importPaths
|
||||
.map(
|
||||
p =>
|
||||
`var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`)
|
||||
.join('\n');
|
||||
interface ImportsPerType {
|
||||
// var foo = require('...');
|
||||
varDeclaration?: string[];
|
||||
|
||||
// var foo = require('...'), bar = require('...');
|
||||
varDeclarations?: string[][];
|
||||
|
||||
// exports.foo = require('...');
|
||||
propAssignment?: string[];
|
||||
|
||||
// module.exports = {foo: require('...')};
|
||||
inObjectLiteral?: string[];
|
||||
|
||||
// require('...');
|
||||
forSideEffects?: string[];
|
||||
|
||||
// __export(require('...'));
|
||||
reExportsWithEmittedHelper?: string[];
|
||||
|
||||
// tslib_1.__exportStar(require('...'), exports);
|
||||
reExportsWithImportedHelper?: string[];
|
||||
|
||||
// var foo = require('...');
|
||||
// __export(foo);
|
||||
reExportsWithoutRequire?: string[];
|
||||
}
|
||||
|
||||
function commonJs(importsPerType: ImportsPerType | string[], exportNames: string[] = []): string {
|
||||
if (Array.isArray(importsPerType)) {
|
||||
importsPerType = {varDeclaration: importsPerType};
|
||||
}
|
||||
|
||||
const importStatements = generateImportStatements(importsPerType);
|
||||
const exportStatements =
|
||||
exportNames.map(e => `exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
|
||||
return `${commonJsRequires}
|
||||
${exportStatements}`;
|
||||
|
||||
return `${importStatements}\n\n${exportStatements}`;
|
||||
}
|
||||
|
||||
function generateImportStatements(importsPerType: ImportsPerType): string {
|
||||
const importStatements: string[] = [];
|
||||
|
||||
const {
|
||||
varDeclaration: importsOfTypeVarDeclaration = [],
|
||||
varDeclarations: importsOfTypeVarDeclarations = [],
|
||||
propAssignment: importsOfTypePropAssignment = [],
|
||||
inObjectLiteral: importsOfTypeInObjectLiteral = [],
|
||||
forSideEffects: importsOfTypeForSideEffects = [],
|
||||
reExportsWithEmittedHelper: importsOfTypeReExportsWithEmittedHelper = [],
|
||||
reExportsWithImportedHelper: importsOfTypeReExportsWithImportedHelper = [],
|
||||
reExportsWithoutRequire: importsOfTypeReExportsWithoutRequire = [],
|
||||
} = importsPerType;
|
||||
|
||||
// var foo = require('...');
|
||||
importsOfTypeVarDeclaration.forEach(
|
||||
p => { importStatements.push(`var ${pathToVarName(p)} = require('${p}');`); });
|
||||
|
||||
// var foo = require('...'), bar = require('...');
|
||||
importsOfTypeVarDeclarations.forEach(pp => {
|
||||
const declarations = pp.map(p => `${pathToVarName(p)} = require('${p}')`);
|
||||
importStatements.push(`var ${declarations.join(', ')};`);
|
||||
});
|
||||
|
||||
// exports.foo = require('...');
|
||||
importsOfTypePropAssignment.forEach(
|
||||
p => { importStatements.push(`exports.${pathToVarName(p)} = require('${p}');`); });
|
||||
|
||||
// module.exports = {foo: require('...')};
|
||||
const propAssignments =
|
||||
importsOfTypeInObjectLiteral.map(p => `\n ${pathToVarName(p)}: require('${p}')`)
|
||||
.join(', ');
|
||||
importStatements.push(`module.exports = {${propAssignments}\n};`);
|
||||
|
||||
// require('...');
|
||||
importsOfTypeForSideEffects.forEach(p => { importStatements.push(`require('${p}');`); });
|
||||
|
||||
// __export(require('...'));
|
||||
importsOfTypeReExportsWithEmittedHelper.forEach(
|
||||
p => { importStatements.push(`__export(require('${p}'));`); });
|
||||
|
||||
// tslib_1.__exportStar(require('...'), exports);
|
||||
importsOfTypeReExportsWithImportedHelper.forEach(
|
||||
p => { importStatements.push(`tslib_1.__exportStar(require('${p}'), exports);`); });
|
||||
|
||||
// var foo = require('...');
|
||||
// __export(foo);
|
||||
importsOfTypeReExportsWithoutRequire.forEach(p => {
|
||||
const varName = pathToVarName(p);
|
||||
importStatements.push(`var ${varName} = require('${p}');`);
|
||||
importStatements.push(`__export(varName);`);
|
||||
});
|
||||
|
||||
return importStatements.join('\n');
|
||||
}
|
||||
|
||||
function pathToVarName(path: string): string {
|
||||
return path.replace(/^@(angular\/)?/, '').replace(/\.{0,2}\//g, '');
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue