UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

354 lines (307 loc) 14.4 kB
import * as path from 'path'; import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; import * as ts from 'typescript'; import {convertDecorators} from './decorator-annotator'; import {processES5} from './es5processor'; import {ModulesManifest} from './modules_manifest'; import * as sourceMapUtils from './source_map_utils'; import {annotate, isDtsFileName} from './tsickle'; /** * Tsickle can perform 2 different precompilation transforms - decorator downleveling * and closurization. Both require tsc to have already type checked their * input, so they can't both be run in one call to tsc. If you only want one of * the transforms, you can specify it in the constructor, if you want both, you'll * have to specify it by calling reconfigureForRun() with the appropriate Pass. */ export enum Pass { NONE, DECORATOR_DOWNLEVEL, CLOSURIZE } export interface Options { googmodule?: boolean; es5Mode?: boolean; prelude?: string; /** * If true, convert every type to the Closure {?} type, which means * "don't check types". */ untyped?: boolean; /** * If provided a function that logs an internal warning. * These warnings are not actionable by an end user and should be hidden * by default. */ logWarning?: (warning: ts.Diagnostic) => void; /** If provided, a set of paths whose types should always generate as {?}. */ typeBlackListPaths?: Set<string>; /** * Convert shorthand "/index" imports to full path (include the "/index"). * Annotation will be slower because every import must be resolved. */ convertIndexImportShorthand?: boolean; } /** * Provides hooks to customize TsickleCompilerHost's behavior for different * compilation environments. */ export interface TsickleHost { /** * If true, tsickle and decorator downlevel processing will be skipped for * that file. */ shouldSkipTsickleProcessing(fileName: string): boolean; /** * Takes a context (the current file) and the path of the file to import * and generates a googmodule module name */ pathToModuleName(context: string, importPath: string): string; /** * Tsickle treats warnings as errors, if true, ignore warnings. This might be * useful for e.g. third party code. */ shouldIgnoreWarningsForPath(filePath: string): boolean; /** * If we do googmodule processing, we polyfill module.id, since that's * part of ES6 modules. This function determines what the module.id will be * for each file. */ fileNameToModuleId(fileName: string): string; } const ANNOTATION_SUPPORT = ` interface DecoratorInvocation { type: Function; args?: any[]; } `; /** * TsickleCompilerHost does tsickle processing of input files, including * closure type annotation processing, decorator downleveling and * require -> googmodule rewriting. */ export class TsickleCompilerHost implements ts.CompilerHost { // The manifest of JS modules output by the compiler. public modulesManifest: ModulesManifest = new ModulesManifest(); /** Error messages produced by tsickle, if any. */ public diagnostics: ts.Diagnostic[] = []; /** externs.js files produced by tsickle, if any. */ public externs: {[fileName: string]: string} = {}; private sourceFileToPreexistingSourceMap = new Map<ts.SourceFile, SourceMapGenerator>(); private preexistingSourceMaps = new Map<string, SourceMapGenerator>(); private decoratorDownlevelSourceMaps = new Map<string, SourceMapGenerator>(); private tsickleSourceMaps = new Map<string, SourceMapGenerator>(); private runConfiguration: {oldProgram: ts.Program, pass: Pass}|undefined; constructor( private delegate: ts.CompilerHost, private tscOptions: ts.CompilerOptions, private options: Options, private environment: TsickleHost) {} /** * Tsickle can perform 2 kinds of precompilation source transforms - decorator * downleveling and closurization. They can't be run in the same run of the * typescript compiler, because they both depend on type information that comes * from running the compiler. We need to use the same compiler host to run both * so we have all the source map data when finally write out. Thus if we want * to run both transforms, we call reconfigureForRun() between the calls to * ts.createProgram(). */ public reconfigureForRun(oldProgram: ts.Program, pass: Pass) { this.runConfiguration = {oldProgram, pass}; } getSourceFile( fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void): ts.SourceFile { if (this.runConfiguration === undefined || this.runConfiguration.pass === Pass.NONE) { const sourceFile = this.delegate.getSourceFile(fileName, languageVersion, onError); return this.stripAndStoreExistingSourceMap(sourceFile); } const sourceFile = this.runConfiguration.oldProgram.getSourceFile(fileName); switch (this.runConfiguration.pass) { case Pass.DECORATOR_DOWNLEVEL: return this.downlevelDecorators( sourceFile, this.runConfiguration.oldProgram, fileName, languageVersion); case Pass.CLOSURIZE: return this.closurize( sourceFile, this.runConfiguration.oldProgram, fileName, languageVersion); default: throw new Error('tried to use TsickleCompilerHost with unknown pass enum'); } } writeFile( fileName: string, content: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]): void { if (path.extname(fileName) !== '.map') { if (!isDtsFileName(fileName) && this.tscOptions.inlineSourceMap) { content = this.combineInlineSourceMaps(fileName, content); } if (this.options.googmodule && !isDtsFileName(fileName)) { content = this.convertCommonJsToGoogModule(fileName, content); } } else { content = this.combineSourceMaps(fileName, content); } this.delegate.writeFile(fileName, content, writeByteOrderMark, onError, sourceFiles); } getSourceMapKeyForPathAndName(outputFilePath: string, sourceFileName: string): string { const fileDir = path.dirname(outputFilePath); return this.getCanonicalFileName(path.resolve(fileDir, sourceFileName)); } getSourceMapKeyForSourceFile(sourceFile: ts.SourceFile): string { return this.getCanonicalFileName(path.resolve(sourceFile.path)); } stripAndStoreExistingSourceMap(sourceFile: ts.SourceFile): ts.SourceFile { if (sourceMapUtils.containsInlineSourceMap(sourceFile.text)) { const sourceMapJson = sourceMapUtils.extractInlineSourceMap(sourceFile.text); const sourceMap = sourceMapUtils.sourceMapTextToGenerator(sourceMapJson); const stripedSourceText = sourceMapUtils.removeInlineSourceMap(sourceFile.text); const stripedSourceFile = ts.createSourceFile(sourceFile.fileName, stripedSourceText, sourceFile.languageVersion); this.sourceFileToPreexistingSourceMap.set(stripedSourceFile, sourceMap); return stripedSourceFile; } return sourceFile; } combineSourceMaps(filePath: string, tscSourceMapText: string): string { // We stripe inline source maps off source files before they've been parsed // which is before they have path properties, so we need to construct the // map of sourceMapKey to preexistingSourceMap after the whole program has been // loaded. if (this.sourceFileToPreexistingSourceMap.size > 0 && this.preexistingSourceMaps.size === 0) { this.sourceFileToPreexistingSourceMap.forEach((sourceMap, sourceFile) => { const sourceMapKey = this.getSourceMapKeyForSourceFile(sourceFile); this.preexistingSourceMaps.set(sourceMapKey, sourceMap); }); } const tscSourceMapConsumer = sourceMapUtils.sourceMapTextToConsumer(tscSourceMapText); const tscSourceMapGenerator = sourceMapUtils.sourceMapConsumerToGenerator(tscSourceMapConsumer); if (this.tsickleSourceMaps.size > 0) { // TODO(lucassloan): remove when the .d.ts has the correct types for (const sourceFileName of (tscSourceMapConsumer as any).sources) { const sourceMapKey = this.getSourceMapKeyForPathAndName(filePath, sourceFileName); const tsickleSourceMapGenerator = this.tsickleSourceMaps.get(sourceMapKey)!; const tsickleSourceMapConsumer = sourceMapUtils.sourceMapGeneratorToConsumerWithFileName( tsickleSourceMapGenerator, sourceFileName); tscSourceMapGenerator.applySourceMap(tsickleSourceMapConsumer); } } if (this.decoratorDownlevelSourceMaps.size > 0) { // TODO(lucassloan): remove when the .d.ts has the correct types for (const sourceFileName of (tscSourceMapConsumer as any).sources) { const sourceMapKey = this.getSourceMapKeyForPathAndName(filePath, sourceFileName); const decoratorDownlevelSourceMapGenerator = this.decoratorDownlevelSourceMaps.get(sourceMapKey)!; const decoratorDownlevelSourceMapConsumer = sourceMapUtils.sourceMapGeneratorToConsumerWithFileName( decoratorDownlevelSourceMapGenerator, sourceFileName); tscSourceMapGenerator.applySourceMap(decoratorDownlevelSourceMapConsumer); } } if (this.preexistingSourceMaps.size > 0) { // TODO(lucassloan): remove when the .d.ts has the correct types for (const sourceFileName of (tscSourceMapConsumer as any).sources) { const sourceMapKey = this.getSourceMapKeyForPathAndName(filePath, sourceFileName); const preexistingSourceMapGenerator = this.preexistingSourceMaps.get(sourceMapKey); if (preexistingSourceMapGenerator) { const preexistingSourceMapConsumer = new SourceMapConsumer(preexistingSourceMapGenerator.toJSON()); tscSourceMapGenerator.applySourceMap(preexistingSourceMapConsumer); } } } return tscSourceMapGenerator.toString(); } combineInlineSourceMaps(filePath: string, compiledJsWithInlineSourceMap: string): string { const sourceMapJson = sourceMapUtils.extractInlineSourceMap(compiledJsWithInlineSourceMap); const composedSourceMap = this.combineSourceMaps(filePath, sourceMapJson); return sourceMapUtils.setInlineSourceMap(compiledJsWithInlineSourceMap, composedSourceMap); } convertCommonJsToGoogModule(fileName: string, content: string): string { const moduleId = this.environment.fileNameToModuleId(fileName); let {output, referencedModules} = processES5( fileName, moduleId, content, this.environment.pathToModuleName.bind(this.environment), this.options.es5Mode, this.options.prelude); const moduleName = this.environment.pathToModuleName('', fileName); this.modulesManifest.addModule(fileName, moduleName); for (let referenced of referencedModules) { this.modulesManifest.addReferencedModule(fileName, referenced); } return output; } private downlevelDecorators( sourceFile: ts.SourceFile, program: ts.Program, fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile { this.decoratorDownlevelSourceMaps.set( this.getSourceMapKeyForSourceFile(sourceFile), new SourceMapGenerator()); if (this.environment.shouldSkipTsickleProcessing(fileName)) return sourceFile; let fileContent = sourceFile.text; const converted = convertDecorators(program.getTypeChecker(), sourceFile); if (converted.diagnostics) { this.diagnostics.push(...converted.diagnostics); } if (converted.output === fileContent) { // No changes; reuse the existing parse. return sourceFile; } fileContent = converted.output + ANNOTATION_SUPPORT; this.decoratorDownlevelSourceMaps.set( this.getSourceMapKeyForSourceFile(sourceFile), converted.sourceMap); return ts.createSourceFile(fileName, fileContent, languageVersion, true); } private closurize( sourceFile: ts.SourceFile, program: ts.Program, fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile { this.tsickleSourceMaps.set( this.getSourceMapKeyForSourceFile(sourceFile), new SourceMapGenerator()); let isDefinitions = isDtsFileName(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 && this.environment.shouldSkipTsickleProcessing(fileName)) return sourceFile; let {output, externs, diagnostics, sourceMap} = annotate(program, sourceFile, this.options, this.delegate, this.tscOptions); if (externs) { this.externs[fileName] = externs; } if (this.environment.shouldIgnoreWarningsForPath(sourceFile.path)) { // All diagnostics (including warnings) are treated as errors. // If we've decided to ignore them, just discard them. // Warnings include stuff like "don't use @type in your jsdoc"; tsickle // warns and then fixes up the code to be Closure-compatible anyway. diagnostics = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); } this.diagnostics = diagnostics; this.tsickleSourceMaps.set(this.getSourceMapKeyForSourceFile(sourceFile), sourceMap); return ts.createSourceFile(fileName, output, languageVersion, true); } /** Concatenate all generated externs definitions together into a string. */ getGeneratedExterns(): string { let allExterns = ''; for (let fileName of Object.keys(this.externs)) { allExterns += `// externs from ${fileName}:\n`; allExterns += this.externs[fileName]; } return allExterns; } // Delegate everything else to the original compiler host. fileExists(fileName: string): boolean { return this.delegate.fileExists(fileName); } getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); }; useCaseSensitiveFileNames(): boolean { return this.delegate.useCaseSensitiveFileNames(); } getNewLine(): string { return this.delegate.getNewLine(); } getDirectories(path: string) { return this.delegate.getDirectories(path); } readFile(fileName: string): string { return this.delegate.readFile(fileName); } getDefaultLibFileName(options: ts.CompilerOptions): string { return this.delegate.getDefaultLibFileName(options); } getCanonicalFileName(fileName: string): string { return this.delegate.getCanonicalFileName(fileName); } }