fix(ivy): add @nocollapse when writing closure-annotated code (#25775)

Closure requires @nocollapse on Ivy definition static fields in order
to not convert them to standalone constants. However tsickle, the tool
which would ordinarily be responsible for adding @nocollapse, doesn't
properly annotate fields which are added synthetically via transforms.
So this commit adds @nocollapse by applying regular expressions against
code during the final write to disk.

PR Close #25775
This commit is contained in:
Alex Rickabaugh 2018-08-28 14:13:22 -07:00 committed by Igor Minar
parent 7ba0cb7c93
commit a0c4b2d8f0
4 changed files with 92 additions and 37 deletions

View File

@ -11,6 +11,7 @@ import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import * as api from '../transformers/api'; import * as api from '../transformers/api';
import {nocollapseHack} from '../transformers/nocollapse_hack';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations'; import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations';
import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {BaseDefDecoratorHandler} from './annotations/src/base_def';
@ -30,6 +31,7 @@ export class NgtscProgram implements api.Program {
private _reflector: TypeScriptReflectionHost|undefined = undefined; private _reflector: TypeScriptReflectionHost|undefined = undefined;
private _isCore: boolean|undefined = undefined; private _isCore: boolean|undefined = undefined;
private rootDirs: string[]; private rootDirs: string[];
private closureCompilerEnabled: boolean;
constructor( constructor(
@ -43,6 +45,7 @@ export class NgtscProgram implements api.Program {
} else { } else {
this.rootDirs.push(host.getCurrentDirectory()); this.rootDirs.push(host.getCurrentDirectory());
} }
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceLoader = host.readResource !== undefined ? this.resourceLoader = host.readResource !== undefined ?
new HostResourceLoader(host.readResource.bind(host)) : new HostResourceLoader(host.readResource.bind(host)) :
new FileResourceLoader(); new FileResourceLoader();
@ -156,6 +159,8 @@ export class NgtscProgram implements api.Program {
if (fileName.endsWith('.d.ts')) { if (fileName.endsWith('.d.ts')) {
data = sourceFiles.reduce( data = sourceFiles.reduce(
(data, sf) => this.compilation !.transformedDtsFor(sf.fileName, data), data); (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); this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
}; };

View File

@ -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);
}

View File

@ -22,6 +22,7 @@ import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalRef
import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from './inline_resources'; import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from './inline_resources';
import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions'; import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions';
import {MetadataCache, MetadataTransformer} from './metadata_cache'; import {MetadataCache, MetadataTransformer} from './metadata_cache';
import {nocollapseHack} from './nocollapse_hack';
import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {StripDecoratorsMetadataTransformer, getDecoratorStripTransformerFactory} from './r3_strip_decorators'; 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'; 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
* passing individual targetSourceFiles. * passing individual targetSourceFiles.
@ -300,10 +270,6 @@ 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,
@ -332,7 +298,7 @@ class AngularCompilerProgram implements Program {
let genFile: GeneratedFile|undefined; let genFile: GeneratedFile|undefined;
if (this.options.annotateForClosureCompiler && sourceFile && if (this.options.annotateForClosureCompiler && sourceFile &&
TS.test(sourceFile.fileName)) { TS.test(sourceFile.fileName)) {
outData = this._annotateR3Properties(outData); outData = nocollapseHack(outData);
} }
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles); this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
}; };
@ -425,7 +391,7 @@ class AngularCompilerProgram implements Program {
} }
} }
if (this.options.annotateForClosureCompiler && TS.test(sourceFile.fileName)) { if (this.options.annotateForClosureCompiler && TS.test(sourceFile.fileName)) {
outData = this._annotateR3Properties(outData); outData = nocollapseHack(outData);
} }
} }
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);

View File

@ -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 {?} */');
});
});