fix(tsc-wrapped): use tsickle's new source map composition feature (#14150)

Tsickle transforms typescript code, which can change the location of code.
This can cause issues such as runtime stack traces reporting that errors
were raised on the incorrect line in the orginal source.  This change replaces
the DecoratorDownlevelCompilerHost and the TsickleCompilerHost with tsickle's
TsickleCompilerHost, which automatically composes tsickle's source maps with
typescript's source maps, so that line numbers are correctly reported at
runtime.

PR Close #14150
This commit is contained in:
Lucas Sloan
2017-01-27 10:36:46 -08:00
committed by Miško Hevery
parent 2e1413016e
commit 5bccff0d7a
8 changed files with 1223 additions and 127 deletions

View File

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

View File

@ -50,64 +50,6 @@ export abstract class DelegatingHost implements ts.CompilerHost {
directoryExists = (directoryName: string) => this.delegate.directoryExists(directoryName);
}
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 =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => {
const originalContent = this.delegate.readFile(fileName);
let newContent = originalContent;
if (!/\.d\.ts$/.test(fileName)) {
try {
const converted = tsickle.convertDecorators(
this.program.getTypeChecker(), this.program.getSourceFile(fileName));
if (converted.diagnostics) {
this.diagnostics.push(...converted.diagnostics);
}
newContent = converted.output + this.ANNOTATION_SUPPORT;
} catch (e) {
console.error('Cannot convertDecorators on file', fileName);
throw e;
}
}
return ts.createSourceFile(fileName, newContent, languageVersion, true);
}
}
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;
const es2015Target = this.options.target == ts.ScriptTarget.ES2015; // This covers ES6 too
let {output, externs, diagnostics} = tsickle.annotate(
this.oldProgram, sourceFile, {untyped: true, convertIndexImportShorthand: es2015Target},
this.delegate, this.options);
this.diagnostics = diagnostics;
return ts.createSourceFile(fileName, output, languageVersion, true);
}
}
const IGNORED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$/;
export class MetadataWriterHost extends DelegatingHost {

View File

@ -8,12 +8,13 @@
import * as fs from 'fs';
import * as path from 'path';
import * as tsickle from 'tsickle';
import * as ts from 'typescript';
import {check, tsc} from './tsc';
import NgOptions from './options';
import {MetadataWriterHost, DecoratorDownlevelCompilerHost, TsickleCompilerHost} from './compiler_host';
import {MetadataWriterHost} from './compiler_host';
import {CliOptions} from './cli_options';
import {VinylFile, isVinylFile} from './vinyl_file';
@ -77,22 +78,40 @@ export function main(
let preprocessHost = host;
let programForJsEmit = programWithCodegen;
const tsickleCompilerHostOptions: tsickle.Options = {
googmodule: false,
untyped: true,
convertIndexImportShorthand:
ngOptions.target === ts.ScriptTarget.ES2015, // This covers ES6 too
};
const tsickleHost: tsickle.TsickleHost = {
shouldSkipTsickleProcessing: (fileName) => false,
pathToModuleName: (context, importPath) => '',
shouldIgnoreWarningsForPath: (filePath) => false,
fileNameToModuleId: (fileName) => fileName,
};
const tsickleCompilerHost = new tsickle.TsickleCompilerHost(
preprocessHost, ngOptions, tsickleCompilerHostOptions, tsickleHost);
if (ngOptions.annotationsAs !== 'decorators') {
if (diagnostics) console.time('NG downlevel');
const downlevelHost = new DecoratorDownlevelCompilerHost(preprocessHost, programForJsEmit);
tsickleCompilerHost.reconfigureForRun(programForJsEmit, tsickle.Pass.DECORATOR_DOWNLEVEL);
// A program can be re-used only once; save the programWithCodegen to be reused by
// metadataWriter
programForJsEmit = createProgram(downlevelHost);
check(downlevelHost.diagnostics);
preprocessHost = downlevelHost;
programForJsEmit = createProgram(tsickleCompilerHost);
check(tsickleCompilerHost.diagnostics);
preprocessHost = tsickleCompilerHost;
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);
tsickleCompilerHost.reconfigureForRun(programForJsEmit, tsickle.Pass.CLOSURIZE);
programForJsEmit = createProgram(tsickleCompilerHost);
check(tsickleCompilerHost.diagnostics);
if (diagnostics) console.timeEnd('NG JSDoc');
}

View File

@ -1,51 +0,0 @@
/**
* @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

@ -238,4 +238,30 @@ describe('tsc-wrapped', () => {
})
.catch(e => done.fail(e));
});
it('should produce valid source maps', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"moduleResolution": "node",
"target": "es2015",
"sourceMap": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true
},
"files": ["test.ts"]
}`);
main(basePath, {basePath})
.then(() => {
const out = readOut('js.map');
expect(out).toContain('"sources":["../test.ts"]');
done();
})
.catch(e => done.fail(e));
});
});