diff --git a/fixtures/vuejs-jsx/App.css b/fixtures/vuejs-jsx/App.css
new file mode 100644
index 00000000..bc719469
--- /dev/null
+++ b/fixtures/vuejs-jsx/App.css
@@ -0,0 +1,8 @@
+#app {
+ font-family: 'Avenir', Helvetica, Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-align: center;
+ color: #2c3e50;
+ margin-top: 60px;
+}
diff --git a/fixtures/vuejs-jsx/App.jsx b/fixtures/vuejs-jsx/App.jsx
new file mode 100644
index 00000000..1fa1817a
--- /dev/null
+++ b/fixtures/vuejs-jsx/App.jsx
@@ -0,0 +1,20 @@
+import './App.css';
+import './App.scss';
+import './App.less';
+import Hello from './components/Hello';
+
+class TestClassSyntax {
+
+}
+
+export default {
+ name: 'app',
+ render() {
+ return (
+
+
}/)
+
+
+ );
+ },
+};
diff --git a/fixtures/vuejs-jsx/App.less b/fixtures/vuejs-jsx/App.less
new file mode 100644
index 00000000..b388ea23
--- /dev/null
+++ b/fixtures/vuejs-jsx/App.less
@@ -0,0 +1,3 @@
+#app {
+ margin-top: 40px;
+}
diff --git a/fixtures/vuejs-jsx/App.scss b/fixtures/vuejs-jsx/App.scss
new file mode 100644
index 00000000..35f925ce
--- /dev/null
+++ b/fixtures/vuejs-jsx/App.scss
@@ -0,0 +1,4 @@
+#app {
+ display: flex;
+ color: #2c3e90;
+}
diff --git a/fixtures/vuejs-jsx/assets/logo.png b/fixtures/vuejs-jsx/assets/logo.png
new file mode 100644
index 00000000..f3d2503f
Binary files /dev/null and b/fixtures/vuejs-jsx/assets/logo.png differ
diff --git a/fixtures/vuejs-jsx/components/Hello.css b/fixtures/vuejs-jsx/components/Hello.css
new file mode 100644
index 00000000..d721d13d
--- /dev/null
+++ b/fixtures/vuejs-jsx/components/Hello.css
@@ -0,0 +1,17 @@
+.h1, .h2 {
+ font-weight: normal;
+}
+
+.ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+.li {
+ display: inline-block;
+ margin: 0 10px;
+}
+
+.a {
+ color: #42b983;
+}
diff --git a/fixtures/vuejs-jsx/components/Hello.jsx b/fixtures/vuejs-jsx/components/Hello.jsx
new file mode 100644
index 00000000..0e1f150a
--- /dev/null
+++ b/fixtures/vuejs-jsx/components/Hello.jsx
@@ -0,0 +1,33 @@
+import styles from './Hello.css?module';
+
+export default {
+ name: 'hello',
+ data() {
+ return {
+ msg: 'Welcome to Your Vue.js App',
+ };
+ },
+ render() {
+ return (
+
+
{this.msg}
+
Essential Links
+
+
Ecosystem
+
+
+ );
+ },
+};
diff --git a/fixtures/vuejs-jsx/main.js b/fixtures/vuejs-jsx/main.js
new file mode 100644
index 00000000..8845066e
--- /dev/null
+++ b/fixtures/vuejs-jsx/main.js
@@ -0,0 +1,8 @@
+import Vue from 'vue'
+import App from './App'
+
+new Vue({
+ el: '#app',
+ template: '',
+ components: { App }
+})
diff --git a/index.js b/index.js
index 4749101b..c0045379 100644
--- a/index.js
+++ b/index.js
@@ -936,11 +936,26 @@ class Encore {
* options.preLoaders = { ... }
* });
*
+ * // or configure Encore-specific options
+ * Encore.enableVueLoader(() => {}, {
+ * // set optional Encore-specific options, for instance:
+ *
+ * // enable JSX usage in Vue components
+ * // https://vuejs.org/v2/guide/render-function.html#JSX
+ * useJsx: true
+ * })
+ *
+ * Supported options:
+ * * {boolean} useJsx (default=false)
+ * Configure Babel to use the preset "@vue/babel-preset-jsx",
+ * in order to enable JSX usage in Vue components.
+ *
* @param {function} vueLoaderOptionsCallback
+ * @param {object} encoreOptions
* @returns {Encore}
*/
- enableVueLoader(vueLoaderOptionsCallback = () => {}) {
- webpackConfig.enableVueLoader(vueLoaderOptionsCallback);
+ enableVueLoader(vueLoaderOptionsCallback = () => {}, encoreOptions = {}) {
+ webpackConfig.enableVueLoader(vueLoaderOptionsCallback, encoreOptions);
return this;
}
diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js
index e7849897..9d4b5d7a 100644
--- a/lib/WebpackConfig.js
+++ b/lib/WebpackConfig.js
@@ -88,6 +88,9 @@ class WebpackConfig {
useBuiltIns: false,
corejs: null,
};
+ this.vueOptions = {
+ useJsx: false,
+ };
// Features/Loaders options callbacks
this.postCssLoaderOptionsCallback = () => {};
@@ -602,7 +605,7 @@ class WebpackConfig {
forkedTypeScriptTypesCheckOptionsCallback;
}
- enableVueLoader(vueLoaderOptionsCallback = () => {}) {
+ enableVueLoader(vueLoaderOptionsCallback = () => {}, vueOptions = {}) {
this.useVueLoader = true;
if (typeof vueLoaderOptionsCallback !== 'function') {
@@ -610,6 +613,15 @@ class WebpackConfig {
}
this.vueLoaderOptionsCallback = vueLoaderOptionsCallback;
+
+ // Check allowed keys
+ for (const key of Object.keys(vueOptions)) {
+ if (!(key in this.vueOptions)) {
+ throw new Error(`"${key}" is not a valid key for enableVueLoader(). Valid keys: ${Object.keys(this.vueOptions).join(', ')}.`);
+ }
+ }
+
+ this.vueOptions = vueOptions;
}
enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}) {
diff --git a/lib/features.js b/lib/features.js
index a165c908..7043bd7b 100644
--- a/lib/features.js
+++ b/lib/features.js
@@ -89,6 +89,14 @@ const features = {
],
description: 'load VUE files'
},
+ 'vue-jsx': {
+ method: 'enableVueLoader()',
+ packages: [
+ { name: '@vue/babel-preset-jsx' },
+ { name: '@vue/babel-helper-vue-jsx-merge-props' }
+ ],
+ description: 'use Vue with JSX support'
+ },
eslint: {
method: 'enableEslintLoader()',
// eslint is needed so the end-user can do things
diff --git a/lib/loaders/babel.js b/lib/loaders/babel.js
index a2e40cae..8fd0d1fb 100644
--- a/lib/loaders/babel.js
+++ b/lib/loaders/babel.js
@@ -72,6 +72,11 @@ module.exports = {
}
}
+ if (webpackConfig.useVueLoader && webpackConfig.vueOptions.useJsx) {
+ loaderFeatures.ensurePackagesExistAndAreCorrectVersion('vue-jsx');
+ babelConfig.presets.push('@vue/babel-preset-jsx');
+ }
+
babelConfig = applyOptionsCallback(webpackConfig.babelConfigurationCallback, babelConfig);
}
diff --git a/package.json b/package.json
index 166efcb4..557e022f 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,8 @@
"devDependencies": {
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/preset-react": "^7.0.0",
+ "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.3",
+ "@vue/babel-preset-jsx": "^1.0.0-beta.3",
"autoprefixer": "^8.5.0",
"babel-eslint": "^10.0.1",
"chai": "^3.5.0",
diff --git a/test/WebpackConfig.js b/test/WebpackConfig.js
index e10e73f0..1fc1b4b1 100644
--- a/test/WebpackConfig.js
+++ b/test/WebpackConfig.js
@@ -876,6 +876,27 @@ describe('WebpackConfig object', () => {
expect(config.useVueLoader).to.be.true;
expect(config.vueLoaderOptionsCallback).to.equal(callback);
});
+
+ it('Should validate Encore-specific options', () => {
+ const config = createConfig();
+
+ expect(() => {
+ config.enableVueLoader(() => {}, {
+ notExisting: false,
+ });
+ }).to.throw('"notExisting" is not a valid key for enableVueLoader(). Valid keys: useJsx.');
+ });
+
+ it('Should set Encore-specific options', () => {
+ const config = createConfig();
+ config.enableVueLoader(() => {}, {
+ useJsx: true,
+ });
+
+ expect(config.vueOptions).to.deep.equal({
+ useJsx: true,
+ });
+ });
});
diff --git a/test/functional.js b/test/functional.js
index e7833ad0..078b2b32 100644
--- a/test/functional.js
+++ b/test/functional.js
@@ -1445,6 +1445,81 @@ module.exports = {
}, true);
});
+ it('Vue.js is compiled correctly with JSX support', (done) => {
+ const appDir = testSetup.createTestAppDir();
+
+ fs.writeFileSync(
+ path.join(appDir, 'postcss.config.js'),
+ `
+module.exports = {
+ plugins: [
+ require('autoprefixer')()
+ ]
+} `
+ );
+
+ const config = testSetup.createWebpackConfig(appDir, 'www/build', 'dev');
+ config.enableSingleRuntimeChunk();
+ config.setPublicPath('/build');
+ config.addEntry('main', './vuejs-jsx/main');
+ config.enableVueLoader(() => {}, {
+ useJsx: true,
+ });
+ config.enableSassLoader();
+ config.enableLessLoader();
+ config.configureBabel(function(config) {
+ expect(config.presets[0][0]).to.equal('@babel/preset-env');
+ config.presets[0][1].targets = {
+ chrome: 52
+ };
+ });
+
+ testSetup.runWebpack(config, (webpackAssert) => {
+ expect(config.outputPath).to.be.a.directory().with.deep.files([
+ 'main.js',
+ 'main.css',
+ 'images/logo.82b9c7a5.png',
+ 'manifest.json',
+ 'entrypoints.json',
+ 'runtime.js',
+ ]);
+
+ // test that our custom babel config is used
+ webpackAssert.assertOutputFileContains(
+ 'main.js',
+ 'class TestClassSyntax'
+ );
+
+ // test that global styles are working correctly
+ webpackAssert.assertOutputFileContains(
+ 'main.css',
+ '#app {'
+ );
+
+ // test that CSS Modules (for scoped styles) is used
+ webpackAssert.assertOutputFileContains(
+ 'main.css',
+ '.h1_' // `.h1` is transformed to `.h1_[a-zA-Z0-9]`
+ );
+
+ testSetup.requestTestPage(
+ path.join(config.getContext(), 'www'),
+ [
+ 'build/runtime.js',
+ 'build/main.js'
+ ],
+ (browser) => {
+ // assert that the vue.js app rendered
+ browser.assert.text('#app h1', 'Welcome to Your Vue.js App');
+ // make sure the styles are not inlined
+ browser.assert.elements('style', 0);
+
+ done();
+ }
+ );
+ });
+ });
+
it('configureUrlLoader() allows to use the URL loader for images/fonts', (done) => {
const config = createWebpackConfig('web/build', 'dev');
config.setPublicPath('/build');
diff --git a/test/loaders/babel.js b/test/loaders/babel.js
index 1f5e4c0f..6a022300 100644
--- a/test/loaders/babel.js
+++ b/test/loaders/babel.js
@@ -126,4 +126,22 @@ describe('loaders/babel', () => {
const actualLoaders = babelLoader.getLoaders(config);
expect(actualLoaders[0].options).to.deep.equal({ 'foo': true });
});
+
+ it('getLoaders() with Vue and JSX support', () => {
+ const config = createConfig();
+ config.enableVueLoader(() => {}, {
+ useJsx: true,
+ });
+
+ config.configureBabel(function(babelConfig) {
+ babelConfig.presets.push('foo');
+ });
+
+ const actualLoaders = babelLoader.getLoaders(config);
+
+ expect(actualLoaders[0].options.presets).to.deep.include.members([
+ '@vue/babel-preset-jsx',
+ 'foo'
+ ]);
+ });
});
diff --git a/yarn.lock b/yarn.lock
index 4deec35c..0ad32a24 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -345,6 +345,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
+"@babel/plugin-syntax-jsx@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7"
+ integrity sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
"@babel/plugin-syntax-object-rest-spread@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
@@ -792,6 +799,70 @@
"@types/uglify-js" "*"
source-map "^0.6.0"
+"@vue/babel-helper-vue-jsx-merge-props@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0-beta.3.tgz#e4c2e7125b3e0d2a9d493e457850b2abb0fd3cad"
+ integrity sha512-cbFQnd3dDPsfWuxbWW2phynX2zsckwC4GfAkcE1QH1lZL2ZAD2V97xY3BmvTowMkjeFObRKQt1P3KKA6AoB0hQ==
+
+"@vue/babel-plugin-transform-vue-jsx@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.0.0-beta.3.tgz#a1a44e801d8ed615e49f145ef1b3eaca2c16e2e6"
+ integrity sha512-yn+j2B/2aEagaxXrMSK3qcAJnlidfXg9v+qmytqrjUXc4zfi8QVC/b4zCev1FDmTip06/cs/csENA4law6Xhpg==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+ "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.3"
+ html-tags "^2.0.0"
+ lodash.kebabcase "^4.1.1"
+ svg-tags "^1.0.0"
+
+"@vue/babel-preset-jsx@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.0.0-beta.3.tgz#15c584bd62c0286a80f0196749ae38cde5cd703b"
+ integrity sha512-qMKGRorTI/0nE83nLEM7MyQiBZUqc62sZyjkBdVaaU7S61MHI8RKHPtbLMMZlWXb2NCJ0fQci8xJWUK5JE+TFA==
+ dependencies:
+ "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.3"
+ "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.3"
+ "@vue/babel-sugar-functional-vue" "^1.0.0-beta.3"
+ "@vue/babel-sugar-inject-h" "^1.0.0-beta.3"
+ "@vue/babel-sugar-v-model" "^1.0.0-beta.3"
+ "@vue/babel-sugar-v-on" "^1.0.0-beta.3"
+
+"@vue/babel-sugar-functional-vue@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.0.0-beta.3.tgz#41a855786971dacbbe8044858eefe98de089bf12"
+ integrity sha512-CBIa0sQWn3vfBS2asfTgv0WwdyKvNTKtE/cCfulZ7MiewLBh0RlvvSmdK9BIMTiHErdeZNSGUGlU6JuSHLyYkQ==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-inject-h@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.0.0-beta.3.tgz#be1d00b74a1a89fed35a9b1415a738c36f125966"
+ integrity sha512-HKMBMmFfdK9GBp3rX2bHIwILBdgc5F3ahmCB72keJxzaAQrgDAnD+ho70exUge+inAGlNF34WsQcGPElTf9QZg==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-v-model@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.0.0-beta.3.tgz#ea935b0e08bf58c125a1349b819156059590993c"
+ integrity sha512-et39eTEh7zW4wfZoSl9Jf0/n2r9OTT8U02LtSbXsjgYcqaDQFusN0+n7tw4bnOqvnnSVjEp7bVsQCWwykC3Wgg==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+ "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.3"
+ "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.3"
+ camelcase "^5.0.0"
+ html-tags "^2.0.0"
+ svg-tags "^1.0.0"
+
+"@vue/babel-sugar-v-on@^1.0.0-beta.3":
+ version "1.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.0.0-beta.3.tgz#2f5fedb43883f603fe76010f253b85c7465855fe"
+ integrity sha512-F+GapxCiy50jf2Q2B4exw+KYBzlGdeKMAMW1Dbvb0Oa59SA0CH6tsUOIAsXb0A05jwwg/of0LaVeo+4aLefVxQ==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+ "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.3"
+ camelcase "^5.0.0"
+
"@vue/component-compiler-utils@^1.2.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.3.1.tgz#686f0b913d59590ae327b2a1cb4b6d9b931bbe0e"
@@ -4072,6 +4143,11 @@ html-entities@^1.2.0:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
+html-tags@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b"
+ integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=
+
htmlparser2@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
@@ -4979,6 +5055,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+lodash.kebabcase@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
+ integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY=
+
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -7920,6 +8001,11 @@ supports-color@^6.1.0:
dependencies:
has-flag "^3.0.0"
+svg-tags@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
+ integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
+
svgo@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985"