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 * as ts from 'typescript';
|
||||||
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
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 {DependencyHostBase} from './dependency_host';
|
||||||
import {ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';
|
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.
|
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
|
||||||
const sf =
|
const sf =
|
||||||
ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
|
ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
|
||||||
|
const requireCalls: RequireCall[] = [];
|
||||||
|
|
||||||
for (const statement of sf.statements) {
|
for (const stmt of sf.statements) {
|
||||||
const declarations =
|
if (ts.isVariableStatement(stmt)) {
|
||||||
ts.isVariableStatement(statement) ? statement.declarationList.declarations : [];
|
// Regular import(s):
|
||||||
for (const declaration of declarations) {
|
// `var foo = require('...')` or `var foo = require('...'), bar = require('...')`
|
||||||
if (declaration.initializer && isRequireCall(declaration.initializer)) {
|
const declarations = stmt.declarationList.declarations;
|
||||||
const importPath = declaration.initializer.arguments[0].text;
|
for (const declaration of declarations) {
|
||||||
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
|
if ((declaration.initializer !== undefined) && isRequireCall(declaration.initializer)) {
|
||||||
if (resolvedModule) {
|
requireCalls.push(declaration.initializer);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,142 @@ runInEachFileSystem(() => {
|
||||||
expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true);
|
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', () => {
|
it('should capture missing external imports', () => {
|
||||||
const {dependencies, missing, deepImports} = createDependencyInfo();
|
const {dependencies, missing, deepImports} = createDependencyInfo();
|
||||||
host.collectDependencies(
|
host.collectDependencies(
|
||||||
|
@ -224,16 +360,102 @@ runInEachFileSystem(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function commonJs(importPaths: string[], exportNames: string[] = []) {
|
interface ImportsPerType {
|
||||||
const commonJsRequires =
|
// var foo = require('...');
|
||||||
importPaths
|
varDeclaration?: string[];
|
||||||
.map(
|
|
||||||
p =>
|
// var foo = require('...'), bar = require('...');
|
||||||
`var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`)
|
varDeclarations?: string[][];
|
||||||
.join('\n');
|
|
||||||
|
// 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 =
|
const exportStatements =
|
||||||
exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n');
|
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