Skip to content

Commit 99ce91d

Browse files
committed
feature #553 Add JSX integration for Vue.js (Kocal)
This PR was squashed before being merged into the master branch (closes #553). Discussion ---------- Add JSX integration for Vue.js Will close #551. Doc: symfony/symfony-docs#11346 This PR enable JSX support in Vue.js with the following code: ```js Encore.enableVueLoader(() => {}, { useJsx: true }); ``` I've added inline documentation and some tests for: - `enableVueLoader()` behavior (and validation) - Babel loader rules generation - Functional test, with styles and scoped styles (using CSS Modules) As proof of concept for styles, after adding `<link href="build/main.css" rel="stylesheet">` in generated `testing.html` file: ![Capture d’écran de 2019-04-07 13-03-38](https://user-images.githubusercontent.com/2103975/55682600-27105a80-5936-11e9-8eb9-704b71aa7b49.png) - Styles from `App.css`, `App.scss` and `App.less` are applied globally correctly - Styles from `Hello.css` are applied correctly to `Hello.jsx` component only (`import styles from './Hello.css?module'`) <details> <summary>This is an example of generated `main.css` file</summary> ```css #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; } #app { display: flex; color: #2c3e90; } #app { margin-top: 40px; } .h1_jKs9e, .h2_3H2pR { font-weight: normal; } .ul_3us5c { list-style-type: none; padding: 0; } .li_3bINq { display: inline-block; margin: 0 10px; } .a_wKHXy { color: #42b983; } ``` </details> --- Some notes for the documentation: - Install `@vue/babel-preset-jsx` and `@babel/plugin-transform-react-jsx` - If you need to use scoped styles, use [CSS Modules](https://github.com/css-modules/css-modules) like this: ```css /* MyComponent.css */ .title { color: red } ``` ```jsx // MyComponent.jsx import styles from './MyComponent.css'; export default { name: 'MyComponent', render() { return ( <div class={styles.title}> My component! </div> ); } }; ``` - Not only CSS is supported for CSS Modules, Sass, Less and Stylus are supported too - If you need to require an image, `<img src="./assets/image.png">` will not work, you should require it yourself like `<img src={require("./assets.image.png")}/>`. Commits ------- 9cabf9b Add JSX integration for Vue.js
2 parents 6e1b371 + 9cabf9b commit 99ce91d

File tree

17 files changed

+338
-3
lines changed

17 files changed

+338
-3
lines changed

fixtures/vuejs-jsx/App.css

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#app {
2+
font-family: 'Avenir', Helvetica, Arial, sans-serif;
3+
-webkit-font-smoothing: antialiased;
4+
-moz-osx-font-smoothing: grayscale;
5+
text-align: center;
6+
color: #2c3e50;
7+
margin-top: 60px;
8+
}

fixtures/vuejs-jsx/App.jsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import './App.css';
2+
import './App.scss';
3+
import './App.less';
4+
import Hello from './components/Hello';
5+
6+
class TestClassSyntax {
7+
8+
}
9+
10+
export default {
11+
name: 'app',
12+
render() {
13+
return (
14+
<div id="app">
15+
<img src={require('./assets/logo.png')}/>
16+
<Hello></Hello>
17+
</div>
18+
);
19+
},
20+
};

fixtures/vuejs-jsx/App.less

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#app {
2+
margin-top: 40px;
3+
}

fixtures/vuejs-jsx/App.scss

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#app {
2+
display: flex;
3+
color: #2c3e90;
4+
}

fixtures/vuejs-jsx/assets/logo.png

6.69 KB
Loading
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.h1, .h2 {
2+
font-weight: normal;
3+
}
4+
5+
.ul {
6+
list-style-type: none;
7+
padding: 0;
8+
}
9+
10+
.li {
11+
display: inline-block;
12+
margin: 0 10px;
13+
}
14+
15+
.a {
16+
color: #42b983;
17+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import styles from './Hello.css?module';
2+
3+
export default {
4+
name: 'hello',
5+
data() {
6+
return {
7+
msg: 'Welcome to Your Vue.js App',
8+
};
9+
},
10+
render() {
11+
return (
12+
<div class="hello">
13+
<h1 class={styles.h1}>{this.msg}</h1>
14+
<h2 class={styles.h2}>Essential Links</h2>
15+
<ul class={styles.ul}>
16+
<li class={styles.li}><a class={styles.a} href="https://vuejs.org" target="_blank">Core Docs</a></li>
17+
<li class={styles.li}><a class={styles.a} href="https://forum.vuejs.org" target="_blank">Forum</a></li>
18+
<li class={styles.li}><a class={styles.a} href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
19+
<li class={styles.li}><a class={styles.a} href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
20+
<br/>
21+
<li class={styles.li}><a class={styles.a} href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
22+
</ul>
23+
<h2 class={styles.h2}>Ecosystem</h2>
24+
<ul class={styles.ul}>
25+
<li class={styles.li}><a class={styles.a} href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
26+
<li class={styles.li}><a class={styles.a} href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
27+
<li class={styles.li}><a class={styles.a} href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
28+
<li class={styles.li}><a class={styles.a} href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
29+
</ul>
30+
</div>
31+
);
32+
},
33+
};

fixtures/vuejs-jsx/main.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Vue from 'vue'
2+
import App from './App'
3+
4+
new Vue({
5+
el: '#app',
6+
template: '<App/>',
7+
components: { App }
8+
})

index.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -936,11 +936,26 @@ class Encore {
936936
* options.preLoaders = { ... }
937937
* });
938938
*
939+
* // or configure Encore-specific options
940+
* Encore.enableVueLoader(() => {}, {
941+
* // set optional Encore-specific options, for instance:
942+
*
943+
* // enable JSX usage in Vue components
944+
* // https://vuejs.org/v2/guide/render-function.html#JSX
945+
* useJsx: true
946+
* })
947+
*
948+
* Supported options:
949+
* * {boolean} useJsx (default=false)
950+
* Configure Babel to use the preset "@vue/babel-preset-jsx",
951+
* in order to enable JSX usage in Vue components.
952+
*
939953
* @param {function} vueLoaderOptionsCallback
954+
* @param {object} encoreOptions
940955
* @returns {Encore}
941956
*/
942-
enableVueLoader(vueLoaderOptionsCallback = () => {}) {
943-
webpackConfig.enableVueLoader(vueLoaderOptionsCallback);
957+
enableVueLoader(vueLoaderOptionsCallback = () => {}, encoreOptions = {}) {
958+
webpackConfig.enableVueLoader(vueLoaderOptionsCallback, encoreOptions);
944959

945960
return this;
946961
}

lib/WebpackConfig.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class WebpackConfig {
8888
useBuiltIns: false,
8989
corejs: null,
9090
};
91+
this.vueOptions = {
92+
useJsx: false,
93+
};
9194

9295
// Features/Loaders options callbacks
9396
this.postCssLoaderOptionsCallback = () => {};
@@ -602,14 +605,23 @@ class WebpackConfig {
602605
forkedTypeScriptTypesCheckOptionsCallback;
603606
}
604607

605-
enableVueLoader(vueLoaderOptionsCallback = () => {}) {
608+
enableVueLoader(vueLoaderOptionsCallback = () => {}, vueOptions = {}) {
606609
this.useVueLoader = true;
607610

608611
if (typeof vueLoaderOptionsCallback !== 'function') {
609612
throw new Error('Argument 1 to enableVueLoader() must be a callback function.');
610613
}
611614

612615
this.vueLoaderOptionsCallback = vueLoaderOptionsCallback;
616+
617+
// Check allowed keys
618+
for (const key of Object.keys(vueOptions)) {
619+
if (!(key in this.vueOptions)) {
620+
throw new Error(`"${key}" is not a valid key for enableVueLoader(). Valid keys: ${Object.keys(this.vueOptions).join(', ')}.`);
621+
}
622+
}
623+
624+
this.vueOptions = vueOptions;
613625
}
614626

615627
enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}) {

lib/features.js

+8
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ const features = {
8989
],
9090
description: 'load VUE files'
9191
},
92+
'vue-jsx': {
93+
method: 'enableVueLoader()',
94+
packages: [
95+
{ name: '@vue/babel-preset-jsx' },
96+
{ name: '@vue/babel-helper-vue-jsx-merge-props' }
97+
],
98+
description: 'use Vue with JSX support'
99+
},
92100
eslint: {
93101
method: 'enableEslintLoader()',
94102
// eslint is needed so the end-user can do things

lib/loaders/babel.js

+5
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ module.exports = {
7272
}
7373
}
7474

75+
if (webpackConfig.useVueLoader && webpackConfig.vueOptions.useJsx) {
76+
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('vue-jsx');
77+
babelConfig.presets.push('@vue/babel-preset-jsx');
78+
}
79+
7580
babelConfig = applyOptionsCallback(webpackConfig.babelConfigurationCallback, babelConfig);
7681
}
7782

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
"devDependencies": {
6060
"@babel/plugin-transform-react-jsx": "^7.0.0",
6161
"@babel/preset-react": "^7.0.0",
62+
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0-beta.3",
63+
"@vue/babel-preset-jsx": "^1.0.0-beta.3",
6264
"autoprefixer": "^8.5.0",
6365
"babel-eslint": "^10.0.1",
6466
"chai": "^3.5.0",

test/WebpackConfig.js

+21
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,27 @@ describe('WebpackConfig object', () => {
876876
expect(config.useVueLoader).to.be.true;
877877
expect(config.vueLoaderOptionsCallback).to.equal(callback);
878878
});
879+
880+
it('Should validate Encore-specific options', () => {
881+
const config = createConfig();
882+
883+
expect(() => {
884+
config.enableVueLoader(() => {}, {
885+
notExisting: false,
886+
});
887+
}).to.throw('"notExisting" is not a valid key for enableVueLoader(). Valid keys: useJsx.');
888+
});
889+
890+
it('Should set Encore-specific options', () => {
891+
const config = createConfig();
892+
config.enableVueLoader(() => {}, {
893+
useJsx: true,
894+
});
895+
896+
expect(config.vueOptions).to.deep.equal({
897+
useJsx: true,
898+
});
899+
});
879900
});
880901

881902

test/functional.js

+75
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,81 @@ module.exports = {
14451445
}, true);
14461446
});
14471447

1448+
it('Vue.js is compiled correctly with JSX support', (done) => {
1449+
const appDir = testSetup.createTestAppDir();
1450+
1451+
fs.writeFileSync(
1452+
path.join(appDir, 'postcss.config.js'),
1453+
`
1454+
module.exports = {
1455+
plugins: [
1456+
require('autoprefixer')()
1457+
]
1458+
} `
1459+
);
1460+
1461+
const config = testSetup.createWebpackConfig(appDir, 'www/build', 'dev');
1462+
config.enableSingleRuntimeChunk();
1463+
config.setPublicPath('/build');
1464+
config.addEntry('main', './vuejs-jsx/main');
1465+
config.enableVueLoader(() => {}, {
1466+
useJsx: true,
1467+
});
1468+
config.enableSassLoader();
1469+
config.enableLessLoader();
1470+
config.configureBabel(function(config) {
1471+
expect(config.presets[0][0]).to.equal('@babel/preset-env');
1472+
config.presets[0][1].targets = {
1473+
chrome: 52
1474+
};
1475+
});
1476+
1477+
testSetup.runWebpack(config, (webpackAssert) => {
1478+
expect(config.outputPath).to.be.a.directory().with.deep.files([
1479+
'main.js',
1480+
'main.css',
1481+
'images/logo.82b9c7a5.png',
1482+
'manifest.json',
1483+
'entrypoints.json',
1484+
'runtime.js',
1485+
]);
1486+
1487+
// test that our custom babel config is used
1488+
webpackAssert.assertOutputFileContains(
1489+
'main.js',
1490+
'class TestClassSyntax'
1491+
);
1492+
1493+
// test that global styles are working correctly
1494+
webpackAssert.assertOutputFileContains(
1495+
'main.css',
1496+
'#app {'
1497+
);
1498+
1499+
// test that CSS Modules (for scoped styles) is used
1500+
webpackAssert.assertOutputFileContains(
1501+
'main.css',
1502+
'.h1_' // `.h1` is transformed to `.h1_[a-zA-Z0-9]`
1503+
);
1504+
1505+
testSetup.requestTestPage(
1506+
path.join(config.getContext(), 'www'),
1507+
[
1508+
'build/runtime.js',
1509+
'build/main.js'
1510+
],
1511+
(browser) => {
1512+
// assert that the vue.js app rendered
1513+
browser.assert.text('#app h1', 'Welcome to Your Vue.js App');
1514+
// make sure the styles are not inlined
1515+
browser.assert.elements('style', 0);
1516+
1517+
done();
1518+
}
1519+
);
1520+
});
1521+
});
1522+
14481523
it('configureUrlLoader() allows to use the URL loader for images/fonts', (done) => {
14491524
const config = createWebpackConfig('web/build', 'dev');
14501525
config.setPublicPath('/build');

test/loaders/babel.js

+18
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,22 @@ describe('loaders/babel', () => {
126126
const actualLoaders = babelLoader.getLoaders(config);
127127
expect(actualLoaders[0].options).to.deep.equal({ 'foo': true });
128128
});
129+
130+
it('getLoaders() with Vue and JSX support', () => {
131+
const config = createConfig();
132+
config.enableVueLoader(() => {}, {
133+
useJsx: true,
134+
});
135+
136+
config.configureBabel(function(babelConfig) {
137+
babelConfig.presets.push('foo');
138+
});
139+
140+
const actualLoaders = babelLoader.getLoaders(config);
141+
142+
expect(actualLoaders[0].options.presets).to.deep.include.members([
143+
'@vue/babel-preset-jsx',
144+
'foo'
145+
]);
146+
});
129147
});

0 commit comments

Comments
 (0)