diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 7b73608579..8d311901d7 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -11,6 +11,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import * as api from '../transformers/api'; +import {nocollapseHack} from '../transformers/nocollapse_hack'; import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations'; import {BaseDefDecoratorHandler} from './annotations/src/base_def'; @@ -30,6 +31,7 @@ export class NgtscProgram implements api.Program { private _reflector: TypeScriptReflectionHost|undefined = undefined; private _isCore: boolean|undefined = undefined; private rootDirs: string[]; + private closureCompilerEnabled: boolean; constructor( @@ -43,6 +45,7 @@ export class NgtscProgram implements api.Program { } else { this.rootDirs.push(host.getCurrentDirectory()); } + this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.resourceLoader = host.readResource !== undefined ? new HostResourceLoader(host.readResource.bind(host)) : new FileResourceLoader(); @@ -156,6 +159,8 @@ export class NgtscProgram implements api.Program { if (fileName.endsWith('.d.ts')) { data = sourceFiles.reduce( (data, sf) => this.compilation !.transformedDtsFor(sf.fileName, data), data); + } else if (this.closureCompilerEnabled && fileName.endsWith('.ts')) { + data = nocollapseHack(data); } this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); }; diff --git a/packages/compiler-cli/src/transformers/nocollapse_hack.ts b/packages/compiler-cli/src/transformers/nocollapse_hack.ts new file mode 100644 index 0000000000..92fb1e1b99 --- /dev/null +++ b/packages/compiler-cli/src/transformers/nocollapse_hack.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// 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 = [ + 'ngBaseDef', + 'ngComponentDef', + 'ngDirectiveDef', + 'ngInjectableDef', + 'ngInjectorDef', + 'ngModuleDef', + 'ngPipeDef', +].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'); + +const R3_TSICKLE_DECL_PATTERN = + `(\\/\\*\\*[*\\s]*)(@[^*]+\\*\\/\\s+[^.]+\\.(?:${R3_DEF_NAME_PATTERN});)`; + +const R3_MATCH_TSICKLE_DECL = new RegExp(R3_TSICKLE_DECL_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'; + +const R3_NOCOLLAPSE_TSICKLE_DECL = '$1@nocollapse $2'; + +export function nocollapseHack(contents: string): string { + return contents.replace(R3_MATCH_DEFS, R3_NOCOLLAPSE_DEFS) + .replace(R3_MATCH_TSICKLE_DECL, R3_NOCOLLAPSE_TSICKLE_DECL); +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 72864ecc58..8315f0e1a5 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -22,6 +22,7 @@ import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalRef import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from './inline_resources'; import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions'; import {MetadataCache, MetadataTransformer} from './metadata_cache'; +import {nocollapseHack} from './nocollapse_hack'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; import {StripDecoratorsMetadataTransformer, getDecoratorStripTransformerFactory} from './r3_strip_decorators'; @@ -30,37 +31,6 @@ import {TscPassThroughProgram} from './tsc_pass_through'; 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 * passing individual targetSourceFiles. @@ -300,10 +270,6 @@ 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, @@ -332,7 +298,7 @@ class AngularCompilerProgram implements Program { let genFile: GeneratedFile|undefined; if (this.options.annotateForClosureCompiler && sourceFile && TS.test(sourceFile.fileName)) { - outData = this._annotateR3Properties(outData); + outData = nocollapseHack(outData); } this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles); }; @@ -425,7 +391,7 @@ class AngularCompilerProgram implements Program { } } if (this.options.annotateForClosureCompiler && TS.test(sourceFile.fileName)) { - outData = this._annotateR3Properties(outData); + outData = nocollapseHack(outData); } } this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); diff --git a/packages/compiler-cli/test/transformers/nocollapse_hack_spec.ts b/packages/compiler-cli/test/transformers/nocollapse_hack_spec.ts new file mode 100644 index 0000000000..095fe8af59 --- /dev/null +++ b/packages/compiler-cli/test/transformers/nocollapse_hack_spec.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {nocollapseHack} from '../../src/transformers/nocollapse_hack'; + +describe('@nocollapse hack', () => { + it('should add @nocollapse to a basic class', () => { + const decl = `Foo.ngInjectorDef = define(...);`; + expect(nocollapseHack(decl)).toEqual('/** @nocollapse */ ' + decl); + }); + + it('should add nocollapse to an if (false) declaration of the kind generated by tsickle', () => { + const decl = ` + if (false) { + /** @type {?} */ + Foo.ngInjectorDef; + } + `; + expect(nocollapseHack(decl)).toContain('/** @nocollapse @type {?} */'); + }); +}); \ No newline at end of file