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:
parent
f95730b8e2
commit
6e00410e1c
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue