fix(compiler-cli): annotate Ivy fields as @nocollapse in closure mode (#22691)

Closure has a transformation which turns:

Service.ngInjectableDef = ...;

into:

Service$ngInjectableDef = ...;

This transformation obviously breaks Ivy in a major way. The solution is
to annotate the fields as @nocollapse. However, Typescript appears to ignore
synthetic comments added to a node during a transformation, so the "right"
way to add these comments doesn't work.

As an interim measure, a post-processing step just before the compiled JS is
written to disk appends the correct comments with a regular expression.

PR Close #22691
This commit is contained in:
Alex Rickabaugh 2018-03-09 10:16:09 -08:00 committed by Kara Erickson
parent f95730b8e2
commit 6e00410e1c
2 changed files with 61 additions and 0 deletions

View File

@ -25,7 +25,36 @@ import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {getAngularClassTransformerFactory} from './r3_transform'; import {getAngularClassTransformerFactory} from './r3_transform';
import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util'; import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
// Closure compiler transforms the form `Service.ngInjectableDef = X` into
// `Service$ngInjectableDef = X`. To prevent this transformation, such assignments need to be
// annotated with @nocollapse. Unfortunately, a bug in Typescript where comments aren't propagated
// through the TS transformations precludes adding the comment via the AST. This workaround detects
// the static assignments to R3 properties such as ngInjectableDef using a regex, as output files
// are written, and applies the annotation through regex replacement.
//
// TODO(alxhub): clean up once fix for TS transformers lands in upstream
//
// Typescript reference issue: https://github.com/Microsoft/TypeScript/issues/22497
// Pattern matching all Render3 property names.
const R3_DEF_NAME_PATTERN = ['ngInjectableDef'].join('|');
// Pattern matching `Identifier.property` where property is a Render3 property.
const R3_DEF_ACCESS_PATTERN = `[^\\s\\.()[\\]]+\.(${R3_DEF_NAME_PATTERN})`;
// Pattern matching a source line that contains a Render3 static property assignment.
// It declares two matching groups - one for the preceding whitespace, the second for the rest
// of the assignment expression.
const R3_DEF_LINE_PATTERN = `^(\\s*)(${R3_DEF_ACCESS_PATTERN} = .*)$`;
// Regex compilation of R3_DEF_LINE_PATTERN. Matching group 1 yields the whitespace preceding the
// assignment, matching group 2 gives the rest of the assignment expressions.
const R3_MATCH_DEFS = new RegExp(R3_DEF_LINE_PATTERN, 'gmu');
// Replacement string that complements R3_MATCH_DEFS. It inserts `/** @nocollapse */` before the
// assignment but after any indentation. Note that this will mess up any sourcemaps on this line
// (though there shouldn't be any, since Render3 properties are synthetic).
const R3_NOCOLLAPSE_DEFS = '$1\/** @nocollapse *\/ $2';
/** /**
* Maximum number of files that are emitable via calling ts.Program.emit * Maximum number of files that are emitable via calling ts.Program.emit
@ -223,6 +252,10 @@ class AngularCompilerProgram implements Program {
this._emitRender2(parameters); this._emitRender2(parameters);
} }
private _annotateR3Properties(contents: string): string {
return contents.replace(R3_MATCH_DEFS, R3_NOCOLLAPSE_DEFS);
}
private _emitRender3( private _emitRender3(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers, {emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: { emitCallback = defaultEmitCallback}: {
@ -242,6 +275,10 @@ class AngularCompilerProgram implements Program {
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null; const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
let genFile: GeneratedFile|undefined; let genFile: GeneratedFile|undefined;
if (this.options.annotateForClosureCompiler && sourceFile &&
TS.test(sourceFile.fileName)) {
outData = this._annotateR3Properties(outData);
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles); this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
}; };
@ -309,6 +346,9 @@ class AngularCompilerProgram implements Program {
emittedSourceFiles.push(originalFile); emittedSourceFiles.push(originalFile);
} }
} }
if (this.options.annotateForClosureCompiler && TS.test(sourceFile.fileName)) {
outData = this._annotateR3Properties(outData);
}
} }
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
}; };

View File

@ -2067,6 +2067,27 @@ describe('ngc transformer command-line', () => {
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/); expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
}); });
it('ngInjectableDef in es5 mode is annotated @nocollapse when closure options are enabled',
() => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"annotateForClosureCompiler": true
},
"files": ["service.ts"]
}`);
const source = compileService(`
import {Injectable} from '@angular/core';
import {Module} from './module';
@Injectable({
scope: Module,
})
export class Service {}
`);
expect(source).toMatch(/\/\*\* @nocollapse \*\/ Service\.ngInjectableDef =/);
});
it('compiles a useValue InjectableDef', () => { it('compiles a useValue InjectableDef', () => {
const source = compileService(` const source = compileService(`
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';