fix(ivy): move the generation of ɵNonEmptyModule to shim construction (#27483)

ngfactory files have a ɵNonEmptyModule constant included if there are no
other exported factory symbols. Previously this extra export was added
dynamically in a TS transformer.

However, synthetically constructed exports don't get properly downleveled
during JS emit, and this generated constant caused issues with downstream
tests.

Instead, this commit configures the shim to always have this export to
begin with, and to filter it out if it's not required.

Testing strategy: covered by existing ngtsc_spec tests which verify the
presence of the ɵNonEmptyModule symbol.

PR Close #27483
This commit is contained in:
Alex Rickabaugh 2018-12-05 13:40:26 -08:00 committed by Igor Minar
parent d553ec2f36
commit 276bdd1f3e
1 changed files with 52 additions and 18 deletions

View File

@ -64,6 +64,11 @@ export class FactoryGenerator implements ShimGenerator {
...varLines,
].join('\n');
}
// Add an extra export to ensure this module has at least one. It'll be removed later in the
// factory transformer if it ends up not being needed.
sourceText += '\nexport const ɵNonEmptyModule = true;';
const genFile = ts.createSourceFile(
genFilePath, sourceText, original.languageVersion, true, ts.ScriptKind.TS);
if (original.moduleName !== undefined) {
@ -114,38 +119,67 @@ function transformFactorySourceFile(
const clone = ts.getMutableClone(file);
const transformedStatements = file.statements.map(stmt => {
// Not every exported factory statement is valid. They were generated before the program was
// analyzed, and before ngtsc knew which symbols were actually NgModules. factoryMap contains
// that knowledge now, so this transform filters the statement list and removes exported factories
// that aren't actually factories.
//
// This could leave the generated factory file empty. To prevent this (it causes issues with
// closure compiler) a 'ɵNonEmptyModule' export was added when the factory shim was created.
// Preserve that export if needed, and remove it otherwise.
//
// Additionally, an import to @angular/core is generated, but the current compilation unit could
// actually be @angular/core, in which case such an import is invalid and should be replaced with
// the proper path to access Ivy symbols in core.
// The filtered set of statements.
const transformedStatements: ts.Statement[] = [];
// The statement identified as the ɵNonEmptyModule export.
let nonEmptyExport: ts.Statement|null = null;
// Consider all the statements.
for (const stmt of file.statements) {
// Look for imports to @angular/core.
if (coreImportsFrom !== null && ts.isImportDeclaration(stmt) &&
ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === '@angular/core') {
// Update the import path to point to the correct file (coreImportsFrom).
const path = relativePathBetween(sourceFilePath, coreImportsFrom.fileName);
if (path !== null) {
return ts.updateImportDeclaration(
stmt, stmt.decorators, stmt.modifiers, stmt.importClause, ts.createStringLiteral(path));
} else {
return ts.createNotEmittedStatement(stmt);
transformedStatements.push(ts.updateImportDeclaration(
stmt, stmt.decorators, stmt.modifiers, stmt.importClause,
ts.createStringLiteral(path)));
}
} else if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) {
const decl = stmt.declarationList.declarations[0];
// If this is the ɵNonEmptyModule export, then save it for later.
if (ts.isIdentifier(decl.name)) {
if (decl.name.text === 'ɵNonEmptyModule') {
nonEmptyExport = stmt;
continue;
}
// Otherwise, check if this export is a factory for a known NgModule, and retain it if so.
const match = STRIP_NG_FACTORY.exec(decl.name.text);
if (match === null || !moduleSymbolNames.has(match[1])) {
// Remove the given factory as it wasn't actually for an NgModule.
return ts.createNotEmittedStatement(stmt);
if (match !== null && moduleSymbolNames.has(match[1])) {
transformedStatements.push(stmt);
}
}
return stmt;
} else {
return stmt;
// Leave the statement alone, as it can't be understood.
transformedStatements.push(stmt);
}
});
if (!transformedStatements.some(ts.isVariableStatement)) {
} else {
// Include non-variable statements (imports, etc).
transformedStatements.push(stmt);
}
}
// Check whether the empty module export is still needed.
if (!transformedStatements.some(ts.isVariableStatement) && nonEmptyExport !== null) {
// If the resulting file has no factories, include an empty export to
// satisfy closure compiler.
transformedStatements.push(ts.createVariableStatement(
[ts.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.createVariableDeclarationList(
[ts.createVariableDeclaration('ɵNonEmptyModule', undefined, ts.createTrue())],
ts.NodeFlags.Const)));
transformedStatements.push(nonEmptyExport);
}
clone.statements = ts.createNodeArray(transformedStatements);
return clone;