From fdf47410a93593db791534a8b620d47eff1be9f7 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Sat, 6 Jul 2019 10:15:39 -0400 Subject: [PATCH 1/2] 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. --- index.js | 35 +++++++++++++++++++++++++++++++++++ options.js | 27 +++++++++++++-------------- package-lock.json | 32 +++++--------------------------- package.json | 4 ++-- test/class-properties.ts | 3 +++ test/tests.js | 28 ++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 43 deletions(-) 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/tests.js b/test/tests.js index 343e900..1fbf0b6 100644 --- a/test/tests.js +++ b/test/tests.js @@ -288,9 +288,37 @@ 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")); + }); }); describe("Babel", function() { From 72e3cf5118ca197bdf9b7f68d00966d3780bc7ab Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Sat, 6 Jul 2019 10:41:01 -0400 Subject: [PATCH 2/2] Test that JSX syntax works in .tsx files. --- test/react.tsx | 3 +++ test/tests.js | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 test/react.tsx 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 1fbf0b6..6d95403 100644 --- a/test/tests.js +++ b/test/tests.js @@ -319,6 +319,16 @@ describe("meteor-babel", () => { "})(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() {