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"