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:
parent
7ba0cb7c93
commit
a0c4b2d8f0
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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 {?} */');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue