feature(tsc-wrapped): add option for closure compiler JSDoc annotations

This commit is contained in:
Alex Eagle
2016-11-16 16:42:24 -08:00
committed by Chuck Jazdzewski
parent c1a62e2154
commit 664a6273e1
22 changed files with 2715 additions and 5093 deletions

View File

@ -7,12 +7,20 @@
*/
import {writeFileSync} from 'fs';
import {convertDecorators} from 'tsickle';
import * as tsickle from 'tsickle';
import * as ts from 'typescript';
import NgOptions from './options';
import {MetadataCollector} from './collector';
export function formatDiagnostics(d: ts.Diagnostic[]): string {
const host: ts.FormatDiagnosticsHost = {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getNewLine: () => ts.sys.newLine,
getCanonicalFileName: (f: string) => f
};
return ts.formatDiagnostics(d, host);
}
/**
* Implementation of CompilerHost that forwards all methods to another instance.
@ -41,15 +49,16 @@ export abstract class DelegatingHost implements ts.CompilerHost {
directoryExists = (directoryName: string) => this.delegate.directoryExists(directoryName);
}
export class TsickleHost extends DelegatingHost {
// Additional diagnostics gathered by pre- and post-emit transformations.
public diagnostics: ts.Diagnostic[] = [];
private TSICKLE_SUPPORT = `
export class DecoratorDownlevelCompilerHost extends DelegatingHost {
private ANNOTATION_SUPPORT = `
interface DecoratorInvocation {
type: Function;
args?: any[];
}
`;
/** Error messages produced by tsickle, if any. */
public diagnostics: ts.Diagnostic[] = [];
constructor(delegate: ts.CompilerHost, private program: ts.Program) { super(delegate); }
getSourceFile =
@ -58,12 +67,12 @@ interface DecoratorInvocation {
let newContent = originalContent;
if (!/\.d\.ts$/.test(fileName)) {
try {
const converted = convertDecorators(
const converted = tsickle.convertDecorators(
this.program.getTypeChecker(), this.program.getSourceFile(fileName));
if (converted.diagnostics) {
this.diagnostics.push(...converted.diagnostics);
}
newContent = converted.output + this.TSICKLE_SUPPORT;
newContent = converted.output + this.ANNOTATION_SUPPORT;
} catch (e) {
console.error('Cannot convertDecorators on file', fileName);
throw e;
@ -73,14 +82,35 @@ interface DecoratorInvocation {
};
}
export class TsickleCompilerHost extends DelegatingHost {
/** Error messages produced by tsickle, if any. */
public diagnostics: ts.Diagnostic[] = [];
constructor(
delegate: ts.CompilerHost, private oldProgram: ts.Program, private options: NgOptions) {
super(delegate);
}
getSourceFile =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => {
let sourceFile = this.oldProgram.getSourceFile(fileName);
let isDefinitions = /\.d\.ts$/.test(fileName);
// Don't tsickle-process any d.ts that isn't a compilation target;
// this means we don't process e.g. lib.d.ts.
if (isDefinitions) return sourceFile;
let {output, externs, diagnostics} =
tsickle.annotate(this.oldProgram, sourceFile, {untyped: true});
this.diagnostics = diagnostics;
return ts.createSourceFile(fileName, output, languageVersion, true);
};
}
const IGNORED_FILES = /\.ngfactory\.js$|\.css\.js$|\.css\.shim\.js$/;
export class MetadataWriterHost extends DelegatingHost {
private metadataCollector = new MetadataCollector();
constructor(
delegate: ts.CompilerHost, private program: ts.Program, private ngOptions: NgOptions) {
super(delegate);
}
constructor(delegate: ts.CompilerHost, private ngOptions: NgOptions) { super(delegate); }
private writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) {
// TODO: replace with DTS filePath when https://github.com/Microsoft/TypeScript/pull/8412 is

View File

@ -13,7 +13,7 @@ import * as ts from 'typescript';
import {check, tsc} from './tsc';
import NgOptions from './options';
import {MetadataWriterHost, TsickleHost} from './compiler_host';
import {MetadataWriterHost, DecoratorDownlevelCompilerHost, TsickleCompilerHost} from './compiler_host';
import {CliOptions} from './cli_options';
export type CodegenExtension =
@ -34,6 +34,10 @@ export function main(
// read the configuration options from wherever you store them
const {parsed, ngOptions} = tsc.readConfiguration(project, basePath);
ngOptions.basePath = basePath;
const createProgram = (host: ts.CompilerHost, oldProgram?: ts.Program) =>
ts.createProgram(parsed.fileNames, parsed.options, host, oldProgram);
const diagnostics = (parsed.options as any).diagnostics;
if (diagnostics) (ts as any).performance.enable();
const host = ts.createCompilerHost(parsed.options, true);
@ -42,30 +46,60 @@ export function main(
// todo(misko): remove once facade symlinks are removed
host.realpath = (path) => path;
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const program = createProgram(host);
const errors = program.getOptionsDiagnostics();
check(errors);
if (ngOptions.skipTemplateCodegen || !codegen) {
codegen = () => Promise.resolve(null);
}
if (diagnostics) console.time('NG codegen');
return codegen(ngOptions, cliOptions, program, host).then(() => {
// Create a new program since codegen files were created after making the old program
const newProgram = ts.createProgram(parsed.fileNames, parsed.options, host, program);
tsc.typeCheck(host, newProgram);
// Emit *.js with Decorators lowered to Annotations, and also *.js.map
const tsicklePreProcessor = new TsickleHost(host, newProgram);
tsc.emit(tsicklePreProcessor, newProgram);
if (diagnostics) console.timeEnd('NG codegen');
let definitionsHost = host;
if (!ngOptions.skipMetadataEmit) {
// Emit *.metadata.json and *.d.ts
// Not in the same emit pass with above, because tsickle erases
// decorators which we want to read or document.
// Do this emit second since TypeScript will create missing directories for us
// in the standard emit.
const metadataWriter = new MetadataWriterHost(host, newProgram, ngOptions);
tsc.emit(metadataWriter, newProgram);
definitionsHost = new MetadataWriterHost(host, ngOptions);
}
// Create a new program since codegen files were created after making the old program
let programWithCodegen = createProgram(definitionsHost, program);
tsc.typeCheck(host, programWithCodegen);
let preprocessHost = host;
let programForJsEmit = programWithCodegen;
if (ngOptions.annotationsAs !== 'decorators') {
if (diagnostics) console.time('NG downlevel');
const downlevelHost = new DecoratorDownlevelCompilerHost(preprocessHost, programForJsEmit);
// A program can be re-used only once; save the programWithCodegen to be reused by
// metadataWriter
programForJsEmit = createProgram(downlevelHost);
check(downlevelHost.diagnostics);
preprocessHost = downlevelHost;
if (diagnostics) console.timeEnd('NG downlevel');
}
if (ngOptions.annotateForClosureCompiler) {
if (diagnostics) console.time('NG JSDoc');
const tsickleHost = new TsickleCompilerHost(preprocessHost, programForJsEmit, ngOptions);
programForJsEmit = createProgram(tsickleHost);
check(tsickleHost.diagnostics);
if (diagnostics) console.timeEnd('NG JSDoc');
}
// Emit *.js and *.js.map
tsc.emit(programForJsEmit);
// Emit *.d.ts and maybe *.metadata.json
// Not in the same emit pass with above, because tsickle erases
// decorators which we want to read or document.
// Do this emit second since TypeScript will create missing directories for us
// in the standard emit.
tsc.emit(programWithCodegen);
if (diagnostics) {
(ts as any).performance.forEachMeasure(
(name: string, duration: number) => { console.error(`TS ${name}: ${duration}ms`); });
}
});
} catch (e) {

View File

@ -9,31 +9,43 @@
import * as ts from 'typescript';
interface Options extends ts.CompilerOptions {
// Absolute path to a directory where generated file structure is written
genDir: string;
// Absolute path to a directory where generated file structure is written.
// If unspecified, generated files will be written alongside sources.
genDir?: string;
// Path to the directory containing the tsconfig.json file.
basePath: string;
basePath?: string;
// Don't produce .metadata.json files (they don't work for bundled emit with --out)
skipMetadataEmit: boolean;
skipMetadataEmit?: boolean;
// Produce an error if the metadata written for a class would produce an error if used.
strictMetadataEmit: boolean;
strictMetadataEmit?: boolean;
// Don't produce .ngfactory.ts or .css.shim.ts files
skipTemplateCodegen: boolean;
skipTemplateCodegen?: boolean;
// Whether to generate code for library code.
// If true, produce .ngfactory.ts and .css.shim.ts files for .d.ts inputs.
// Default is true.
generateCodeForLibraries?: boolean;
// Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean;
// Modify how angular annotations are emitted to improve tree-shaking.
annotationsAs?: string; /* 'decorator'|'static field' */
// Default is static fields.
// decorators: Leave the Decorators in-place. This makes compilation faster.
// TypeScript will emit calls to the __decorate helper.
// `--emitDecoratorMetadata` can be used for runtime reflection.
// However, the resulting code will not properly tree-shake.
// static fields: Replace decorators with a static field in the class.
// Allows advanced tree-shakers like Closure Compiler to remove
// unused classes.
annotationsAs?: 'decorators'|'static fields';
// Print extra information while running the compiler
trace: boolean;
trace?: boolean;
// Whether to embed debug information in the compiled templates
debug?: boolean;

View File

@ -11,7 +11,6 @@ import * as path from 'path';
import * as ts from 'typescript';
import AngularCompilerOptions from './options';
import {TsickleHost} from './compiler_host';
/**
* Our interface to the TypeScript standard compiler.
@ -22,7 +21,7 @@ export interface CompilerInterface {
readConfiguration(project: string, basePath: string):
{parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions};
typeCheck(compilerHost: ts.CompilerHost, program: ts.Program): void;
emit(compilerHost: ts.CompilerHost, program: ts.Program): number;
emit(program: ts.Program): number;
}
const DEBUG = false;
@ -134,17 +133,11 @@ export class Tsc implements CompilerInterface {
check(diagnostics);
}
emit(compilerHost: TsickleHost, oldProgram: ts.Program): number {
// Create a program if we are lowering annotations with tsickle.
const program = this.ngOptions.annotationsAs === 'static fields' ?
ts.createProgram(this.parsed.fileNames, this.parsed.options, compilerHost) :
oldProgram;
emit(program: ts.Program): number {
debug('Emitting outputs...');
const emitResult = program.emit();
const diagnostics: ts.Diagnostic[] = [];
diagnostics.push(...emitResult.diagnostics);
check(compilerHost.diagnostics);
return emitResult.emitSkipped ? 1 : 0;
}
}