diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 0b045509b6..a8a8e5d696 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -25,7 +25,36 @@ import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; import {getAngularClassTransformerFactory} from './r3_transform'; 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 @@ -223,6 +252,10 @@ class AngularCompilerProgram implements Program { this._emitRender2(parameters); } + private _annotateR3Properties(contents: string): string { + return contents.replace(R3_MATCH_DEFS, R3_NOCOLLAPSE_DEFS); + } + private _emitRender3( {emitFlags = EmitFlags.Default, cancellationToken, customTransformers, emitCallback = defaultEmitCallback}: { @@ -242,6 +275,10 @@ class AngularCompilerProgram implements Program { (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null; 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); }; @@ -309,6 +346,9 @@ class AngularCompilerProgram implements Program { emittedSourceFiles.push(originalFile); } } + if (this.options.annotateForClosureCompiler && TS.test(sourceFile.fileName)) { + outData = this._annotateR3Properties(outData); + } } this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); }; diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index d70889ae94..619dd8615a 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2067,6 +2067,27 @@ describe('ngc transformer command-line', () => { 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', () => { const source = compileService(` import {Injectable} from '@angular/core';