Skip to content

Commit

Permalink
feature #694 Add Encore.enableBabelTypeScriptPreset() to "compile" Ty…
Browse files Browse the repository at this point in the history
…peScript with Babel (Kocal)

This PR was squashed before being merged into the master branch (closes #694).

Discussion
----------

Add Encore.enableBabelTypeScriptPreset() to "compile" TypeScript with Babel

See #691, I thought it can be interesting to have a test here.

Using Babel to "compile" TypeScript is faster than using `ts-loader` or `tsc` directly, because in fact, it literally remove types annotations.

To continue to check types, you have to run `tsc --emitDeclarationOnly` manually (or in a CI). But this is not part of the PR.

To migrate an already existing TypeScript app, you just have to configure `babel-loader` to run over `.tsx?` file like this:
```diff
Encore
-  .enableTypeScriptLoader()
+  .configureLoaderRule('javascript', loader => {
+    loader.test = /.(j|t)sx?$/; // let Babel to run over .tsx? files too
+  })
```

Install some dependencies: `yarn add --dev @babel/preset-typescript @babel/plugin-proposal-class-properties`.

And modify your Babel configuration:
```diff
{
    "presets": [
        "@babel/env",
+        "@babel/typescript"
    ],
+    "plugins": [
+        "@babel/proposal-class-properties"
+    ]
}
```

Maybe I can update `Encore.configureBabel()` and add an option to runs over TypeScript files too... like I did in #574, something like this:
```js
Encore
  .configureBabel(null, {
    typescript: true
  })
```

I've also changed the legacy import/export (`import a = require('...')` to `import a from '...'`). Because it's the legacy way (ES6 imports are very fine) and the Babel TypeScript was not compatible with them:
![Capture d’écran de 2020-02-07 22-06-11](https://user-images.githubusercontent.com/2103975/74066752-0a323100-49f8-11ea-91b8-cfdbc6de28a2.png)

**EDIT :** Added `Encore.enableBabelTypeScriptPreset()` that do all the job for us! :)

```js
// simple usage
Encore.enableBabelTypeScriptPreset();

// configure TypeScript preset (https://babeljs.io/docs/en/babel-preset-typescript#options)
Encore.enableBabelTypeScriptPreset({
  isTSX: true;
})
```

`Encore.enableBabelTypeScriptPreset()` can not be used aside `Encore.enableTypeScriptLoader()` or `Encore.enableForkedTypeScriptTypesChecking()`.

Commits
-------

bdd553a chore: typo
96a666b feat: implement Encore.enableBabelTypeScriptPreset()
6f992b0 feat: prepare method "enableBabelTypeScriptPreset"
8238c32 fixture: add code that only works in TypeScript
66067a0 test: add test for TypeScript "compilation" with Babel
2e21d4f chore(deps-dev): install Babel TypeScript preset, with class properties plugin
e053a5e fix(fixtures): use "better" syntax for TypeScript import/export
  • Loading branch information
weaverryan committed Mar 27, 2020
2 parents b09eb03 + bdd553a commit 03f217a
Show file tree
Hide file tree
Showing 15 changed files with 384 additions and 17 deletions.
4 changes: 2 additions & 2 deletions fixtures/js/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import render = require('./render');
import render from './render';

render();
render();
5 changes: 3 additions & 2 deletions fixtures/js/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
function render() {
document.getElementById('app').innerHTML = "<h1>Welcome to Your TypeScript App</h1>";
const html: string = "<h1>Welcome to Your TypeScript App</h1>";
document.getElementById('app').innerHTML = html;
}

export = render;
export default render;
2 changes: 1 addition & 1 deletion fixtures/js/render2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ function render() {
document.getElementById('app').innerHTML = "<h1>Welcome to Your TypeScript App</h1>";
}

export = render;
export default render;
6 changes: 4 additions & 2 deletions fixtures/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"compilerOptions": {}
}
"compilerOptions": {
"allowSyntheticDefaultImports": true
}
}
38 changes: 37 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ class Encore {

/**
* If enabled, a Preact preset will be applied to
* the generated Webpack configuration.
* the generated Webpack and Babel configuration.
*
* ```
* Encore.enablePreactPreset()
Expand Down Expand Up @@ -1044,6 +1044,42 @@ class Encore {
return this;
}


/**
* If enabled, a TypeScript preset will be applied to
* the generated Webpack and Babel configuration.
*
* ```
* Encore.enableBabelTypeScriptPreset()
* ```
*
* This method let Babel handle your TypeScript code
* and can not be used with `Encore.enableTypeScriptLoader()`
* or `Encore.enableForkedTypeScriptTypesChecking()`.
*
* Since all types are removed by Babel,
* you must run `tsc --noEmit` yourself for types checking.
*
* The Babel TypeScript preset can be configured,
* see https://babeljs.io/docs/en/babel-preset-typescript#options
* for available options.
*
* For example:
* ```
* Encore.enableBabelTypeScriptPreset({
* isTSX: true
* })
* ```
*
* @param {object} options
* @returns {Encore}
*/
enableBabelTypeScriptPreset(options) {
webpackConfig.enableBabelTypeScriptPreset(options);

return this;
}

/**
* If enabled, the Vue.js loader is enabled.
*
Expand Down
22 changes: 22 additions & 0 deletions lib/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class WebpackConfig {
this.useEslintLoader = false;
this.useTypeScriptLoader = false;
this.useForkedTypeScriptTypeChecking = false;
this.useBabelTypeScriptPreset = false;
this.useWebpackNotifier = false;
this.useHandlebarsLoader = false;

Expand All @@ -130,6 +131,7 @@ class WebpackConfig {
useBuiltIns: false,
corejs: null,
};
this.babelTypeScriptPresetOptions = {};
this.vueOptions = {
useJsx: false,
};
Expand Down Expand Up @@ -647,6 +649,10 @@ class WebpackConfig {
}

enableTypeScriptLoader(callback = () => {}) {
if (this.useBabelTypeScriptPreset) {
throw new Error('Encore.enableTypeScriptLoader() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
}

this.useTypeScriptLoader = true;

if (typeof callback !== 'function') {
Expand All @@ -657,6 +663,9 @@ class WebpackConfig {
}

enableForkedTypeScriptTypesChecking(forkedTypeScriptTypesCheckOptionsCallback = () => {}) {
if (this.useBabelTypeScriptPreset) {
throw new Error('Encore.enableForkedTypeScriptTypesChecking() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
}

if (typeof forkedTypeScriptTypesCheckOptionsCallback !== 'function') {
throw new Error('Argument 1 to enableForkedTypeScriptTypesChecking() must be a callback function.');
Expand All @@ -667,6 +676,19 @@ class WebpackConfig {
forkedTypeScriptTypesCheckOptionsCallback;
}

enableBabelTypeScriptPreset(options = {}) {
if (this.useTypeScriptLoader) {
throw new Error('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableTypeScriptLoader() has been called.');
}

if (this.useForkedTypeScriptTypeChecking) {
throw new Error('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableForkedTypeScriptTypesChecking() has been called.');
}

this.useBabelTypeScriptPreset = true;
this.babelTypeScriptPresetOptions = options;
}

enableVueLoader(vueLoaderOptionsCallback = () => {}, vueOptions = {}) {
this.useVueLoader = true;

Expand Down
3 changes: 1 addition & 2 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,7 @@ class ConfigGenerator {

let rules = [
applyRuleConfigurationCallback('javascript', {
// match .js and .jsx
test: /\.jsx?$/,
test: babelLoaderUtil.getTest(this.webpackConfig),
exclude: this.webpackConfig.babelOptions.exclude,
use: babelLoaderUtil.getLoaders(this.webpackConfig)
}),
Expand Down
9 changes: 9 additions & 0 deletions lib/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ const features = {
],
description: 'check TypeScript types in a separate process'
},
'typescript-babel': {
method: 'enableBabelTypeScriptPreset',
packages: [
{ name: 'typescript' },
{ name: '@babel/preset-typescript', enforce_version: true },
{ name: '@babel/plugin-proposal-class-properties', enforce_version: true },
],
description: 'process TypeScript files with Babel'
},
vue: {
method: 'enableVueLoader()',
// vue is needed so the end-user can do things
Expand Down
23 changes: 23 additions & 0 deletions lib/loaders/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ module.exports = {
plugins: ['@babel/plugin-syntax-dynamic-import']
});

if (webpackConfig.useBabelTypeScriptPreset) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('typescript-babel');

babelConfig.presets.push(['@babel/preset-typescript', webpackConfig.babelTypeScriptPresetOptions]);
babelConfig.plugins.push('@babel/plugin-proposal-class-properties');
}

if (webpackConfig.useReact) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('react');

Expand Down Expand Up @@ -95,5 +102,21 @@ module.exports = {
options: babelConfig
}
];
},

/**
* @param {WebpackConfig} webpackConfig
* @return {RegExp} to use for eslint-loader `test` rule
*/
getTest(webpackConfig) {
const extensions = [
'jsx?', // match .js and .jsx
];

if (webpackConfig.useBabelTypeScriptPreset) {
extensions.push('tsx?'); // match .ts and .tsx
}

return new RegExp(`\\.(${extensions.join('|')})$`);
}
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
},
"devDependencies": {
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.0.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.3",
"@vue/babel-preset-jsx": "^1.0.0-beta.3",
"autoprefixer": "^8.5.0",
Expand Down
47 changes: 47 additions & 0 deletions test/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,15 @@ describe('WebpackConfig object', () => {
config.enableTypeScriptLoader('FOO');
}).to.throw('must be a callback function');
});

it('TypeScript can not be compiled by ts-loader if Babel is already handling TypeScript', () => {
const config = createConfig();
config.enableBabelTypeScriptPreset();

expect(function() {
config.enableTypeScriptLoader();
}).to.throw('Encore.enableTypeScriptLoader() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
});
});

describe('enableForkedTypeScriptTypesChecking', () => {
Expand All @@ -891,6 +900,44 @@ describe('WebpackConfig object', () => {
config.enableForkedTypeScriptTypesChecking('FOO');
}).to.throw('must be a callback function');
});

it('TypeScript can not be compiled by Babel if forked types checking is enabled', () => {
const config = createConfig();
config.enableBabelTypeScriptPreset();

expect(function() {
config.enableForkedTypeScriptTypesChecking();
}).to.throw('Encore.enableForkedTypeScriptTypesChecking() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
});
});

describe('enableBabelTypeScriptPreset', () => {
it('TypeScript can not be compiled by Babel if ts-loader is already enabled', () => {
const config = createConfig();
config.enableTypeScriptLoader();

expect(function() {
config.enableBabelTypeScriptPreset();
}).to.throw('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableTypeScriptLoader() has been called.');
});

it('TypeScript can not be compiled by Babel if ts-loader is already enabled', () => {
const config = createConfig();
config.enableForkedTypeScriptTypesChecking();

expect(function() {
config.enableBabelTypeScriptPreset();
}).to.throw('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableForkedTypeScriptTypesChecking() has been called.');
});

it('Options should be defined', () => {
const config = createConfig();
const options = { isTSX: true };

config.enableBabelTypeScriptPreset(options);

expect(config.babelTypeScriptPresetOptions).to.equal(options);
});
});

describe('enableVueLoader', () => {
Expand Down
14 changes: 7 additions & 7 deletions test/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);

// check for the default env preset only
expect(JSON.stringify(jsRule.use[0].options.presets)).contains('@babel/preset-env');
Expand Down Expand Up @@ -932,7 +932,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
expect(String(jsRule.exclude)).to.equal(String(/(node_modules|bower_components)/));

const babelLoader = jsRule.use.find(loader => loader.loader === 'babel-loader');
Expand All @@ -951,7 +951,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
expect(String(jsRule.exclude)).to.equal(String(/foo/));
});

Expand All @@ -966,7 +966,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
expect(jsRule.exclude).to.be.a('Function');
expect(jsRule.exclude(path.join('test', 'node_modules', 'foo', 'index.js'))).to.be.false;
expect(jsRule.exclude(path.join('test', 'node_modules', 'bar', 'index.js'))).to.be.true;
Expand All @@ -984,7 +984,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
const babelLoader = jsRule.use.find(loader => loader.loader === 'babel-loader');
const babelEnvPreset = babelLoader.options.presets.find(([name]) => name === '@babel/preset-env');
expect(babelEnvPreset[1].useBuiltIns).to.equal('usage');
Expand All @@ -1001,7 +1001,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
const babelLoader = jsRule.use.find(loader => loader.loader === 'babel-loader');
const babelEnvPreset = babelLoader.options.presets.find(([name]) => name === '@babel/preset-env');
expect(babelEnvPreset[1].useBuiltIns).to.equal(false);
Expand All @@ -1018,7 +1018,7 @@ describe('The config-generator function', () => {

const actualConfig = configGenerator(config);

const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
const babelLoader = jsRule.use.find(loader => loader.loader === 'babel-loader');
const babelEnvPreset = babelLoader.options.presets.find(([name]) => name === '@babel/preset-env');
expect(babelEnvPreset[1].useBuiltIns).to.equal('usage');
Expand Down
29 changes: 29 additions & 0 deletions test/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,35 @@ module.exports = {
}).to.throw('wrong `tsconfig` path in fork plugin configuration (should be a relative or absolute path)');
});

it('TypeScript can be compiled by Babel', (done) => {
const config = createWebpackConfig('www/build', 'dev');
config.setPublicPath('/build');
config.addEntry('main', ['./js/render.ts', './js/index.ts']);
config.enableBabelTypeScriptPreset();

testSetup.runWebpack(config, (webpackAssert) => {
// check that babel-loader transformed the ts file
webpackAssert.assertOutputFileContains(
'main.js',
'document.getElementById(\'app\').innerHTML =',
);

testSetup.requestTestPage(
path.join(config.getContext(), 'www'),
[
'build/runtime.js',
'build/main.js',
],
(browser) => {

// assert that the ts module rendered
browser.assert.text('#app h1', 'Welcome to Your TypeScript App');
done();
},
);
});
});

it('When configured, Handlebars is compiled', (done) => {
const config = createWebpackConfig('www/build', 'dev');
config.setPublicPath('/build');
Expand Down

0 comments on commit 03f217a

Please sign in to comment.