diff --git a/fixtures/js/index.ts b/fixtures/js/index.ts
new file mode 100644
index 00000000..7ff32dcf
--- /dev/null
+++ b/fixtures/js/index.ts
@@ -0,0 +1,3 @@
+import render = require('./render');
+
+render();
\ No newline at end of file
diff --git a/fixtures/js/render.ts b/fixtures/js/render.ts
new file mode 100644
index 00000000..1a3c83f2
--- /dev/null
+++ b/fixtures/js/render.ts
@@ -0,0 +1,5 @@
+function render() {
+ document.getElementById('app').innerHTML = "
Welcome to Your TypeScript App
";
+}
+
+export = render;
\ No newline at end of file
diff --git a/fixtures/js/render2.tsx b/fixtures/js/render2.tsx
new file mode 100644
index 00000000..1a3c83f2
--- /dev/null
+++ b/fixtures/js/render2.tsx
@@ -0,0 +1,5 @@
+function render() {
+ document.getElementById('app').innerHTML = "Welcome to Your TypeScript App
";
+}
+
+export = render;
\ No newline at end of file
diff --git a/fixtures/js/tsconfig.json b/fixtures/js/tsconfig.json
new file mode 100644
index 00000000..4ee3592b
--- /dev/null
+++ b/fixtures/js/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "compilerOptions": {}
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index cc7c35ee..bbceb886 100644
--- a/index.js
+++ b/index.js
@@ -342,6 +342,23 @@ module.exports = {
return this;
},
+ /**
+ * Call this if you plan on loading TypeScript files.
+ *
+ * Encore.enableTypeScriptLoader(function(tsConfig) {
+ * // change the tsConfig
+ * });
+ *
+ * Supported configuration options:
+ * @see https://github.com/TypeStrong/ts-loader/blob/master/README.md#available-options
+ *
+ * @param {function} callback
+ * @return {exports}
+ */
+ enableTypeScriptLoader(callback) {
+ webpackConfig.enableTypeScriptLoader(callback);
+ }
+
/**
* If enabled, the Vue.js loader is enabled.
*
diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js
index 2a741c9f..f9956a4e 100644
--- a/lib/WebpackConfig.js
+++ b/lib/WebpackConfig.js
@@ -53,6 +53,8 @@ class WebpackConfig {
this.useVueLoader = false;
this.vueLoaderOptionsCallback = () => {};
this.loaders = [];
+ this.useTypeScriptLoader = false;
+ this.tsConfigurationCallback = function() {};
}
getContext() {
@@ -224,6 +226,16 @@ class WebpackConfig {
this.useReact = true;
}
+ enableTypeScriptLoader(callback) {
+ this.useTypeScriptLoader = true;
+
+ if (typeof callback !== 'function') {
+ throw new Error('Argument 1 to enableTypeScriptLoader() must be a callback function.');
+ }
+
+ this.tsConfigurationCallback = callback;
+ }
+
enableVueLoader(vueLoaderOptionsCallback = () => {}) {
this.useVueLoader = true;
diff --git a/lib/config-generator.js b/lib/config-generator.js
index aac8b3ce..26fca25c 100644
--- a/lib/config-generator.js
+++ b/lib/config-generator.js
@@ -29,6 +29,7 @@ const cssLoaderUtil = require('./loaders/css');
const sassLoaderUtil = require('./loaders/sass');
const lessLoaderUtil = require('./loaders/less');
const babelLoaderUtil = require('./loaders/babel');
+const tsLoaderUtil = require('./loaders/typescript');
const vueLoaderUtil = require('./loaders/vue');
class ConfigGenerator {
@@ -72,7 +73,7 @@ class ConfigGenerator {
config.stats = this.buildStatsConfig();
config.resolve = {
- extensions: ['.js', '.jsx', '.vue'],
+ extensions: ['.js', '.jsx', '.vue', '.ts', '.tsx'],
alias: {}
};
@@ -161,6 +162,14 @@ class ConfigGenerator {
});
}
+ if (this.webpackConfig.useTypeScriptLoader) {
+ this.webpackConfig.addLoader({
+ test: /\.tsx?$/,
+ exclude: /node_modules/,
+ use: tsLoaderUtil.getLoaders(this.webpackConfig)
+ });
+ }
+
this.webpackConfig.loaders.forEach((loader) => {
rules.push(loader);
});
diff --git a/lib/friendly-errors/transformers/missing-loader.js b/lib/friendly-errors/transformers/missing-loader.js
index 1f247cdf..a955f83c 100644
--- a/lib/friendly-errors/transformers/missing-loader.js
+++ b/lib/friendly-errors/transformers/missing-loader.js
@@ -49,6 +49,10 @@ function transform(error) {
case 'jsx':
error.loaderName = 'react';
break;
+ case 'tsx':
+ case 'ts':
+ error.loaderName = 'typescript';
+ break;
// add more as needed
default:
return error;
diff --git a/lib/loader-features.js b/lib/loader-features.js
index 3023688e..e5cf87e7 100644
--- a/lib/loader-features.js
+++ b/lib/loader-features.js
@@ -36,6 +36,11 @@ const loaderFeatures = {
packages: ['babel-preset-react'],
description: 'process React JS files'
},
+ typescript: {
+ method: 'enableTypeScriptLoader()',
+ packages: ['typescript', 'ts-loader'],
+ description: 'process TypeScript files'
+ },
vue: {
method: 'enableVueLoader()',
// vue is needed so the end-user can do things
diff --git a/lib/loaders/typescript.js b/lib/loaders/typescript.js
new file mode 100644
index 00000000..53d9f17a
--- /dev/null
+++ b/lib/loaders/typescript.js
@@ -0,0 +1,46 @@
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+'use strict';
+
+const loaderFeatures = require('../loader-features');
+const babelLoader = require('./babel');
+
+/**
+ * @param {WebpackConfig} webpackConfig
+ * @return {Array} of loaders to use for TypeScript
+ */
+module.exports = {
+ getLoaders(webpackConfig) {
+ loaderFeatures.ensureLoaderPackagesExist('typescript');
+
+ // some defaults
+ let config = {
+ silent: true,
+ };
+
+ // allow for ts-loader config to be controlled
+ webpackConfig.tsConfigurationCallback.apply(
+ // use config as the this variable
+ config,
+ [config]
+ );
+
+ // use ts alongside with babel
+ // @see https://github.com/TypeStrong/ts-loader/blob/master/README.md#babel
+ let loaders = babelLoader.getLoaders(webpackConfig);
+ return loaders.concat([
+ {
+ loader: 'ts-loader',
+ // @see https://github.com/TypeStrong/ts-loader/blob/master/README.md#available-options
+ options: config
+ }
+ ]);
+ }
+};
diff --git a/package.json b/package.json
index cd7686ec..41fe1bc5 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"loader-utils": "^1.1.0",
"lodash": ">=3.5 <5",
"pkg-up": "^1.0.0",
- "pretty-error": "^2.1.0",
+ "pretty-error": "^2.1.1",
"resolve-url-loader": "^2.0.2",
"style-loader": "^0.13.2",
"webpack": "^2.2.0",
@@ -64,6 +64,8 @@
"postcss-loader": "^1.3.3",
"sass-loader": "^6.0.3",
"sinon": "^2.3.4",
+ "ts-loader": "^2.1.0",
+ "typescript": "^2.3.4",
"vue": "^2.3.4",
"vue-loader": "^12.2.1",
"vue-template-compiler": "^2.3.4",
diff --git a/test/WebpackConfig.js b/test/WebpackConfig.js
index 857a8261..1d19e8b1 100644
--- a/test/WebpackConfig.js
+++ b/test/WebpackConfig.js
@@ -278,6 +278,27 @@ describe('WebpackConfig object', () => {
});
});
+ describe('enableTypeScriptLoader', () => {
+ it('Calling method sets it', () => {
+ const config = createConfig();
+ const testCallback = () => {};
+ config.enableTypeScriptLoader(testCallback);
+ expect(config.tsConfigurationCallback).to.equal(testCallback);
+ });
+
+ it('Calling with non-callback throws an error', () => {
+ const config = createConfig();
+
+ expect(() => {
+ config.enableTypeScriptLoader('FOO');
+ }).to.throw('must be a callback function');
+
+ expect(() => {
+ config.enableTypeScriptLoader();
+ }).to.throw('must be a callback function');
+ });
+ });
+
describe('addPlugin', () => {
it('extends the current registered plugins', () => {
const config = createConfig();
diff --git a/test/friendly-errors/transformers/missing-loader.js b/test/friendly-errors/transformers/missing-loader.js
index 268707cf..7a23effd 100644
--- a/test/friendly-errors/transformers/missing-loader.js
+++ b/test/friendly-errors/transformers/missing-loader.js
@@ -60,5 +60,18 @@ describe('transform/missing-loader', () => {
expect(actualError.type).to.deep.equal('loader-not-enabled');
expect(actualError.loaderName).to.deep.equal('sass');
});
+
+ it('Typescript error is properly transformed', () => {
+ const startError = {
+ name: 'ModuleParseError',
+ message: 'You may need an appropriate loader',
+ file: '/path/to/file.ts'
+ };
+ const actualError = transform(Object.assign({}, startError));
+
+ expect(actualError.name).to.deep.equal('Loader not enabled');
+ expect(actualError.type).to.deep.equal('loader-not-enabled');
+ expect(actualError.loaderName).to.deep.equal('typescript');
+ });
});
});
diff --git a/test/functional.js b/test/functional.js
index 2baa5f60..6a753cd0 100644
--- a/test/functional.js
+++ b/test/functional.js
@@ -45,7 +45,7 @@ describe('Functional tests using webpack', function() {
testSetup.emptyTmpDir();
});
- describe('Basic scenarios', () => {
+ describe('Basic scenarios.', () => {
it('Builds a few simple entries file + manifest.json', (done) => {
const config = createWebpackConfig('web/build', 'dev');
@@ -583,6 +583,40 @@ module.exports = {
});
});
+ it('When configured, TypeScript is compiled!', (done) => {
+ const config = createWebpackConfig('www/build', 'dev');
+ config.setPublicPath('/build');
+ config.addEntry('main', ['./js/render.ts', './js/index.ts']);
+ const testCallback = () => {};
+ config.enableTypeScriptLoader(testCallback);
+
+ testSetup.runWebpack(config, (webpackAssert) => {
+ // check that ts-loader transformed the ts file
+ webpackAssert.assertOutputFileContains(
+ 'main.js',
+ 'document.getElementById(\'app\').innerHTML = "Welcome to Your TypeScript App
";'
+ );
+
+ expect(config.outputPath).to.be.a.directory().with.deep.files([
+ 'main.js',
+ 'manifest.json'
+ ]);
+
+ testSetup.requestTestPage(
+ path.join(config.getContext(), 'www'),
+ [
+ 'build/main.js'
+ ],
+ (browser) => {
+
+ // assert that the ts module rendered
+ browser.assert.text('#app h1', 'Welcome to Your TypeScript App');
+ done();
+ }
+ );
+ });
+ });
+
it('The output directory is cleaned between builds', (done) => {
const config = createWebpackConfig('www/build', 'dev');
config.setPublicPath('/build');
diff --git a/test/loaders/typescript.js b/test/loaders/typescript.js
new file mode 100644
index 00000000..55a75301
--- /dev/null
+++ b/test/loaders/typescript.js
@@ -0,0 +1,51 @@
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+'use strict';
+
+const expect = require('chai').expect;
+const WebpackConfig = require('../../lib/WebpackConfig');
+const RuntimeConfig = require('../../lib/config/RuntimeConfig');
+const tsLoader = require('../../lib/loaders/typescript');
+
+function createConfig() {
+ const runtimeConfig = new RuntimeConfig();
+ runtimeConfig.context = __dirname;
+ runtimeConfig.babelRcFileExists = false;
+
+ return new WebpackConfig(runtimeConfig);
+}
+
+describe('loaders/typescript', () => {
+ it('getLoaders() basic usage', () => {
+ const config = createConfig();
+ config.enableTypeScriptLoader(function(config) {
+ config.foo = 'bar';
+ });
+
+ const actualLoaders = tsLoader.getLoaders(config);
+ expect(actualLoaders).to.have.lengthOf(2);
+ // callback is used
+ expect(actualLoaders[1].options.foo).to.equal('bar');
+ });
+
+ it('getLoaders() check defaults configuration values', () => {
+ const config = createConfig();
+ config.enableTypeScriptLoader(function(config) {
+ config.foo = 'bar';
+ });
+
+ const actualLoaders = tsLoader.getLoaders(config);
+ // callback is used
+ expect(actualLoaders[1].options.foo).to.equal('bar');
+ // defaults
+ expect(actualLoaders[1].options.silent).to.be.true;
+ });
+
+});
diff --git a/yarn.lock b/yarn.lock
index d3c9b6db..83db0613 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5281,6 +5281,15 @@ tryit@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+ts-loader@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-2.2.0.tgz#e5fd8f29b967c4fb42abc76396aa8127a1e49924"
+ dependencies:
+ colors "^1.0.3"
+ enhanced-resolve "^3.0.0"
+ loader-utils "^1.0.2"
+ semver "^5.0.1"
+
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -5324,6 +5333,10 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+typescript@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.0.tgz#aef5a8d404beba36ad339abf079ddddfffba86dd"
+
uglify-js@^2.8.27:
version "2.8.29"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"