Skip to content

Commit

Permalink
feature #553 Add JSX integration for Vue.js (Kocal)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
weaverryan committed Apr 10, 2019
2 parents 6e1b371 + 9cabf9b commit 99ce91d
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 3 deletions.
8 changes: 8 additions & 0 deletions 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;
}
20 changes: 20 additions & 0 deletions 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 (
<div id="app">
<img src={require('./assets/logo.png')}/>
<Hello></Hello>
</div>
);
},
};
3 changes: 3 additions & 0 deletions fixtures/vuejs-jsx/App.less
@@ -0,0 +1,3 @@
#app {
margin-top: 40px;
}
4 changes: 4 additions & 0 deletions fixtures/vuejs-jsx/App.scss
@@ -0,0 +1,4 @@
#app {
display: flex;
color: #2c3e90;
}
Binary file added fixtures/vuejs-jsx/assets/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions 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;
}
33 changes: 33 additions & 0 deletions 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 (
<div class="hello">
<h1 class={styles.h1}>{this.msg}</h1>
<h2 class={styles.h2}>Essential Links</h2>
<ul class={styles.ul}>
<li class={styles.li}><a class={styles.a} href="https://vuejs.org" target="_blank">Core Docs</a></li>
<li class={styles.li}><a class={styles.a} href="https://forum.vuejs.org" target="_blank">Forum</a></li>
<li class={styles.li}><a class={styles.a} href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
<li class={styles.li}><a class={styles.a} href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
<br/>
<li class={styles.li}><a class={styles.a} href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
</ul>
<h2 class={styles.h2}>Ecosystem</h2>
<ul class={styles.ul}>
<li class={styles.li}><a class={styles.a} href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
<li class={styles.li}><a class={styles.a} href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
<li class={styles.li}><a class={styles.a} href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
<li class={styles.li}><a class={styles.a} href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
</ul>
</div>
);
},
};
8 changes: 8 additions & 0 deletions fixtures/vuejs-jsx/main.js
@@ -0,0 +1,8 @@
import Vue from 'vue'
import App from './App'

new Vue({
el: '#app',
template: '<App/>',
components: { App }
})
19 changes: 17 additions & 2 deletions index.js
Expand Up @@ -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;
}
Expand Down
14 changes: 13 additions & 1 deletion lib/WebpackConfig.js
Expand Up @@ -88,6 +88,9 @@ class WebpackConfig {
useBuiltIns: false,
corejs: null,
};
this.vueOptions = {
useJsx: false,
};

// Features/Loaders options callbacks
this.postCssLoaderOptionsCallback = () => {};
Expand Down Expand Up @@ -602,14 +605,23 @@ class WebpackConfig {
forkedTypeScriptTypesCheckOptionsCallback;
}

enableVueLoader(vueLoaderOptionsCallback = () => {}) {
enableVueLoader(vueLoaderOptionsCallback = () => {}, vueOptions = {}) {
this.useVueLoader = true;

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

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 = () => {}) {
Expand Down
8 changes: 8 additions & 0 deletions lib/features.js
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/loaders/babel.js
Expand Up @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions test/WebpackConfig.js
Expand Up @@ -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,
});
});
});


Expand Down
75 changes: 75 additions & 0 deletions test/functional.js
Expand Up @@ -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');
Expand Down
18 changes: 18 additions & 0 deletions test/loaders/babel.js
Expand Up @@ -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'
]);
});
});

0 comments on commit 99ce91d

Please sign in to comment.