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

@ -22,6 +22,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -22,5 +22,8 @@
"files": [
"index.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts"
]
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true
}
}

View File

@ -23,6 +23,7 @@
"../../system.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,6 +27,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -24,6 +24,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -24,6 +24,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,6 +27,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -24,6 +24,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,6 +27,7 @@
"index.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,6 +27,7 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

File diff suppressed because it is too large Load Diff

3680
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -77,7 +77,7 @@
"source-map-support": "^0.4.2",
"systemjs": "0.18.10",
"ts-api-guardian": "0.1.4",
"tsickle": "^0.1.7",
"tsickle": "^0.2.1",
"tslint": "^3.15.1",
"typescript": "^2.0.2",
"universal-analytics": "^0.3.9",

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {MetadataWriterHost, TsickleHost} from './src/compiler_host';
export {DecoratorDownlevelCompilerHost, MetadataWriterHost} from './src/compiler_host';
export {CodegenExtension, main} from './src/main';
export {default as AngularCompilerOptions} from './src/options';

View File

@ -11,7 +11,7 @@
"license": "MIT",
"repository": {"type":"git","url":"https://github.com/angular/angular.git"},
"dependencies": {
"tsickle": "^0.1.7"
"tsickle": "^0.2"
},
"peerDependencies": {
"typescript": "^2.0.2"

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
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.
const metadataWriter = new MetadataWriterHost(host, newProgram, ngOptions);
tsc.emit(metadataWriter, newProgram);
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;
}
}

View File

@ -0,0 +1,51 @@
/**
* @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 * as ts from 'typescript';
import NgOptions from '../src/options';
import {formatDiagnostics, TsickleCompilerHost} from '../src/compiler_host';
import {writeTempFile} from './test_support';
describe('Compiler Host', () => {
function makeProgram(fileName: string, source: string): [ts.Program, ts.CompilerHost, NgOptions] {
let fn = writeTempFile(fileName, source);
let opts: NgOptions = {
target: ts.ScriptTarget.ES5,
types: [],
genDir: '/tmp',
basePath: '/tmp',
noEmit: true,
};
// TsickleCompilerHost wants a ts.Program, which is the result of
// parsing and typechecking the code before tsickle processing.
// So we must create and run the entire stack of CompilerHost.
let host = ts.createCompilerHost(opts);
let program = ts.createProgram([fn], opts, host);
// To get types resolved, you must first call getPreEmitDiagnostics.
let diags = formatDiagnostics(ts.getPreEmitDiagnostics(program));
expect(diags).toEqual('');
return [program, host, opts];
}
it('inserts JSDoc annotations', () => {
const [program, host, opts] = makeProgram('foo.ts', 'let x: number = 123');
const tsickleHost = new TsickleCompilerHost(host, program, opts);
const f = tsickleHost.getSourceFile(program.getRootFileNames()[0], ts.ScriptTarget.ES5);
expect(f.text).toContain('/** @type {?} */');
});
it('reports diagnostics about existing JSDoc', () => {
const [program, host, opts] =
makeProgram('error.ts', '/** @param {string} x*/ function f(x: string){};');
const tsickleHost = new TsickleCompilerHost(host, program, opts);
const f = tsickleHost.getSourceFile(program.getRootFileNames()[0], ts.ScriptTarget.ES5);
expect(formatDiagnostics(tsickleHost.diagnostics)).toContain('redundant with TypeScript types');
});
});

View File

@ -0,0 +1,209 @@
/**
* @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 * as fs from 'fs';
import * as path from 'path';
import {main} from '../src/main';
import {makeTempDir} from './test_support';
describe('tsc-wrapped', () => {
let basePath: string;
let write: (fileName: string, content: string) => void;
beforeEach(() => {
basePath = makeTempDir();
write = (fileName: string, content: string) => {
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
};
write('decorators.ts', '/** @Annotation */ export var Component: Function;');
write('dep.ts', `
export const A = 1;
export const B = 2;
`);
write('test.ts', `
import {Component} from './decorators';
export * from './dep';
@Component({})
export class Comp {
/**
* Comment that is
* multiple lines
*/
method(x: string): void {}
}
`);
});
function readOut(ext: string) {
return fs.readFileSync(path.join(basePath, 'built', `test.${ext}`), {encoding: 'utf-8'});
}
it('should report error if project not found', () => {
main('not-exist', null as any)
.then(() => fail('should report error'))
.catch(e => expect(e.message).toContain('ENOENT'));
});
it('should pre-process sources', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"module": "es2015"
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true
},
"files": ["test.ts"]
}`);
main(basePath, {basePath})
.then(() => {
const out = readOut('js');
// No helpers since decorators were lowered
expect(out).not.toContain('__decorate');
// Expand `export *`
expect(out).toContain('export { A, B }');
// Annotated for Closure compiler
expect(out).toContain('* @param {?} x');
// Comments should stay multi-line
expect(out).not.toContain('Comment that is multiple lines');
// Decorator is now an annotation
expect(out).toMatch(/Comp.decorators = \[\s+\{ type: Component/);
const decl = readOut('d.ts');
expect(decl).toContain('declare class Comp');
const metadata = readOut('metadata.json');
expect(metadata).toContain('"Comp":{"__symbolic":"class"');
done();
})
.catch(e => done.fail(e));
});
it('should allow all options disabled', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": false,
"module": "es2015"
},
"angularCompilerOptions": {
"annotateForClosureCompiler": false,
"annotationsAs": "decorators",
"skipMetadataEmit": true,
"skipTemplateCodegen": true
},
"files": ["test.ts"]
}`);
main(basePath, {basePath})
.then(() => {
const out = readOut('js');
// TypeScript's decorator emit
expect(out).toContain('__decorate');
// Not annotated for Closure compiler
expect(out).not.toContain('* @param {?} x');
expect(() => fs.accessSync(path.join(basePath, 'built', 'test.metadata.json'))).toThrow();
expect(() => fs.accessSync(path.join(basePath, 'built', 'test.d.ts'))).toThrow();
done();
})
.catch(e => done.fail(e));
});
it('should allow JSDoc annotations without decorator downleveling', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"annotationsAs": "decorators"
},
"files": ["test.ts"]
}`);
main(basePath, {basePath}).then(() => done()).catch(e => done.fail(e));
});
xit('should run quickly (performance baseline)', (done) => {
for (let i = 0; i < 1000; i++) {
write(`input${i}.ts`, `
import {Component} from './decorators';
@Component({})
export class Input${i} {
private __brand: string;
}
`);
}
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"diagnostics": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": false,
"annotationsAs": "decorators",
"skipMetadataEmit": true
},
"include": ["input*.ts"]
}`);
console.time('BASELINE');
main(basePath, {basePath})
.then(() => {
console.timeEnd('BASELINE');
done();
})
.catch(e => done.fail(e));
});
xit('should run quickly (performance test)', (done) => {
for (let i = 0; i < 1000; i++) {
write(`input${i}.ts`, `
import {Component} from './decorators';
@Component({})
export class Input${i} {
private __brand: string;
}
`);
}
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"diagnostics": true,
"skipLibCheck": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true
},
"include": ["input*.ts"]
}`);
console.time('TSICKLE');
main(basePath, {basePath})
.then(() => {
console.timeEnd('TSICKLE');
done();
})
.catch(e => done.fail(e));
});
});

View File

@ -0,0 +1,28 @@
/**
* @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 * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const tmpdir = process.env.TEST_TMPDIR || os.tmpdir();
export function writeTempFile(name: string, contents: string): string {
// TEST_TMPDIR is set by bazel.
const id = (Math.random() * 1000000).toFixed(0);
const fn = path.join(tmpdir, `tmp.${id}.${name}`);
fs.writeFileSync(fn, contents);
return fn;
}
export function makeTempDir(): string {
const id = (Math.random() * 1000000).toFixed(0);
const dir = path.join(tmpdir, `tmp.${id}`);
fs.mkdirSync(dir);
return dir;
}