Lucas Sloan 5bccff0d7a 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
2017-02-07 16:16:29 -06:00

154 lines
5.6 KiB
TypeScript

/**
* @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 * as tsickle from 'tsickle';
import * as ts from 'typescript';
import {check, tsc} from './tsc';
import NgOptions from './options';
import {MetadataWriterHost} from './compiler_host';
import {CliOptions} from './cli_options';
import {VinylFile, isVinylFile} from './vinyl_file';
export {UserError} from './tsc';
export type CodegenExtension =
(ngOptions: NgOptions, cliOptions: CliOptions, program: ts.Program, host: ts.CompilerHost) =>
Promise<void>;
export function main(
project: string | VinylFile, cliOptions: CliOptions, codegen?: CodegenExtension,
options?: ts.CompilerOptions): Promise<any> {
try {
let projectDir = project;
// project is vinyl like file object
if (isVinylFile(project)) {
projectDir = path.dirname(project.path);
}
// project is path to project file
else if (fs.lstatSync(project).isFile()) {
projectDir = path.dirname(project);
}
// file names in tsconfig are resolved relative to this absolute path
const basePath = path.resolve(process.cwd(), cliOptions.basePath || projectDir);
// read the configuration options from wherever you store them
const {parsed, ngOptions} = tsc.readConfiguration(project, basePath, options);
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);
// HACK: patch the realpath to solve symlink issue here:
// https://github.com/Microsoft/TypeScript/issues/9552
// todo(misko): remove once facade symlinks are removed
host.realpath = (path) => path;
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(() => {
if (diagnostics) console.timeEnd('NG codegen');
let definitionsHost = host;
if (!ngOptions.skipMetadataEmit) {
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;
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');
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(tsickleCompilerHost);
check(tsickleCompilerHost.diagnostics);
preprocessHost = tsickleCompilerHost;
if (diagnostics) console.timeEnd('NG downlevel');
}
if (ngOptions.annotateForClosureCompiler) {
if (diagnostics) console.time('NG JSDoc');
tsickleCompilerHost.reconfigureForRun(programForJsEmit, tsickle.Pass.CLOSURIZE);
programForJsEmit = createProgram(tsickleCompilerHost);
check(tsickleCompilerHost.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) {
return Promise.reject(e);
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
let {options, fileNames, errors} = (ts as any).parseCommandLine(args);
check(errors);
const project = options.project || '.';
// TODO(alexeagle): command line should be TSC-compatible, remove "CliOptions" here
const cliOptions = new CliOptions(require('minimist')(args));
main(project, cliOptions, null, options)
.then((exitCode: any) => process.exit(exitCode))
.catch((e: any) => {
console.error(e.stack);
console.error('Compilation failed');
process.exit(1);
});
}