diff --git a/dist/main/lang/modules/building.js b/dist/main/lang/modules/building.js index 9b8b0d332..b87af35b1 100644 --- a/dist/main/lang/modules/building.js +++ b/dist/main/lang/modules/building.js @@ -1,6 +1,8 @@ var mkdirp = require('mkdirp'); var path = require('path'); var fs = require('fs'); +var fsUtil_1 = require("../../utils/fsUtil"); +var babel; exports.Not_In_Context = "/* NotInContext */"; function diagnosticToTSError(diagnostic) { var filePath = diagnostic.file.fileName; @@ -29,10 +31,20 @@ function emitFile(proj, filePath) { var startPosition = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); errors.push(diagnosticToTSError(diagnostic)); }); - output.outputFiles.forEach(function (o) { - mkdirp.sync(path.dirname(o.name)); - fs.writeFileSync(o.name, o.text, "utf8"); - }); + { + var sourceMapContents = {}; + output.outputFiles.forEach(function (o) { + mkdirp.sync(path.dirname(o.name)); + var additionalEmits = runExternalTranspiler(o, proj, sourceMapContents); + if (!sourceMapContents[o.name]) { + fs.writeFileSync(o.name, o.text, "utf8"); + } + additionalEmits.forEach(function (a) { + mkdirp.sync(path.dirname(a.name)); + fs.writeFileSync(a.name, a.text, "utf8"); + }); + }); + } var outputFiles = output.outputFiles.map(function (o) { return o.name; }); if (path.extname(filePath) == '.d.ts') { outputFiles.push(filePath); @@ -61,3 +73,71 @@ function getRawOutput(proj, filePath) { return output; } exports.getRawOutput = getRawOutput; +function runExternalTranspiler(outputFile, project, sourceMapContents) { + if (!isJSFile(outputFile.name) && !isJSSourceMapFile(outputFile.name)) { + return []; + } + var settings = project.projectFile.project; + var externalTranspiler = settings.externalTranspiler; + if (!externalTranspiler) { + return []; + } + if (isJSSourceMapFile(outputFile.name)) { + var sourceMapPayload = JSON.parse(outputFile.text); + var jsFileName = fsUtil_1.consistentPath(path.resolve(path.dirname(outputFile.name), sourceMapPayload.file)); + sourceMapContents[outputFile.name] = { jsFileName: jsFileName, sourceMapPayload: sourceMapPayload }; + return []; + } + if (externalTranspiler.toLocaleLowerCase() === "babel") { + babel = require("babel"); + var babelOptions = {}; + var sourceMapFileName = getJSMapNameForJSFile(outputFile.name); + if (sourceMapContents[sourceMapFileName]) { + babelOptions.inputSourceMap = sourceMapContents[sourceMapFileName].sourceMapPayload; + } + if (settings.compilerOptions.sourceMap) { + babelOptions.sourceMaps = true; + } + if (settings.compilerOptions.inlineSourceMap) { + babelOptions.sourceMaps = "inline"; + } + if (!settings.compilerOptions.removeComments) { + babelOptions.comments = true; + } + var babelResult = babel.transform(outputFile.text, babelOptions); + outputFile.text = babelResult.code; + if (babelResult.map && settings.compilerOptions.sourceMap) { + var additionalEmit = { + name: sourceMapFileName, + text: JSON.stringify(babelResult.map), + writeByteOrderMark: settings.compilerOptions.emitBOM + }; + if (additionalEmit.name === "") { + console.warn("The TypeScript language service did not yet provide a .js.map name for file " + outputFile.name); + return []; + } + return [additionalEmit]; + } + return []; + } + function getJSMapNameForJSFile(jsFileName) { + for (var jsMapName in sourceMapContents) { + if (sourceMapContents.hasOwnProperty(jsMapName)) { + if (sourceMapContents[jsMapName].jsFileName === jsFileName) { + return jsMapName; + } + } + } + return ""; + } +} +function isJSFile(fileName) { + return (path.extname(fileName).toLocaleLowerCase() === ".js"); +} +function isJSSourceMapFile(fileName) { + var lastExt = path.extname(fileName); + if (lastExt === ".map") { + return isJSFile(fileName.substr(0, fileName.length - 4)); + } + return false; +} diff --git a/dist/main/tsconfig/tsconfig.js b/dist/main/tsconfig/tsconfig.js index 992b6bba9..d9638d8b9 100644 --- a/dist/main/tsconfig/tsconfig.js +++ b/dist/main/tsconfig/tsconfig.js @@ -240,7 +240,8 @@ function getProjectSync(pathOrSrcFile) { formatCodeOptions: formatting.makeFormatCodeOptions(projectSpec.formatCodeOptions), compileOnSave: projectSpec.compileOnSave == undefined ? true : projectSpec.compileOnSave, package: pkg, - typings: [] + typings: [], + externalTranspiler: projectSpec.externalTranspiler == undefined ? undefined : projectSpec.externalTranspiler }; var validationResult = validator.validate(projectSpec.compilerOptions); if (validationResult.errorMessage) { diff --git a/docs/faq.md b/docs/faq.md index 476854fe6..58fbe29e2 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -14,6 +14,22 @@ If it conforms the latest TypeScript services API then yes! Just set the path to However, please note that the [version](https://github.com/TypeStrong/atom-typescript/blob/master/docs/tsconfig.md#version) in `tsconfig.json` does not indicate the compiler atom is using. That's strictly an aide-mémoire - it's to remind you which version of the TypeScript this project is intended to work with. +## Can I use an alternate transpiler? +Atom-typescript supports using Babel as an alternate ES5 transpiler in coordination with the TypeScript language service. This may be useful if TypeScript does not yet support transpiling a certain feature correctly (for example [scope per for loop iteration with let](https://github.com/Microsoft/TypeScript/issues/3915)). + +To enable using Babel as the transpiler, make these changes to your `tsconfig.json` file: + +**1:** Add this key in the root: + +```js +{ + "externalTranspiler": "babel" +} +``` +**2:** Set the `target` compiler option to `"es6"`. This is not *technically* required, but if you don't do this, you'll just be transpiling an already-transpiled file. + +Note that atom-typescript's Babel integraion works with in concert with the `removeComments`, `sourceMap`, and `inlineSourceMap` compiler options settings in `tsconfig.json`, so those items should just work as expected. Any source maps should be doubly-mapped back to the original TypeScript. + ## I prefer single (or double) quotes You can set that in the package settings https://atom.io/docs/latest/using-atom-atom-packages#package-settings diff --git a/lib/main/atom/atomUtils.ts b/lib/main/atom/atomUtils.ts index bf276cb42..a21ae161c 100644 --- a/lib/main/atom/atomUtils.ts +++ b/lib/main/atom/atomUtils.ts @@ -19,7 +19,7 @@ export function getEditorPositionForBufferPosition(editor: AtomCore.IEditor, buf return buffer.characterIndexForPosition(bufferPos); } -export function isAllowedExtension(ext) { +export function isAllowedExtension(ext: string) { return (ext == '.ts' || ext == '.tst' || ext == '.tsx'); } diff --git a/lib/main/lang/modules/building.ts b/lib/main/lang/modules/building.ts index 4d66b065d..ccc9868c2 100644 --- a/lib/main/lang/modules/building.ts +++ b/lib/main/lang/modules/building.ts @@ -6,6 +6,7 @@ import {pathIsRelative, makeRelativePath} from "../../tsconfig/tsconfig"; import {consistentPath} from "../../utils/fsUtil"; import {createMap} from "../utils"; +let babel: any; export const Not_In_Context = "/* NotInContext */"; export function diagnosticToTSError(diagnostic: ts.Diagnostic): TSError { @@ -40,10 +41,23 @@ export function emitFile(proj: project.Project, filePath: string): EmitOutput { errors.push(diagnosticToTSError(diagnostic)); }); - output.outputFiles.forEach(o => { - mkdirp.sync(path.dirname(o.name)); - fs.writeFileSync(o.name, o.text, "utf8"); - }); + { + let sourceMapContents: {[index:string]: any} = {}; + output.outputFiles.forEach(o => { + mkdirp.sync(path.dirname(o.name)); + let additionalEmits = runExternalTranspiler(o, proj, sourceMapContents); + + if (!sourceMapContents[o.name]) { + // .js.map files will be written as an "additional emit" later. + fs.writeFileSync(o.name, o.text, "utf8"); + } + + additionalEmits.forEach(a => { + mkdirp.sync(path.dirname(a.name)); + fs.writeFileSync(a.name, a.text, "utf8"); + }) + }); + } var outputFiles = output.outputFiles.map((o) => o.name); if (path.extname(filePath) == '.d.ts') { @@ -71,3 +85,90 @@ export function getRawOutput(proj: project.Project, filePath: string): ts.EmitOu } return output; } + +function runExternalTranspiler(outputFile: ts.OutputFile, project: project.Project, sourceMapContents: {[index:string]: any}) : ts.OutputFile[] { + if (!isJSFile(outputFile.name) && !isJSSourceMapFile(outputFile.name)) { + return []; + } + + let settings = project.projectFile.project; + let externalTranspiler = settings.externalTranspiler; + if (!externalTranspiler) { + return []; + } + + if (isJSSourceMapFile(outputFile.name)) { + let sourceMapPayload = JSON.parse(outputFile.text); + let jsFileName = consistentPath(path.resolve(path.dirname(outputFile.name), sourceMapPayload.file)); + sourceMapContents[outputFile.name] = {jsFileName: jsFileName, sourceMapPayload}; + return []; + } + + if (externalTranspiler.toLocaleLowerCase() === "babel") { + babel = require("babel"); + + let babelOptions : any = {}; + + let sourceMapFileName = getJSMapNameForJSFile(outputFile.name); + + if (sourceMapContents[sourceMapFileName]) { + babelOptions.inputSourceMap = sourceMapContents[sourceMapFileName].sourceMapPayload; + } + if (settings.compilerOptions.sourceMap) { + babelOptions.sourceMaps = true; + } + if (settings.compilerOptions.inlineSourceMap) { + babelOptions.sourceMaps = "inline"; + } + if (!settings.compilerOptions.removeComments) { + babelOptions.comments = true; + } + + let babelResult = babel.transform(outputFile.text, babelOptions); + outputFile.text = babelResult.code; + + if (babelResult.map && settings.compilerOptions.sourceMap) { + let additionalEmit : ts.OutputFile = { + name: sourceMapFileName, + text : JSON.stringify(babelResult.map), + writeByteOrderMark: settings.compilerOptions.emitBOM + }; + + if (additionalEmit.name === "") { + // can't emit a blank file name - this should only be reached if the TypeScript + // language service returns the .js file before the .js.map file. + console.warn(`The TypeScript language service did not yet provide a .js.map name for file ${outputFile.name}`); + return []; + } + + return [additionalEmit]; + } + + return []; + } + + function getJSMapNameForJSFile(jsFileName: string) { + for (let jsMapName in sourceMapContents) { + if (sourceMapContents.hasOwnProperty(jsMapName)) { + if (sourceMapContents[jsMapName].jsFileName === jsFileName) { + return jsMapName; + } + } + } + return ""; + } +} + + + +function isJSFile(fileName: string) { + return (path.extname(fileName).toLocaleLowerCase() === ".js"); +} + +function isJSSourceMapFile(fileName: string) { + let lastExt = path.extname(fileName); + if (lastExt === ".map") { + return isJSFile(fileName.substr(0,fileName.length - 4)); + } + return false; +} diff --git a/lib/main/tsconfig/tsconfig.ts b/lib/main/tsconfig/tsconfig.ts index bbac22597..22d6bc490 100644 --- a/lib/main/tsconfig/tsconfig.ts +++ b/lib/main/tsconfig/tsconfig.ts @@ -102,6 +102,7 @@ interface TypeScriptProjectRawSpecification { filesGlob?: string[]; // optional: An array of 'glob / minimatch / RegExp' patterns to specify source files formatCodeOptions?: formatting.FormatCodeOptions; // optional: formatting options compileOnSave?: boolean; // optional: compile on save. Ignored to build tools. Used by IDEs + externalTranspiler?: string; } interface UsefulFromPackageJson { @@ -123,6 +124,7 @@ export interface TypeScriptProjectSpecification { formatCodeOptions: ts.FormatCodeOptions; compileOnSave: boolean; package?: UsefulFromPackageJson; + externalTranspiler?: string; } ///////// FOR USE WITH THE API ///////////// @@ -404,7 +406,8 @@ export function getProjectSync(pathOrSrcFile: string): TypeScriptProjectFileDeta formatCodeOptions: formatting.makeFormatCodeOptions(projectSpec.formatCodeOptions), compileOnSave: projectSpec.compileOnSave == undefined ? true : projectSpec.compileOnSave, package: pkg, - typings: [] + typings: [], + externalTranspiler: projectSpec.externalTranspiler == undefined ? undefined : projectSpec.externalTranspiler }; // Validate the raw compiler options before converting them to TS compiler options diff --git a/package.json b/package.json index 553c80bdf..105669ab4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "atom-package-dependencies": "https://github.com/basarat/atom-package-dependencies/archive/cb1.tar.gz", "atom-space-pen-views": "^2.0.4", + "babel": "^5.6.23", "basarat-text-buffer": "6.0.0", "d3": "^3.5.5", "emissary": "^1.3.3",