Skip to content

Commit 03f217a

Browse files
committed
feature #694 Add Encore.enableBabelTypeScriptPreset() to "compile" TypeScript 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
2 parents b09eb03 + bdd553a commit 03f217a

File tree

15 files changed

+384
-17
lines changed

15 files changed

+384
-17
lines changed

fixtures/js/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import render = require('./render');
1+
import render from './render';
22

3-
render();
3+
render();

fixtures/js/render.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
function render() {
2-
document.getElementById('app').innerHTML = "<h1>Welcome to Your TypeScript App</h1>";
2+
const html: string = "<h1>Welcome to Your TypeScript App</h1>";
3+
document.getElementById('app').innerHTML = html;
34
}
45

5-
export = render;
6+
export default render;

fixtures/js/render2.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ function render() {
22
document.getElementById('app').innerHTML = "<h1>Welcome to Your TypeScript App</h1>";
33
}
44

5-
export = render;
5+
export default render;

fixtures/js/tsconfig.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
{
2-
"compilerOptions": {}
3-
}
2+
"compilerOptions": {
3+
"allowSyntheticDefaultImports": true
4+
}
5+
}

index.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ class Encore {
980980

981981
/**
982982
* If enabled, a Preact preset will be applied to
983-
* the generated Webpack configuration.
983+
* the generated Webpack and Babel configuration.
984984
*
985985
* ```
986986
* Encore.enablePreactPreset()
@@ -1044,6 +1044,42 @@ class Encore {
10441044
return this;
10451045
}
10461046

1047+
1048+
/**
1049+
* If enabled, a TypeScript preset will be applied to
1050+
* the generated Webpack and Babel configuration.
1051+
*
1052+
* ```
1053+
* Encore.enableBabelTypeScriptPreset()
1054+
* ```
1055+
*
1056+
* This method let Babel handle your TypeScript code
1057+
* and can not be used with `Encore.enableTypeScriptLoader()`
1058+
* or `Encore.enableForkedTypeScriptTypesChecking()`.
1059+
*
1060+
* Since all types are removed by Babel,
1061+
* you must run `tsc --noEmit` yourself for types checking.
1062+
*
1063+
* The Babel TypeScript preset can be configured,
1064+
* see https://babeljs.io/docs/en/babel-preset-typescript#options
1065+
* for available options.
1066+
*
1067+
* For example:
1068+
* ```
1069+
* Encore.enableBabelTypeScriptPreset({
1070+
* isTSX: true
1071+
* })
1072+
* ```
1073+
*
1074+
* @param {object} options
1075+
* @returns {Encore}
1076+
*/
1077+
enableBabelTypeScriptPreset(options) {
1078+
webpackConfig.enableBabelTypeScriptPreset(options);
1079+
1080+
return this;
1081+
}
1082+
10471083
/**
10481084
* If enabled, the Vue.js loader is enabled.
10491085
*

lib/WebpackConfig.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class WebpackConfig {
109109
this.useEslintLoader = false;
110110
this.useTypeScriptLoader = false;
111111
this.useForkedTypeScriptTypeChecking = false;
112+
this.useBabelTypeScriptPreset = false;
112113
this.useWebpackNotifier = false;
113114
this.useHandlebarsLoader = false;
114115

@@ -130,6 +131,7 @@ class WebpackConfig {
130131
useBuiltIns: false,
131132
corejs: null,
132133
};
134+
this.babelTypeScriptPresetOptions = {};
133135
this.vueOptions = {
134136
useJsx: false,
135137
};
@@ -647,6 +649,10 @@ class WebpackConfig {
647649
}
648650

649651
enableTypeScriptLoader(callback = () => {}) {
652+
if (this.useBabelTypeScriptPreset) {
653+
throw new Error('Encore.enableTypeScriptLoader() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
654+
}
655+
650656
this.useTypeScriptLoader = true;
651657

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

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

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

679+
enableBabelTypeScriptPreset(options = {}) {
680+
if (this.useTypeScriptLoader) {
681+
throw new Error('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableTypeScriptLoader() has been called.');
682+
}
683+
684+
if (this.useForkedTypeScriptTypeChecking) {
685+
throw new Error('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableForkedTypeScriptTypesChecking() has been called.');
686+
}
687+
688+
this.useBabelTypeScriptPreset = true;
689+
this.babelTypeScriptPresetOptions = options;
690+
}
691+
670692
enableVueLoader(vueLoaderOptionsCallback = () => {}, vueOptions = {}) {
671693
this.useVueLoader = true;
672694

lib/config-generator.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,7 @@ class ConfigGenerator {
250250

251251
let rules = [
252252
applyRuleConfigurationCallback('javascript', {
253-
// match .js and .jsx
254-
test: /\.jsx?$/,
253+
test: babelLoaderUtil.getTest(this.webpackConfig),
255254
exclude: this.webpackConfig.babelOptions.exclude,
256255
use: babelLoaderUtil.getLoaders(this.webpackConfig)
257256
}),

lib/features.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ const features = {
7878
],
7979
description: 'check TypeScript types in a separate process'
8080
},
81+
'typescript-babel': {
82+
method: 'enableBabelTypeScriptPreset',
83+
packages: [
84+
{ name: 'typescript' },
85+
{ name: '@babel/preset-typescript', enforce_version: true },
86+
{ name: '@babel/plugin-proposal-class-properties', enforce_version: true },
87+
],
88+
description: 'process TypeScript files with Babel'
89+
},
8190
vue: {
8291
method: 'enableVueLoader()',
8392
// vue is needed so the end-user can do things

lib/loaders/babel.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ module.exports = {
5858
plugins: ['@babel/plugin-syntax-dynamic-import']
5959
});
6060

61+
if (webpackConfig.useBabelTypeScriptPreset) {
62+
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('typescript-babel');
63+
64+
babelConfig.presets.push(['@babel/preset-typescript', webpackConfig.babelTypeScriptPresetOptions]);
65+
babelConfig.plugins.push('@babel/plugin-proposal-class-properties');
66+
}
67+
6168
if (webpackConfig.useReact) {
6269
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('react');
6370

@@ -95,5 +102,21 @@ module.exports = {
95102
options: babelConfig
96103
}
97104
];
105+
},
106+
107+
/**
108+
* @param {WebpackConfig} webpackConfig
109+
* @return {RegExp} to use for eslint-loader `test` rule
110+
*/
111+
getTest(webpackConfig) {
112+
const extensions = [
113+
'jsx?', // match .js and .jsx
114+
];
115+
116+
if (webpackConfig.useBabelTypeScriptPreset) {
117+
extensions.push('tsx?'); // match .ts and .tsx
118+
}
119+
120+
return new RegExp(`\\.(${extensions.join('|')})$`);
98121
}
99122
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
},
5656
"devDependencies": {
5757
"@babel/plugin-transform-react-jsx": "^7.0.0",
58+
"@babel/plugin-proposal-class-properties": "^7.0.0",
5859
"@babel/preset-react": "^7.0.0",
60+
"@babel/preset-typescript": "^7.0.0",
5961
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.3",
6062
"@vue/babel-preset-jsx": "^1.0.0-beta.3",
6163
"autoprefixer": "^8.5.0",

test/WebpackConfig.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,15 @@ describe('WebpackConfig object', () => {
872872
config.enableTypeScriptLoader('FOO');
873873
}).to.throw('must be a callback function');
874874
});
875+
876+
it('TypeScript can not be compiled by ts-loader if Babel is already handling TypeScript', () => {
877+
const config = createConfig();
878+
config.enableBabelTypeScriptPreset();
879+
880+
expect(function() {
881+
config.enableTypeScriptLoader();
882+
}).to.throw('Encore.enableTypeScriptLoader() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
883+
});
875884
});
876885

877886
describe('enableForkedTypeScriptTypesChecking', () => {
@@ -891,6 +900,44 @@ describe('WebpackConfig object', () => {
891900
config.enableForkedTypeScriptTypesChecking('FOO');
892901
}).to.throw('must be a callback function');
893902
});
903+
904+
it('TypeScript can not be compiled by Babel if forked types checking is enabled', () => {
905+
const config = createConfig();
906+
config.enableBabelTypeScriptPreset();
907+
908+
expect(function() {
909+
config.enableForkedTypeScriptTypesChecking();
910+
}).to.throw('Encore.enableForkedTypeScriptTypesChecking() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
911+
});
912+
});
913+
914+
describe('enableBabelTypeScriptPreset', () => {
915+
it('TypeScript can not be compiled by Babel if ts-loader is already enabled', () => {
916+
const config = createConfig();
917+
config.enableTypeScriptLoader();
918+
919+
expect(function() {
920+
config.enableBabelTypeScriptPreset();
921+
}).to.throw('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableTypeScriptLoader() has been called.');
922+
});
923+
924+
it('TypeScript can not be compiled by Babel if ts-loader is already enabled', () => {
925+
const config = createConfig();
926+
config.enableForkedTypeScriptTypesChecking();
927+
928+
expect(function() {
929+
config.enableBabelTypeScriptPreset();
930+
}).to.throw('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableForkedTypeScriptTypesChecking() has been called.');
931+
});
932+
933+
it('Options should be defined', () => {
934+
const config = createConfig();
935+
const options = { isTSX: true };
936+
937+
config.enableBabelTypeScriptPreset(options);
938+
939+
expect(config.babelTypeScriptPresetOptions).to.equal(options);
940+
});
894941
});
895942

896943
describe('enableVueLoader', () => {

test/config-generator.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ describe('The config-generator function', () => {
520520

521521
const actualConfig = configGenerator(config);
522522

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

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

933933
const actualConfig = configGenerator(config);
934934

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

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

952952
const actualConfig = configGenerator(config);
953953

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

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

967967
const actualConfig = configGenerator(config);
968968

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

985985
const actualConfig = configGenerator(config);
986986

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

10021002
const actualConfig = configGenerator(config);
10031003

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

10191019
const actualConfig = configGenerator(config);
10201020

1021-
const jsRule = findRule(/\.jsx?$/, actualConfig.module.rules);
1021+
const jsRule = findRule(/\.(jsx?)$/, actualConfig.module.rules);
10221022
const babelLoader = jsRule.use.find(loader => loader.loader === 'babel-loader');
10231023
const babelEnvPreset = babelLoader.options.presets.find(([name]) => name === '@babel/preset-env');
10241024
expect(babelEnvPreset[1].useBuiltIns).to.equal('usage');

test/functional.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,35 @@ module.exports = {
13211321
}).to.throw('wrong `tsconfig` path in fork plugin configuration (should be a relative or absolute path)');
13221322
});
13231323

1324+
it('TypeScript can be compiled by Babel', (done) => {
1325+
const config = createWebpackConfig('www/build', 'dev');
1326+
config.setPublicPath('/build');
1327+
config.addEntry('main', ['./js/render.ts', './js/index.ts']);
1328+
config.enableBabelTypeScriptPreset();
1329+
1330+
testSetup.runWebpack(config, (webpackAssert) => {
1331+
// check that babel-loader transformed the ts file
1332+
webpackAssert.assertOutputFileContains(
1333+
'main.js',
1334+
'document.getElementById(\'app\').innerHTML =',
1335+
);
1336+
1337+
testSetup.requestTestPage(
1338+
path.join(config.getContext(), 'www'),
1339+
[
1340+
'build/runtime.js',
1341+
'build/main.js',
1342+
],
1343+
(browser) => {
1344+
1345+
// assert that the ts module rendered
1346+
browser.assert.text('#app h1', 'Welcome to Your TypeScript App');
1347+
done();
1348+
},
1349+
);
1350+
});
1351+
});
1352+
13241353
it('When configured, Handlebars is compiled', (done) => {
13251354
const config = createWebpackConfig('www/build', 'dev');
13261355
config.setPublicPath('/build');

0 commit comments

Comments
 (0)