diff --git a/fixtures/js/handlebars.js b/fixtures/js/handlebars.js new file mode 100644 index 00000000..385cad1c --- /dev/null +++ b/fixtures/js/handlebars.js @@ -0,0 +1,5 @@ +var template = require('../templates/template.hbs'); + +document.getElementById('app').innerHTML = template({ + title: 'Welcome to Your Handlebars App' +}); diff --git a/fixtures/templates/template.hbs b/fixtures/templates/template.hbs new file mode 100644 index 00000000..0869f8cc --- /dev/null +++ b/fixtures/templates/template.hbs @@ -0,0 +1 @@ +

{{ titleĀ }}

diff --git a/index.js b/index.js index e345b756..082f2159 100644 --- a/index.js +++ b/index.js @@ -719,6 +719,27 @@ class Encore { return this; } + /** + * Call this if you plan on loading Handlebars files. + * + * Encore.enableHandlebarsLoader(); + * + * Or pass options to the loader + * + * Encore.enableHandlebarsLoader(function(options) { + * // https://github.com/pcardune/handlebars-loader + * // options.debug = true; + * }); + * + * @param {function} callback + * @returns {Encore} + */ + enableHandlebarsLoader(callback = () => {}) { + webpackConfig.enableHandlebarsLoader(callback); + + return this; + } + /** * Call this if you wish to disable the default * images loader. diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index 6f505124..8e96d533 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -65,6 +65,7 @@ class WebpackConfig { this.useCoffeeScriptLoader = false; this.useForkedTypeScriptTypeChecking = false; this.useWebpackNotifier = false; + this.useHandlebarsLoader = false; // Features/Loaders options this.sassOptions = { @@ -87,6 +88,7 @@ class WebpackConfig { this.vueLoaderOptionsCallback = () => {}; this.tsConfigurationCallback = () => {}; this.coffeeScriptConfigurationCallback = () => {}; + this.handlebarsConfigurationCallback = () => {}; // Plugins options this.cleanWebpackPluginPaths = ['**/*']; @@ -446,6 +448,16 @@ class WebpackConfig { this.notifierPluginOptionsCallback = notifierPluginOptionsCallback; } + enableHandlebarsLoader(callback = () => {}) { + this.useHandlebarsLoader = true; + + if (typeof callback !== 'function') { + throw new Error('Argument 1 to enableHandlebarsLoader() must be a callback function.'); + } + + this.handlebarsConfigurationCallback = callback; + } + disableImagesLoader() { this.useImagesLoader = false; } diff --git a/lib/config-generator.js b/lib/config-generator.js index f863b7e6..9fbe53a3 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -21,6 +21,7 @@ const babelLoaderUtil = require('./loaders/babel'); const tsLoaderUtil = require('./loaders/typescript'); const coffeeScriptLoaderUtil = require('./loaders/coffee-script'); const vueLoaderUtil = require('./loaders/vue'); +const handlebarsLoaderUtil = require('./loaders/handlebars'); // plugins utils const extractTextPluginUtil = require('./plugins/extract-text'); const deleteUnusedEntriesPluginUtil = require('./plugins/delete-unused-entries'); @@ -242,6 +243,13 @@ class ConfigGenerator { }); } + if (this.webpackConfig.useHandlebarsLoader) { + rules.push({ + test: /\.(handlebars|hbs)$/, + use: handlebarsLoaderUtil.getLoaders(this.webpackConfig) + }); + } + this.webpackConfig.loaders.forEach((loader) => { rules.push(loader); }); diff --git a/lib/features.js b/lib/features.js index 5eb92b4c..99a25711 100644 --- a/lib/features.js +++ b/lib/features.js @@ -77,6 +77,11 @@ const features = { method: 'configureUrlLoader()', packages: ['url-loader'], description: 'use the url-loader' + }, + handlebars: { + method: 'enableHandlebarsLoader()', + packages: ['handlebars', 'handlebars-loader'], + description: 'load Handlebars files' } }; diff --git a/lib/loaders/handlebars.js b/lib/loaders/handlebars.js new file mode 100644 index 00000000..bc06e036 --- /dev/null +++ b/lib/loaders/handlebars.js @@ -0,0 +1,32 @@ +/* + * This file is part of the Symfony Webpack Encore 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('../features'); +const applyOptionsCallback = require('../utils/apply-options-callback'); + +/** + * @param {WebpackConfig} webpackConfig + * @return {Array} of loaders to use for Handlebars + */ +module.exports = { + getLoaders(webpackConfig) { + loaderFeatures.ensurePackagesExist('handlebars'); + + const options = {}; + + return [ + { + loader: 'handlebars-loader', + options: applyOptionsCallback(webpackConfig.handlebarsConfigurationCallback, options) + } + ]; + } +}; diff --git a/package.json b/package.json index d6f8ba75..da31a87a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "eslint-plugin-header": "^1.0.0", "eslint-plugin-node": "^4.2.2", "fork-ts-checker-webpack-plugin": "^0.2.7", + "handlebars": "^4.0.11", + "handlebars-loader": "^1.7.0", "http-server": "^0.9.0", "less": "^2.7.2", "less-loader": "^4.0.2", diff --git a/test/WebpackConfig.js b/test/WebpackConfig.js index 6d57eefb..3203ce99 100644 --- a/test/WebpackConfig.js +++ b/test/WebpackConfig.js @@ -685,6 +685,28 @@ describe('WebpackConfig object', () => { }); }); + describe('enableHandlebarsLoader', () => { + + it('Call with no config', () => { + const config = createConfig(); + config.enableHandlebarsLoader(); + + expect(config.useHandlebarsLoader).to.be.true; + }); + + it('Pass config', () => { + const config = createConfig(); + const callback = (options) => { + options.debug = true; + }; + config.enableHandlebarsLoader(callback); + + expect(config.useHandlebarsLoader).to.be.true; + expect(config.handlebarsConfigurationCallback).to.equal(callback); + }); + + }); + describe('addPlugin', () => { it('extends the current registered plugins', () => { const config = createConfig(); diff --git a/test/config-generator.js b/test/config-generator.js index 19db6ee8..a406302d 100644 --- a/test/config-generator.js +++ b/test/config-generator.js @@ -354,6 +354,32 @@ describe('The config-generator function', () => { }); }); + describe('enableHandlebarsLoader() adds the handlebars-loader', () => { + + it('without enableHandlebarsLoader()', () => { + const config = createConfig(); + config.outputPath = '/tmp/output/public-path'; + config.publicPath = '/public-path'; + config.addEntry('main', './main'); + const actualConfig = configGenerator(config); + + expect(JSON.stringify(actualConfig.module.rules)).to.not.contain('handlebars-loader'); + }); + + it('enableHandlebarsLoader()', () => { + const config = createConfig(); + config.outputPath = '/tmp/output/public-path'; + config.publicPath = '/public-path'; + config.addEntry('main', './main'); + config.enableHandlebarsLoader(); + + const actualConfig = configGenerator(config); + + expect(JSON.stringify(actualConfig.module.rules)).to.contain('handlebars-loader'); + }); + + }); + describe('addLoader() adds a custom loader', () => { it('addLoader()', () => { const config = createConfig(); diff --git a/test/functional.js b/test/functional.js index 63a504c8..eb3f6db6 100644 --- a/test/functional.js +++ b/test/functional.js @@ -862,6 +862,32 @@ module.exports = { }); }); + it('When configured, Handlebars is compiled', (done) => { + const config = createWebpackConfig('www/build', 'dev'); + config.setPublicPath('/build'); + config.addEntry('main', ['./js/handlebars.js']); + const testCallback = () => {}; + config.enableHandlebarsLoader(testCallback); + + testSetup.runWebpack(config, () => { + 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) => { + browser.assert.text('#app h1', 'Welcome to Your Handlebars App'); + done(); + } + ); + }); + }); + it('The output directory is cleaned between builds', (done) => { const config = createWebpackConfig('www/build', 'dev'); config.setPublicPath('/build'); diff --git a/test/index.js b/test/index.js index aef7e23b..0a7c0e7b 100644 --- a/test/index.js +++ b/test/index.js @@ -260,6 +260,15 @@ describe('Public API', () => { }); + describe('enableHandlebarsLoader', () => { + + it('must return the API object', () => { + const returnedValue = api.enableHandlebarsLoader(); + expect(returnedValue).to.equal(api); + }); + + }); + describe('disableImagesLoader', () => { it('must return the API object', () => { diff --git a/test/loaders/handlebars.js b/test/loaders/handlebars.js new file mode 100644 index 00000000..09ddb5d4 --- /dev/null +++ b/test/loaders/handlebars.js @@ -0,0 +1,59 @@ +/* + * This file is part of the Symfony Webpack Encore 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 handlebarsLoader = require('../../lib/loaders/handlebars'); + +function createConfig() { + const runtimeConfig = new RuntimeConfig(); + runtimeConfig.context = __dirname; + runtimeConfig.babelRcFileExists = false; + + return new WebpackConfig(runtimeConfig); +} + +describe('loaders/handlebars', () => { + it('getLoaders() basic usage', () => { + const config = createConfig(); + config.enableHandlebarsLoader(); + + const actualLoaders = handlebarsLoader.getLoaders(config); + expect(actualLoaders).to.have.lengthOf(1); + expect(actualLoaders[0].options).to.be.empty; + }); + + it('getLoaders() with options callback', () => { + const config = createConfig(); + config.enableHandlebarsLoader((options) => { + options.debug = true; + }); + + const actualLoaders = handlebarsLoader.getLoaders(config); + expect(actualLoaders).to.have.lengthOf(1); + expect(actualLoaders[0].options.debug).to.be.true; + }); + + it('getLoaders() with options callback that returns an object', () => { + const config = createConfig(); + config.enableHandlebarsLoader((options) => { + options.debug = true; + + // This should override the original config + return { foo: true }; + }); + + const actualLoaders = handlebarsLoader.getLoaders(config); + expect(actualLoaders).to.have.lengthOf(1); + expect(actualLoaders[0].options).to.deep.equal({ foo: true }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 160e6e79..7860aee3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -308,7 +308,7 @@ async@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/async/-/async-0.9.0.tgz#ac3613b1da9bed1b47510bb4651b8931e47146c7" -async@^1.5.2: +async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -318,6 +318,10 @@ async@^2.1.2, async@^2.4.1: dependencies: lodash "^4.14.0" +async@~0.2.10: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2508,7 +2512,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" -fastparse@^1.1.1: +fastparse@^1.0.0, fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" @@ -2930,6 +2934,25 @@ handle-thing@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" +handlebars-loader@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/handlebars-loader/-/handlebars-loader-1.7.0.tgz#4f750bc62c350fb922e52d8564d667887e909723" + dependencies: + async "~0.2.10" + fastparse "^1.0.0" + loader-utils "1.0.x" + object-assign "^4.1.0" + +handlebars@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" @@ -3833,6 +3856,14 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" +loader-utils@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.0.4.tgz#13f56197f1523a305891248b4c7244540848426c" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" @@ -4576,7 +4607,7 @@ opn@^5.1.0: dependencies: is-wsl "^1.1.0" -optimist@0.6.x: +optimist@0.6.x, optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -6037,7 +6068,7 @@ source-map@0.1.x, source-map@^0.1.38: dependencies: amdefine ">=0.0.4" -source-map@^0.4.2: +source-map@^0.4.2, source-map@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: @@ -6499,7 +6530,7 @@ ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" -uglify-js@^2.8.29: +uglify-js@^2.6, uglify-js@^2.8.29: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" dependencies: