fix(compiler-cli): merge @fileoverview comments. (#20870)
Previously, this code would unconditionally add a @fileoverview comment to generated files, and only if the contained any code at all. However often existing fileoverview comments should be copied from the file the generated file was originally based off of. This allows users to e.g. include Closure Compiler directives in their original `component.ts` file, which will then automaticallly also apply to code generated from it. This special cases `@license` comments, as Closure disregards directives in comments containing `@license`. PR Close #20870
This commit is contained in:
parent
add3589451
commit
8c52088346
|
@ -26,14 +26,7 @@ export class TypeScriptNodeEmitter {
|
||||||
...stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null));
|
...stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null));
|
||||||
const preambleStmts: ts.Statement[] = [];
|
const preambleStmts: ts.Statement[] = [];
|
||||||
if (preamble) {
|
if (preamble) {
|
||||||
if (preamble.startsWith('/*') && preamble.endsWith('*/')) {
|
const commentStmt = this.createCommentStatement(sourceFile, preamble);
|
||||||
preamble = preamble.substr(2, preamble.length - 4);
|
|
||||||
}
|
|
||||||
const commentStmt = ts.createNotEmittedStatement(sourceFile);
|
|
||||||
ts.setSyntheticLeadingComments(
|
|
||||||
commentStmt,
|
|
||||||
[{kind: ts.SyntaxKind.MultiLineCommentTrivia, text: preamble, pos: -1, end: -1}]);
|
|
||||||
ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue);
|
|
||||||
preambleStmts.push(commentStmt);
|
preambleStmts.push(commentStmt);
|
||||||
}
|
}
|
||||||
const sourceStatments =
|
const sourceStatments =
|
||||||
|
@ -42,6 +35,19 @@ export class TypeScriptNodeEmitter {
|
||||||
const newSourceFile = ts.updateSourceFileNode(sourceFile, sourceStatments);
|
const newSourceFile = ts.updateSourceFileNode(sourceFile, sourceStatments);
|
||||||
return [newSourceFile, converter.getNodeMap()];
|
return [newSourceFile, converter.getNodeMap()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a not emitted statement containing the given comment. */
|
||||||
|
createCommentStatement(sourceFile: ts.SourceFile, comment: string): ts.Statement {
|
||||||
|
if (comment.startsWith('/*') && comment.endsWith('*/')) {
|
||||||
|
comment = comment.substr(2, comment.length - 4);
|
||||||
|
}
|
||||||
|
const commentStmt = ts.createNotEmittedStatement(sourceFile);
|
||||||
|
ts.setSyntheticLeadingComments(
|
||||||
|
commentStmt,
|
||||||
|
[{kind: ts.SyntaxKind.MultiLineCommentTrivia, text: comment, pos: -1, end: -1}]);
|
||||||
|
ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue);
|
||||||
|
return commentStmt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A recorded node is a subtype of the node that is marked as being recoreded. This is used
|
// A recorded node is a subtype of the node that is marked as being recoreded. This is used
|
||||||
|
|
|
@ -12,26 +12,68 @@ import * as ts from 'typescript';
|
||||||
import {TypeScriptNodeEmitter} from './node_emitter';
|
import {TypeScriptNodeEmitter} from './node_emitter';
|
||||||
import {GENERATED_FILES} from './util';
|
import {GENERATED_FILES} from './util';
|
||||||
|
|
||||||
const PREAMBLE = `/**
|
function getPreamble(original: string) {
|
||||||
* @fileoverview This file was generated by the Angular template compiler.
|
return `/**
|
||||||
* Do not edit.
|
* @fileoverview This file was generated by the Angular template compiler. Do not edit.
|
||||||
* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
|
* ${original}
|
||||||
* tslint:disable
|
* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
|
||||||
*/`;
|
* tslint:disable
|
||||||
|
*/`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getAngularEmitterTransformFactory(generatedFiles: Map<string, GeneratedFile>): () =>
|
/**
|
||||||
|
* Returns a transformer that does two things for generated files (ngfactory etc):
|
||||||
|
* - adds a fileoverview JSDoc comment containing Closure Compiler specific "suppress"ions in JSDoc.
|
||||||
|
* The new comment will contain any fileoverview comment text from the original source file this
|
||||||
|
* file was generated from.
|
||||||
|
* - updates generated files that are not in the given map of generatedFiles to have an empty
|
||||||
|
* list of statements as their body.
|
||||||
|
*/
|
||||||
|
export function getAngularEmitterTransformFactory(
|
||||||
|
generatedFiles: Map<string, GeneratedFile>, program: ts.Program): () =>
|
||||||
(sourceFile: ts.SourceFile) => ts.SourceFile {
|
(sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||||
return function() {
|
return function() {
|
||||||
const emitter = new TypeScriptNodeEmitter();
|
const emitter = new TypeScriptNodeEmitter();
|
||||||
return function(sourceFile: ts.SourceFile): ts.SourceFile {
|
return function(sourceFile: ts.SourceFile): ts.SourceFile {
|
||||||
const g = generatedFiles.get(sourceFile.fileName);
|
const g = generatedFiles.get(sourceFile.fileName);
|
||||||
|
const orig = g && program.getSourceFile(g.srcFileUrl);
|
||||||
|
let originalComment = '';
|
||||||
|
if (orig) originalComment = getFileoverviewComment(orig);
|
||||||
|
const preamble = getPreamble(originalComment);
|
||||||
if (g && g.stmts) {
|
if (g && g.stmts) {
|
||||||
const [newSourceFile] = emitter.updateSourceFile(sourceFile, g.stmts, PREAMBLE);
|
const orig = program.getSourceFile(g.srcFileUrl);
|
||||||
|
let originalComment = '';
|
||||||
|
if (orig) originalComment = getFileoverviewComment(orig);
|
||||||
|
const [newSourceFile] = emitter.updateSourceFile(sourceFile, g.stmts, preamble);
|
||||||
return newSourceFile;
|
return newSourceFile;
|
||||||
} else if (GENERATED_FILES.test(sourceFile.fileName)) {
|
} else if (GENERATED_FILES.test(sourceFile.fileName)) {
|
||||||
return ts.updateSourceFileNode(sourceFile, []);
|
// The file should be empty, but emitter.updateSourceFile would still add imports
|
||||||
|
// and various minutiae.
|
||||||
|
// Clear out the source file entirely, only including the preamble comment, so that
|
||||||
|
// ngc produces an empty .js file.
|
||||||
|
return ts.updateSourceFileNode(
|
||||||
|
sourceFile, [emitter.createCommentStatement(sourceFile, preamble)]);
|
||||||
}
|
}
|
||||||
return sourceFile;
|
return sourceFile;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses and returns the comment text (without start and end markers) of a \@fileoverview comment
|
||||||
|
* in the given source file. Returns the empty string if no such comment can be found.
|
||||||
|
*/
|
||||||
|
function getFileoverviewComment(sourceFile: ts.SourceFile): string {
|
||||||
|
const trivia = sourceFile.getFullText().substring(0, sourceFile.getStart());
|
||||||
|
const leadingComments = ts.getLeadingCommentRanges(trivia, 0);
|
||||||
|
if (!leadingComments || leadingComments.length === 0) return '';
|
||||||
|
const comment = leadingComments[0];
|
||||||
|
if (comment.kind !== ts.SyntaxKind.MultiLineCommentTrivia) return '';
|
||||||
|
// Only comments separated with a \n\n from the file contents are considered file-level comments
|
||||||
|
// in TypeScript.
|
||||||
|
if (sourceFile.getFullText().substring(comment.end, comment.end + 2) !== '\n\n') return '';
|
||||||
|
const commentText = sourceFile.getFullText().substring(comment.pos, comment.end);
|
||||||
|
// Closure Compiler ignores @suppress and similar if the comment contains @license.
|
||||||
|
if (commentText.indexOf('@license') !== -1) return '';
|
||||||
|
return commentText.replace(/^\/\*\*/, '').replace(/ ?\*\/$/, '');
|
||||||
|
}
|
||||||
|
|
|
@ -399,7 +399,7 @@ class AngularCompilerProgram implements Program {
|
||||||
if (!this.options.disableExpressionLowering) {
|
if (!this.options.disableExpressionLowering) {
|
||||||
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
|
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
|
||||||
}
|
}
|
||||||
beforeTs.push(getAngularEmitterTransformFactory(genFiles));
|
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
|
||||||
if (customTransformers && customTransformers.beforeTs) {
|
if (customTransformers && customTransformers.beforeTs) {
|
||||||
beforeTs.push(...customTransformers.beforeTs);
|
beforeTs.push(...customTransformers.beforeTs);
|
||||||
}
|
}
|
||||||
|
@ -628,16 +628,13 @@ class AngularCompilerProgram implements Program {
|
||||||
// Filter out generated files for which we didn't generate code.
|
// Filter out generated files for which we didn't generate code.
|
||||||
// This can happen as the stub caclulation is not completely exact.
|
// This can happen as the stub caclulation is not completely exact.
|
||||||
// Note: sourceFile refers to the .ngfactory.ts / .ngsummary.ts file
|
// Note: sourceFile refers to the .ngfactory.ts / .ngsummary.ts file
|
||||||
|
// node_emitter_transform already set the file contents to be empty,
|
||||||
|
// so this code only needs to skip the file if !allowEmptyCodegenFiles.
|
||||||
const isGenerated = GENERATED_FILES.test(outFileName);
|
const isGenerated = GENERATED_FILES.test(outFileName);
|
||||||
if (isGenerated) {
|
if (isGenerated && !this.options.allowEmptyCodegenFiles &&
|
||||||
if (!genFile || !genFile.stmts || genFile.stmts.length === 0) {
|
(!genFile || !genFile.stmts || genFile.stmts.length === 0)) {
|
||||||
if (this.options.allowEmptyCodegenFiles) {
|
|
||||||
outData = '';
|
|
||||||
} else {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if (baseFile) {
|
if (baseFile) {
|
||||||
sourceFiles = sourceFiles ? [...sourceFiles, baseFile] : [baseFile];
|
sourceFiles = sourceFiles ? [...sourceFiles, baseFile] : [baseFile];
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,6 +249,90 @@ describe('ngc transformer command-line', () => {
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('comments', () => {
|
||||||
|
function compileAndRead(contents: string) {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["mymodule.ts"],
|
||||||
|
"angularCompilerOptions": {"allowEmptyCodegenFiles": true}
|
||||||
|
}`);
|
||||||
|
write('mymodule.ts', contents);
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], errorSpy);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
|
||||||
|
const modPath = path.resolve(outDir, 'mymodule.ngfactory.js');
|
||||||
|
expect(fs.existsSync(modPath)).toBe(true);
|
||||||
|
return fs.readFileSync(modPath, {encoding: 'UTF-8'});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should be added', () => {
|
||||||
|
const contents = compileAndRead(`
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
expect(contents).toContain('@fileoverview');
|
||||||
|
expect(contents).toContain('generated by the Angular template compiler');
|
||||||
|
expect(contents).toContain('@suppress {suspiciousCode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be merged with existing fileoverview comments', () => {
|
||||||
|
const contents = compileAndRead(`/** Hello world. */
|
||||||
|
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
expect(contents).toContain('Hello world.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only pick file comments', () => {
|
||||||
|
const contents = compileAndRead(`
|
||||||
|
/** Comment on class. */
|
||||||
|
class MyClass {
|
||||||
|
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(contents).toContain('@fileoverview');
|
||||||
|
expect(contents).not.toContain('Comment on class.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be merged with @license comments', () => {
|
||||||
|
const contents = compileAndRead(`/** @license Some license. */
|
||||||
|
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
expect(contents).toContain('@fileoverview');
|
||||||
|
expect(contents).not.toContain('@license');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be included in empty files', () => {
|
||||||
|
const contents = compileAndRead(`/** My comment. */
|
||||||
|
|
||||||
|
import {Inject, Injectable, Optional} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotAnAngularComponent {}
|
||||||
|
`);
|
||||||
|
expect(contents).toContain('My comment');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should compile with an explicit tsconfig reference', () => {
|
it('should compile with an explicit tsconfig reference', () => {
|
||||||
writeConfig(`{
|
writeConfig(`{
|
||||||
"extends": "./tsconfig-base.json",
|
"extends": "./tsconfig-base.json",
|
||||||
|
|
|
@ -437,7 +437,8 @@ describe('ng program', () => {
|
||||||
sf => sf.fileName === path.join(testSupport.basePath, checks.originalFileName)))
|
sf => sf.fileName === path.join(testSupport.basePath, checks.originalFileName)))
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
if (checks.shouldBeEmpty) {
|
if (checks.shouldBeEmpty) {
|
||||||
expect(writeData !.data).toBe('');
|
// The file should only contain comments (the preamble comment added by ngc).
|
||||||
|
expect(writeData !.data).toMatch(/^(\s*\/\*([^*]|\*[^/])*\*\/\s*)?$/);
|
||||||
} else {
|
} else {
|
||||||
expect(writeData !.data).not.toBe('');
|
expect(writeData !.data).not.toBe('');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue