fix(ngcc): do not emit ES2015 code in ES5 files (#33514)

Previously, ngcc's `Renderer` would add some constants in the processed
files which were emitted as ES2015 code (e.g. `const` declarations).
This would result in invalid ES5 generated code that would break when
run on browsers that do not support the emitted format.

This commit fixes it by adding a `printStatement()` method to
`RenderingFormatter`, which can convert statements to JavaScript code in
a suitable format for the corresponding `RenderingFormatter`.
Additionally, the `translateExpression()` and `translateStatement()`
ngtsc helper methods are augmented to accept an extra hint to know
whether the code needs to be translated to ES5 format or not.

Fixes #32665

PR Close #33514
This commit is contained in:
George Kalpakas
2019-11-04 19:29:01 +02:00
committed by Kara Erickson
parent 704775168d
commit 033aba9351
17 changed files with 222 additions and 55 deletions

View File

@ -119,7 +119,8 @@ runInEachFileSystem(() => {
}
const sf = getSourceFileOrError(program, _('/index.ts'));
const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement = translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER);
const tsStatement =
translateStatement(call, im, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}

View File

@ -64,8 +64,9 @@ class IvyVisitor extends Visitor {
res.forEach(field => {
// Translate the initializer for the field into TS nodes.
const exprNode =
translateExpression(field.initializer, this.importManager, this.defaultImportRecorder);
const exprNode = translateExpression(
field.initializer, this.importManager, this.defaultImportRecorder,
ts.ScriptTarget.ES2015);
// Create a static property declaration for the new field.
const property = ts.createProperty(
@ -73,7 +74,9 @@ class IvyVisitor extends Visitor {
undefined, exprNode);
field.statements
.map(stmt => translateStatement(stmt, this.importManager, this.defaultImportRecorder))
.map(
stmt => translateStatement(
stmt, this.importManager, this.defaultImportRecorder, ts.ScriptTarget.ES2015))
.forEach(stmt => statements.push(stmt));
members.push(property);
@ -218,7 +221,8 @@ function transformIvySourceFile(
// Generate the constant statements first, as they may involve adding additional imports
// to the ImportManager.
const constants = constantPool.statements.map(
stmt => translateStatement(stmt, importManager, defaultImportRecorder));
stmt =>
translateStatement(stmt, importManager, defaultImportRecorder, ts.ScriptTarget.ES2015));
// Preserve @fileoverview comments required by Closure, since the location might change as a
// result of adding extra imports and constant pool statements.

View File

@ -100,17 +100,19 @@ export class ImportManager {
}
export function translateExpression(
expression: Expression, imports: ImportManager,
defaultImportRecorder: DefaultImportRecorder): ts.Expression {
expression: Expression, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder,
scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>): ts.Expression {
return expression.visitExpression(
new ExpressionTranslatorVisitor(imports, defaultImportRecorder), new Context(false));
new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget),
new Context(false));
}
export function translateStatement(
statement: Statement, imports: ImportManager,
defaultImportRecorder: DefaultImportRecorder): ts.Statement {
statement: Statement, imports: ImportManager, defaultImportRecorder: DefaultImportRecorder,
scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>): ts.Statement {
return statement.visitStatement(
new ExpressionTranslatorVisitor(imports, defaultImportRecorder), new Context(true));
new ExpressionTranslatorVisitor(imports, defaultImportRecorder, scriptTarget),
new Context(true));
}
export function translateType(type: Type, imports: ImportManager): ts.TypeNode {
@ -120,10 +122,14 @@ export function translateType(type: Type, imports: ImportManager): ts.TypeNode {
class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor {
private externalSourceFiles = new Map<string, ts.SourceMapSource>();
constructor(
private imports: ImportManager, private defaultImportRecorder: DefaultImportRecorder) {}
private imports: ImportManager, private defaultImportRecorder: DefaultImportRecorder,
private scriptTarget: Exclude<ts.ScriptTarget, ts.ScriptTarget.JSON>) {}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: Context): ts.VariableStatement {
const nodeFlags = stmt.hasModifier(StmtModifier.Final) ? ts.NodeFlags.Const : ts.NodeFlags.None;
const nodeFlags =
((this.scriptTarget >= ts.ScriptTarget.ES2015) && stmt.hasModifier(StmtModifier.Final)) ?
ts.NodeFlags.Const :
ts.NodeFlags.None;
return ts.createVariableStatement(
undefined, ts.createVariableDeclarationList(
[ts.createVariableDeclaration(
@ -149,6 +155,11 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
}
visitDeclareClassStmt(stmt: ClassStmt, context: Context) {
if (this.scriptTarget < ts.ScriptTarget.ES2015) {
throw new Error(
`Unsupported mode: Visiting a "declare class" statement (class ${stmt.name}) while ` +
`targeting ${ts.ScriptTarget[this.scriptTarget]}.`);
}
throw new Error('Method not implemented.');
}
@ -200,7 +211,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
const exprContext = context.withExpressionMode;
const lhs = ts.createElementAccess(
expr.receiver.visitExpression(this, exprContext),
expr.index.visitExpression(this, exprContext), );
expr.index.visitExpression(this, exprContext));
const rhs = expr.value.visitExpression(this, exprContext);
const result: ts.Expression = ts.createBinary(lhs, ts.SyntaxKind.EqualsToken, rhs);
return context.isStatement ? result : ts.createParen(result);
@ -252,6 +263,12 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
}
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
if (this.scriptTarget < ts.ScriptTarget.ES2015) {
// This should never happen.
throw new Error(
'Unsupported mode: Visiting a localized string (which produces a tagged template ' +
`literal) ' while targeting ${ts.ScriptTarget[this.scriptTarget]}.`);
}
return visitLocalizedString(ast, context, this);
}
@ -518,7 +535,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
}
visitTypeofExpr(ast: TypeofExpr, context: Context): ts.TypeQueryNode {
let expr = translateExpression(ast.expr, this.imports, NOOP_DEFAULT_IMPORT_RECORDER);
let expr = translateExpression(
ast.expr, this.imports, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
return ts.createTypeQueryNode(expr as ts.Identifier);
}
}

View File

@ -204,7 +204,8 @@ export class Environment {
const ngExpr = this.refEmitter.emit(ref, this.contextFile);
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER);
return translateExpression(
ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
}
/**