From 40b260e3f111c46f37e122442452d9cc8e08dae2 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Sat, 6 Jul 2019 11:10:04 -0400 Subject: [PATCH] Use actual TypeScript instead of @babel/preset-typescript. (#25) * Use actual TypeScript instead of @babel/preset-typescript. Babel's TypeScript implementation has a few unfortunate caveats: https://babeljs.io/docs/en/babel-plugin-transform-typescript#caveats Most notably, the lack of *full* support for namespaces is painful: https://github.com/babel/babel/issues/8244 https://github.com/babel/babel/pull/9785 By precompiling TypeScript code with the actual TypeScript compiler, we can support features like namespaces without relying on Babel. Of course, Babel still handles everything after TypeScript syntax has been removed. * Test that JSX syntax works in .tsx files. --- index.js | 35 +++++++++++++++++++++++++++++++++++ options.js | 27 +++++++++++++-------------- package-lock.json | 32 +++++--------------------------- package.json | 4 ++-- test/class-properties.ts | 3 +++ test/react.tsx | 3 +++ test/tests.js | 38 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 test/react.tsx diff --git a/index.js b/index.js index e10ca5c..1e959ef 100644 --- a/index.js +++ b/index.js @@ -90,8 +90,13 @@ function compile(source, options) { const optionsCopy = util.deepClone(options); const { ast, plugins, presets } = optionsCopy; delete optionsCopy.plugins; + delete optionsCopy.typescript; optionsCopy.ast = true; + if (options.typescript) { + precompileTypeScript(result, options); + } + function transform(presets) { optionsCopy.plugins = [{ parserOverride: parse @@ -130,6 +135,36 @@ function compile(source, options) { return result; } +function precompileTypeScript(result, options) { + const fileName = options.filename || options.sourceFileName; + if (fileName && ! fileName.endsWith(".ts") && ! fileName.endsWith(".tsx")) { + return; + } + + const ts = require("typescript"); + const tsResult = ts.transpileModule(result.code, { + fileName, + compilerOptions: { + target: ts.ScriptTarget.ES2018, + // Leave module syntax intact so that Babel/Reify can handle it. + module: ts.ModuleKind.ESNext, + sourceMap: true, + inlineSources: true, + } + }); + + result.code = tsResult.outputText.replace( + /\/\/# sourceMappingURL=.*?(\n|$)/g, + "$1" // preserve trailing \n characters + ); + + result.map = JSON.parse(tsResult.sourceMapText); + if (fileName) { + result.map.file = fileName; + result.map.sources = [fileName]; + } +} + exports.minify = function minify(source, options) { // We are not compiling the code in this step, only minifying, so reify // is not used. diff --git a/options.js b/options.js index b1696c0..dca7299 100644 --- a/options.js +++ b/options.js @@ -65,7 +65,6 @@ exports.getDefaults = function getDefaults(features) { } maybeAddReactPlugins(features, combined); - maybeAddTypeScriptPreset(features, combined.presets); if (features && features.jscript) { combined.plugins.push( @@ -75,7 +74,7 @@ exports.getDefaults = function getDefaults(features) { } } - return finish([combined]); + return finish(features, [combined]); }; function maybeAddReactPlugins(features, options) { @@ -89,12 +88,6 @@ function maybeAddReactPlugins(features, options) { } } -function maybeAddTypeScriptPreset(features, presets) { - if (features && features.typescript) { - presets.push(require("@babel/preset-typescript")); - } -} - function getDefaultsForModernBrowsers(features) { const combined = { presets: [], @@ -111,17 +104,16 @@ function getDefaultsForModernBrowsers(features) { } maybeAddReactPlugins(features, combined); - maybeAddTypeScriptPreset(features, combined.presets); } - return finish([combined]); + return finish(features, [combined]); } const parserOpts = require("reify/lib/parsers/babel.js").options; const util = require("./util.js"); -function finish(presets) { - return { +function finish(features, presets) { + const options = { compact: false, sourceMaps: false, ast: false, @@ -132,6 +124,14 @@ function finish(presets) { parserOpts: util.deepClone(parserOpts), presets: presets }; + + if (features && features.typescript) { + // This additional option will be consumed by the meteorBabel.compile + // function before the options are passed to Babel. + options.typescript = true; + } + + return options; } function isObject(value) { @@ -191,10 +191,9 @@ function getDefaultsForNode8(features) { if (! compileModulesOnly) { maybeAddReactPlugins(features, combined); - maybeAddTypeScriptPreset(features, combined.presets); } - return finish([combined]); + return finish(features, [combined]); } exports.getMinifierDefaults = function getMinifierDefaults(features) { diff --git a/package-lock.json b/package-lock.json index 45557b6..b854052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -375,14 +375,6 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, - "@babel/plugin-syntax-typescript": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz", - "integrity": "sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, "@babel/plugin-transform-arrow-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", @@ -618,16 +610,6 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, - "@babel/plugin-transform-typescript": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.5.0.tgz", - "integrity": "sha512-z3T4P70XJFUAHzLtEsmJ37BGVDj+55/KX8W8TBSBF0qk0KLazw8xlwVcRHacxNPgprzTdI4QWW+2eS6bTkQbCA==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.5.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-typescript": "^7.2.0" - } - }, "@babel/plugin-transform-unicode-regex": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", @@ -650,15 +632,6 @@ "@babel/plugin-transform-react-jsx-source": "^7.0.0" } }, - "@babel/preset-typescript": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz", - "integrity": "sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-transform-typescript": "^7.3.2" - } - }, "@babel/runtime": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.0.tgz", @@ -2258,6 +2231,11 @@ "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, + "typescript": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/package.json b/package.json index dfb9953..db98da3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@babel/plugin-transform-modules-commonjs": "^7.5.0", "@babel/plugin-transform-runtime": "^7.5.0", "@babel/preset-react": "^7.0.0", - "@babel/preset-typescript": "^7.3.3", "@babel/runtime": "^7.5.0", "@babel/template": "^7.4.4", "@babel/traverse": "^7.5.0", @@ -46,7 +45,8 @@ "convert-source-map": "^1.6.0", "lodash": "^4.17.11", "meteor-babel-helpers": "0.0.3", - "reify": "^0.20.12" + "reify": "^0.20.12", + "typescript": "^3.5.2" }, "devDependencies": { "@babel/plugin-proposal-decorators": "7.4.4", diff --git a/test/class-properties.ts b/test/class-properties.ts index 03a3237..1750341 100644 --- a/test/class-properties.ts +++ b/test/class-properties.ts @@ -1,4 +1,7 @@ +const enum TestResult { PASS, FAIL } + export class Test { public property: number = 1234; + public result = TestResult.PASS; constructor(public value: string) {} } diff --git a/test/react.tsx b/test/react.tsx new file mode 100644 index 0000000..10f531b --- /dev/null +++ b/test/react.tsx @@ -0,0 +1,3 @@ +export function Component() { + return
oyez
; +} diff --git a/test/tests.js b/test/tests.js index 343e900..6d95403 100644 --- a/test/tests.js +++ b/test/tests.js @@ -288,9 +288,47 @@ describe("meteor-babel", () => { const tsTest = new Test("asdf"); assert.strictEqual(tsTest.property, 1234); assert.strictEqual(tsTest.value, "asdf"); + assert.strictEqual(typeof tsTest.result, "number"); const jsTest = new (class { foo = 42 }); assert.strictEqual(jsTest.foo, 42); }); + + it("can compile TypeScript syntax", () => { + const options = meteorBabel.getDefaultOptions({ + typescript: true, + }); + + assert.strictEqual(options.typescript, true); + + const result = meteorBabel.compile([ + "export namespace Test {", + " export const enabled = true;", + "}", + ].join("\n"), options); + + assert.strictEqual(result.code, [ + "module.export({", + " Test: function () {", + " return Test;", + " }", + "});", + "var Test;", + "", + "(function (Test) {", + " Test.enabled = true;", + "})(Test || module.runSetters(Test = {}));", + ].join("\n")); + }); + + it("can handle JSX syntax in .tsx files", () => { + const { Component } = require("./react.tsx"); + assert.strictEqual(typeof Component, "function"); + assert.strictEqual(String(Component), [ + 'function Component() {', + ' return React.createElement("div", null, "oyez");', + '}', + ].join("\n")); + }); }); describe("Babel", function() {