diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index df85976e20..ae0eb2a663 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -26,14 +26,7 @@ export class TypeScriptNodeEmitter { ...stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null)); const preambleStmts: ts.Statement[] = []; if (preamble) { - if (preamble.startsWith('/*') && preamble.endsWith('*/')) { - 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); + const commentStmt = this.createCommentStatement(sourceFile, preamble); preambleStmts.push(commentStmt); } const sourceStatments = @@ -42,6 +35,19 @@ export class TypeScriptNodeEmitter { const newSourceFile = ts.updateSourceFileNode(sourceFile, sourceStatments); 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 diff --git a/packages/compiler-cli/src/transformers/node_emitter_transform.ts b/packages/compiler-cli/src/transformers/node_emitter_transform.ts index 466664042e..0f2e4f6fb7 100644 --- a/packages/compiler-cli/src/transformers/node_emitter_transform.ts +++ b/packages/compiler-cli/src/transformers/node_emitter_transform.ts @@ -12,26 +12,68 @@ import * as ts from 'typescript'; import {TypeScriptNodeEmitter} from './node_emitter'; import {GENERATED_FILES} from './util'; -const PREAMBLE = `/** -* @fileoverview This file was generated by the Angular template compiler. -* Do not edit. -* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes} -* tslint:disable -*/`; +function getPreamble(original: string) { + return `/** + * @fileoverview This file was generated by the Angular template compiler. Do not edit. + * ${original} + * @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes} + * tslint:disable + */`; +} -export function getAngularEmitterTransformFactory(generatedFiles: Map): () => +/** + * 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, program: ts.Program): () => (sourceFile: ts.SourceFile) => ts.SourceFile { return function() { const emitter = new TypeScriptNodeEmitter(); return function(sourceFile: ts.SourceFile): ts.SourceFile { 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) { - 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; } 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; }; }; } + +/** + * 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(/ ?\*\/$/, ''); +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index e3fa89f936..5ee63237d6 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -399,7 +399,7 @@ class AngularCompilerProgram implements Program { if (!this.options.disableExpressionLowering) { beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram)); } - beforeTs.push(getAngularEmitterTransformFactory(genFiles)); + beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram())); if (customTransformers && customTransformers.beforeTs) { beforeTs.push(...customTransformers.beforeTs); } @@ -628,15 +628,12 @@ class AngularCompilerProgram implements Program { // Filter out generated files for which we didn't generate code. // This can happen as the stub caclulation is not completely exact. // 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); - if (isGenerated) { - if (!genFile || !genFile.stmts || genFile.stmts.length === 0) { - if (this.options.allowEmptyCodegenFiles) { - outData = ''; - } else { - return; - } - } + if (isGenerated && !this.options.allowEmptyCodegenFiles && + (!genFile || !genFile.stmts || genFile.stmts.length === 0)) { + return; } if (baseFile) { sourceFiles = sourceFiles ? [...sourceFiles, baseFile] : [baseFile]; diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index f9da91cbbc..f282628c6e 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -249,6 +249,90 @@ describe('ngc transformer command-line', () => { .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', () => { writeConfig(`{ "extends": "./tsconfig-base.json", diff --git a/packages/compiler-cli/test/transformers/program_spec.ts b/packages/compiler-cli/test/transformers/program_spec.ts index b54248d5cc..fa387a7098 100644 --- a/packages/compiler-cli/test/transformers/program_spec.ts +++ b/packages/compiler-cli/test/transformers/program_spec.ts @@ -437,7 +437,8 @@ describe('ng program', () => { sf => sf.fileName === path.join(testSupport.basePath, checks.originalFileName))) .toBe(true); 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 { expect(writeData !.data).not.toBe(''); }