diff --git a/index.js b/index.js index 3177cd2c..1e922629 100644 --- a/index.js +++ b/index.js @@ -387,6 +387,26 @@ const publicApi = { return this; }, + /** + * If enabled, a Preact preset will be applied to + * the generated Webpack configuration. + * + * Encore.enablePreactPreset() + * + * If you wish to also use preact-compat (https://github.com/developit/preact-compat) + * you can enable it by setting the "preactCompat" option to true: + * + * Encore.enablePreactPreset({ preactCompat: true }) + * + * @param {object} options + * @returns {exports} + */ + enablePreactPreset(options = {}) { + webpackConfig.enablePreactPreset(options); + + return this; + }, + /** * Call this if you plan on loading TypeScript files. * diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index f1c354dc..3d5ab2f8 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -53,6 +53,10 @@ class WebpackConfig { this.providedVariables = {}; this.babelConfigurationCallback = function() {}; this.useReact = false; + this.usePreact = false; + this.preactOptions = { + preactCompat: false + }; this.useVueLoader = false; this.vueLoaderOptionsCallback = () => {}; this.loaders = []; @@ -247,6 +251,18 @@ class WebpackConfig { this.useReact = true; } + enablePreactPreset(options = {}) { + this.usePreact = true; + + for (const optionKey of Object.keys(options)) { + if (!(optionKey in this.preactOptions)) { + throw new Error(`Invalid option "${optionKey}" passed to enablePreactPreset(). Valid keys are ${Object.keys(this.preactOptions).join(', ')}`); + } + + this.preactOptions[optionKey] = options[optionKey]; + } + } + enableTypeScriptLoader(callback = () => {}) { this.useTypeScriptLoader = true; diff --git a/lib/config-generator.js b/lib/config-generator.js index 1440dfd4..f8171317 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -81,6 +81,11 @@ class ConfigGenerator { config.resolve.alias['vue$'] = 'vue/dist/vue.esm.js'; } + if (this.webpackConfig.usePreact && this.webpackConfig.preactOptions.preactCompat) { + config.resolve.alias['react'] = 'preact-compat'; + config.resolve.alias['react-dom'] = 'preact-compat'; + } + return config; } diff --git a/lib/features.js b/lib/features.js index 94dacf82..6d419dea 100644 --- a/lib/features.js +++ b/lib/features.js @@ -36,6 +36,11 @@ const features = { packages: ['babel-preset-react'], description: 'process React JS files' }, + preact: { + method: 'enablePreactPreset()', + packages: ['babel-plugin-transform-react-jsx'], + description: 'process Preact JS files' + }, typescript: { method: 'enableTypeScriptLoader()', packages: ['typescript', 'ts-loader'], diff --git a/lib/loaders/babel.js b/lib/loaders/babel.js index 22a6a86c..83cf9b20 100644 --- a/lib/loaders/babel.js +++ b/lib/loaders/babel.js @@ -50,6 +50,23 @@ module.exports = { babelConfig.presets.push('react'); } + if (webpackConfig.usePreact) { + loaderFeatures.ensurePackagesExist('preact'); + + if (webpackConfig.preactOptions.preactCompat) { + // If preact-compat is enabled tell babel to + // transform JSX into React.createElement calls. + babelConfig.plugins.push(['transform-react-jsx']); + } else { + // If preact-compat is disabled tell babel to + // transform JSX into Preact h() calls. + babelConfig.plugins.push([ + 'transform-react-jsx', + { 'pragma': 'h' } + ]); + } + } + // allow for babel config to be controlled webpackConfig.babelConfigurationCallback.apply( // use babelConfig as the this variable diff --git a/package.json b/package.json index df12d016..b2def932 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "devDependencies": { "autoprefixer": "^6.7.7", + "babel-plugin-transform-react-jsx": "^6.24.1", "babel-preset-react": "^6.23.0", "chai": "^3.5.0", "chai-fs": "^1.0.0", @@ -63,6 +64,8 @@ "node-sass": "^4.5.3", "nsp": "^2.6.3", "postcss-loader": "^1.3.3", + "preact": "^8.2.1", + "preact-compat": "^3.17.0", "sass-loader": "^6.0.3", "sinon": "^2.3.4", "ts-loader": "^2.1.0", diff --git a/test/WebpackConfig.js b/test/WebpackConfig.js index 192c6910..afc1960e 100644 --- a/test/WebpackConfig.js +++ b/test/WebpackConfig.js @@ -364,6 +364,35 @@ describe('WebpackConfig object', () => { }); }); + describe('enablePreactPreset', () => { + it('Without preact-compat', () => { + const config = createConfig(); + config.enablePreactPreset(); + + expect(config.usePreact).to.be.true; + expect(config.preactOptions.preactCompat).to.be.false; + }); + + it('With preact-compat', () => { + const config = createConfig(); + config.enablePreactPreset({ + preactCompat: true + }); + + expect(config.usePreact).to.be.true; + expect(config.preactOptions.preactCompat).to.be.true; + }); + + it('With an invalid option', () => { + const config = createConfig(); + expect(() => { + config.enablePreactPreset({ + foo: true + }); + }).to.throw('Invalid option "foo"'); + }); + }); + describe('enableTypeScriptLoader', () => { it('Calling method sets it', () => { const config = createConfig(); diff --git a/test/config-generator.js b/test/config-generator.js index cbbe710b..543ef8a4 100644 --- a/test/config-generator.js +++ b/test/config-generator.js @@ -529,4 +529,32 @@ describe('The config-generator function', () => { expect(fontsRule.options.name).to.equal('[name].bar.[ext]'); }); }); + + describe('Test preact preset', () => { + describe('Without preact-compat', () => { + it('enablePreactPreset() does not add aliases to use preact-compat', () => { + const config = createConfig(); + config.outputPath = '/tmp/public/build'; + config.setPublicPath('/build/'); + config.enablePreactPreset(); + + const actualConfig = configGenerator(config); + expect(actualConfig.resolve.alias).to.not.include.keys('react', 'react-dom'); + }); + }); + + describe('With preact-compat', () => { + it('enablePreactPreset({ preactCompat: true }) adds aliases to use preact-compat', () => { + const config = createConfig(); + config.outputPath = '/tmp/public/build'; + config.setPublicPath('/build/'); + config.enablePreactPreset({ preactCompat: true }); + + const actualConfig = configGenerator(config); + expect(actualConfig.resolve.alias).to.include.keys('react', 'react-dom'); + expect(actualConfig.resolve.alias['react']).to.equal('preact-compat'); + expect(actualConfig.resolve.alias['react-dom']).to.equal('preact-compat'); + }); + }); + }); }); diff --git a/test/functional.js b/test/functional.js index 94b8f167..e96c094b 100644 --- a/test/functional.js +++ b/test/functional.js @@ -630,6 +630,40 @@ module.exports = { }); }); + it('When enabled, preact JSX is transformed without preact-compat!', (done) => { + const config = createWebpackConfig('www/build', 'dev'); + config.setPublicPath('/build'); + config.addEntry('main', './js/CoolReactComponent.jsx'); + config.enablePreactPreset(); + + testSetup.runWebpack(config, (webpackAssert) => { + // check that babel transformed the JSX + webpackAssert.assertOutputFileContains( + 'main.js', + 'var hiGuys = h(' + ); + + done(); + }); + }); + + it('When enabled, preact JSX is transformed with preact-compat!', (done) => { + const config = createWebpackConfig('www/build', 'dev'); + config.setPublicPath('/build'); + config.addEntry('main', './js/CoolReactComponent.jsx'); + config.enablePreactPreset({ preactCompat: true }); + + testSetup.runWebpack(config, (webpackAssert) => { + // check that babel transformed the JSX + webpackAssert.assertOutputFileContains( + 'main.js', + 'React.createElement' + ); + + done(); + }); + }); + it('When configured, TypeScript is compiled!', (done) => { const config = createWebpackConfig('www/build', 'dev'); config.setPublicPath('/build'); diff --git a/test/index.js b/test/index.js index 881653c2..3a9d2676 100644 --- a/test/index.js +++ b/test/index.js @@ -179,6 +179,15 @@ describe('Public API', () => { }); + describe('enablePreactPreset', () => { + + it('must return the API object', () => { + const returnedValue = api.enablePreactPreset(); + expect(returnedValue).to.equal(api); + }); + + }); + describe('enableTypeScriptLoader', () => { it('must return the API object', () => { diff --git a/test/loaders/babel.js b/test/loaders/babel.js index 7d9be172..5cffa9cd 100644 --- a/test/loaders/babel.js +++ b/test/loaders/babel.js @@ -65,4 +65,40 @@ describe('loaders/babel', () => { // foo is also still there, not overridden expect(actualLoaders[0].options.presets).to.include('foo'); }); + + it('getLoaders() with preact', () => { + const config = createConfig(); + config.enablePreactPreset(); + + config.configureBabel(function(babelConfig) { + babelConfig.plugins.push('foo'); + }); + + const actualLoaders = babelLoader.getLoaders(config); + + // transform-react-jsx & foo + expect(actualLoaders[0].options.plugins).to.have.lengthOf(2); + expect(actualLoaders[0].options.plugins).to.deep.include.members([ + ['transform-react-jsx', { pragma: 'h' }], + 'foo' + ]); + }); + + it('getLoaders() with preact and preact-compat', () => { + const config = createConfig(); + config.enablePreactPreset({ preactCompat: true }); + + config.configureBabel(function(babelConfig) { + babelConfig.plugins.push('foo'); + }); + + const actualLoaders = babelLoader.getLoaders(config); + + // transform-react-jsx & foo + expect(actualLoaders[0].options.plugins).to.have.lengthOf(2); + expect(actualLoaders[0].options.plugins).to.deep.include.members([ + ['transform-react-jsx'], + 'foo' + ]); + }); }); diff --git a/yarn.lock b/yarn.lock index 24cfaec8..0b1dbe38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1828,6 +1828,12 @@ encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" @@ -2176,6 +2182,18 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" +fbjs@^0.8.9: + version "0.8.14" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2676,7 +2694,7 @@ https-proxy-agent@^1.0.0: debug "2" extend "3" -iconv-lite@^0.4.13: +iconv-lite@^0.4.13, iconv-lite@~0.4.13: version "0.4.18" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" @@ -2696,6 +2714,12 @@ image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" +immutability-helper@^2.1.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.3.1.tgz#8ccfce92157208c120b2afad7ed05c11114c086e" + dependencies: + invariant "^2.2.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2926,7 +2950,7 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" -is-stream@^1.1.0: +is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -2970,6 +2994,13 @@ isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -3343,7 +3374,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -3576,6 +3607,13 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +node-fetch@^1.0.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.2.tgz#c54e9aac57e432875233525f3c891c4159ffefd7" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-forge@0.6.33: version "0.6.33" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" @@ -4322,6 +4360,30 @@ postcss@^6.0.1: source-map "^0.5.6" supports-color "^4.2.0" +preact-compat@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/preact-compat/-/preact-compat-3.17.0.tgz#528cfdfc301190c1a0f47567336be1f4be0266b3" + dependencies: + immutability-helper "^2.1.2" + preact-render-to-string "^3.6.0" + preact-transition-group "^1.1.0" + prop-types "^15.5.8" + standalone-react-addons-pure-render-mixin "^0.1.1" + +preact-render-to-string@^3.6.0: + version "3.6.3" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-3.6.3.tgz#481d0d5bdac9192d3347557437d5cd00aa312043" + dependencies: + pretty-format "^3.5.1" + +preact-transition-group@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/preact-transition-group/-/preact-transition-group-1.1.1.tgz#f0a49327ea515ece34ea2be864c4a7d29e5d6e10" + +preact@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-8.2.1.tgz#674243df0c847884d019834044aa2fcd311e72ed" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -4341,6 +4403,10 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" +pretty-format@^3.5.1: + version "3.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" + prime@0.0.5-alpha: version "0.0.5-alpha" resolved "https://registry.yarnpkg.com/prime/-/prime-0.0.5-alpha.tgz#e4d49a657bed2eb30e513adc6e5dfb83622e90c1" @@ -4367,6 +4433,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.5.8: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -4893,7 +4966,7 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -5101,6 +5174,10 @@ stackframe@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.3.tgz#fe64ab20b170e4ce49044b126c119dfa0e5dc7cc" +standalone-react-addons-pure-render-mixin@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/standalone-react-addons-pure-render-mixin/-/standalone-react-addons-pure-render-mixin-0.1.1.tgz#3c7409f4c79c40de9ac72c616cf679a994f37551" + "statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -5406,6 +5483,10 @@ typescript@^2.3.4: version "2.4.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844" +ua-parser-js@^0.7.9: + version "0.7.14" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" + uglify-js@^2.8.29: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -5697,6 +5778,10 @@ websocket-extensions@>=0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + whatwg-url-compat@~0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz#00898111af689bb097541cd5a45ca6c8798445bf"