diff --git a/fixtures/vuejs/App.vue b/fixtures/vuejs/App.vue
new file mode 100644
index 00000000..41f16123
--- /dev/null
+++ b/fixtures/vuejs/App.vue
@@ -0,0 +1,50 @@
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fixtures/vuejs/assets/logo.png b/fixtures/vuejs/assets/logo.png
new file mode 100644
index 00000000..f3d2503f
Binary files /dev/null and b/fixtures/vuejs/assets/logo.png differ
diff --git a/fixtures/vuejs/components/Hello.vue b/fixtures/vuejs/components/Hello.vue
new file mode 100644
index 00000000..2d805395
--- /dev/null
+++ b/fixtures/vuejs/components/Hello.vue
@@ -0,0 +1,53 @@
+
+
+
{{ msg }}
+
Essential Links
+
+
Ecosystem
+
+
+
+
+
+
+
+
diff --git a/fixtures/vuejs/main.js b/fixtures/vuejs/main.js
new file mode 100644
index 00000000..8845066e
--- /dev/null
+++ b/fixtures/vuejs/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 2d1bbd56..cc7c35ee 100644
--- a/index.js
+++ b/index.js
@@ -330,7 +330,8 @@ module.exports = {
},
/**
- * If enabled, the react preset is added to Babel:
+ * If enabled, the react preset is added to Babel.
+ *
* https://babeljs.io/docs/plugins/preset-react/
*
* @returns {exports}
@@ -341,6 +342,28 @@ module.exports = {
return this;
},
+ /**
+ * If enabled, the Vue.js loader is enabled.
+ *
+ * https://github.com/vuejs/vue-loader
+ *
+ * Encore.enableVueLoader();
+ *
+ * // or configure the vue-loader options
+ * // https://vue-loader.vuejs.org/en/configurations/advanced.html
+ * Encore.enableVueLoader(function(options) {
+ * options.preLoaders = { ... }
+ * });
+ *
+ * @param {function} vueLoaderOptionsCallback
+ * @returns {exports}
+ */
+ enableVueLoader(vueLoaderOptionsCallback = () => {}) {
+ webpackConfig.enableVueLoader(vueLoaderOptionsCallback);
+
+ return this;
+ },
+
/**
* If enabled, the output directory is emptied between
* each build (to remove old files).
diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js
index abb8bffd..e1102d97 100644
--- a/lib/WebpackConfig.js
+++ b/lib/WebpackConfig.js
@@ -50,6 +50,8 @@ class WebpackConfig {
this.providedVariables = {};
this.babelConfigurationCallback = function() {};
this.useReact = false;
+ this.useVueLoader = false;
+ this.vueLoaderOptionsCallback = () => {};
this.loaders = [];
}
@@ -222,6 +224,16 @@ class WebpackConfig {
this.useReact = true;
}
+ enableVueLoader(vueLoaderOptionsCallback = () => {}) {
+ this.useVueLoader = true;
+
+ if (typeof vueLoaderOptionsCallback !== 'function') {
+ throw new Error('Argument 1 to enableVueLoader() must be a callback function.');
+ }
+
+ this.vueLoaderOptionsCallback = vueLoaderOptionsCallback;
+ }
+
cleanupOutputBeforeBuild() {
this.cleanupOutput = true;
}
diff --git a/lib/config-generator.js b/lib/config-generator.js
index 20358be5..aac8b3ce 100644
--- a/lib/config-generator.js
+++ b/lib/config-generator.js
@@ -11,6 +11,7 @@
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const extractText = require('./loaders/extract-text');
const ManifestPlugin = require('./webpack/webpack-manifest-plugin');
const DeleteUnusedEntriesJSPlugin = require('./webpack/delete-unused-entries-js-plugin');
const AssetOutputDisplayPlugin = require('./friendly-errors/asset-output-display-plugin');
@@ -21,11 +22,14 @@ const missingLoaderTransformer = require('./friendly-errors/transformers/missing
const missingLoaderFormatter = require('./friendly-errors/formatters/missing-loader');
const missingPostCssConfigTransformer = require('./friendly-errors/transformers/missing-postcss-config');
const missingPostCssConfigFormatter = require('./friendly-errors/formatters/missing-postcss-config');
+const vueUnactivatedLoaderTransformer = require('./friendly-errors/transformers/vue-unactivated-loader-error');
+const vueUnactivatedLoaderFormatter = require('./friendly-errors/formatters/vue-unactivated-loader-error');
const pathUtil = require('./config/path-util');
const cssLoaderUtil = require('./loaders/css');
const sassLoaderUtil = require('./loaders/sass');
const lessLoaderUtil = require('./loaders/less');
const babelLoaderUtil = require('./loaders/babel');
+const vueLoaderUtil = require('./loaders/vue');
class ConfigGenerator {
/**
@@ -68,9 +72,14 @@ class ConfigGenerator {
config.stats = this.buildStatsConfig();
config.resolve = {
- extensions: ['.js', '.jsx']
+ extensions: ['.js', '.jsx', '.vue'],
+ alias: {}
};
+ if (this.webpackConfig.useVueLoader) {
+ config.resolve.alias['vue$'] = 'vue/dist/vue.esm.js';
+ }
+
return config;
}
@@ -111,10 +120,7 @@ class ConfigGenerator {
},
{
test: /\.css$/,
- use: ExtractTextPlugin.extract({
- fallback: 'style-loader' + this.getSourceMapOption(),
- use: cssLoaderUtil.getLoaders(this.webpackConfig)
- })
+ use: extractText.extract(this.webpackConfig, cssLoaderUtil.getLoaders(this.webpackConfig, false))
},
{
test: /\.(png|jpg|jpeg|gif|ico|svg)$/,
@@ -137,20 +143,21 @@ class ConfigGenerator {
if (this.webpackConfig.useSassLoader) {
rules.push({
test: /\.s[ac]ss$/,
- use: ExtractTextPlugin.extract({
- fallback: 'style-loader' + this.getSourceMapOption(),
- use: sassLoaderUtil.getLoaders(this.webpackConfig)
- })
+ use: extractText.extract(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig))
});
}
if (this.webpackConfig.useLessLoader) {
rules.push({
test: /\.less/,
- use: ExtractTextPlugin.extract({
- fallback: 'style-loader' + this.getSourceMapOption(),
- use: lessLoaderUtil.getLoaders(this.webpackConfig)
- })
+ use: extractText.extract(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig))
+ });
+ }
+
+ if (this.webpackConfig.useVueLoader) {
+ rules.push({
+ test: /\.vue$/,
+ use: vueLoaderUtil.getLoaders(this.webpackConfig, this.webpackConfig.vueLoaderOptionsCallback)
});
}
@@ -313,11 +320,13 @@ class ConfigGenerator {
clearConsole: false,
additionalTransformers: [
missingLoaderTransformer,
- missingPostCssConfigTransformer
+ missingPostCssConfigTransformer,
+ vueUnactivatedLoaderTransformer
],
additionalFormatters: [
missingLoaderFormatter,
- missingPostCssConfigFormatter
+ missingPostCssConfigFormatter,
+ vueUnactivatedLoaderFormatter
],
compilationSuccessInfo: {
messages: []
@@ -377,10 +386,6 @@ class ConfigGenerator {
https: this.webpackConfig.useDevServerInHttps()
};
}
-
- getSourceMapOption() {
- return this.webpackConfig.useSourceMaps ? '?sourceMap' : '';
- }
}
/**
diff --git a/lib/friendly-errors/formatters/vue-unactivated-loader-error.js b/lib/friendly-errors/formatters/vue-unactivated-loader-error.js
new file mode 100644
index 00000000..267a0c68
--- /dev/null
+++ b/lib/friendly-errors/formatters/vue-unactivated-loader-error.js
@@ -0,0 +1,42 @@
+/*
+ * This file is part of the Symfony 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 chalk = require('chalk');
+
+function formatErrors(errors) {
+ if (errors.length === 0) {
+ return [];
+ }
+
+ let messages = [];
+ // there will be an error for *every* file, but showing
+ // the error over and over again is not helpful
+
+ messages.push(
+ chalk.red('Vue processing failed:')
+ );
+ messages.push('');
+ for (let error of errors) {
+ messages.push(` * ${error.message}`);
+ }
+
+ messages.push('');
+
+ return messages;
+}
+
+function format(errors) {
+ return formatErrors(errors.filter((e) => (
+ e.type === 'vue-unactivated-loader-error'
+ )));
+}
+
+module.exports = format;
diff --git a/lib/friendly-errors/transformers/vue-unactivated-loader-error.js b/lib/friendly-errors/transformers/vue-unactivated-loader-error.js
new file mode 100644
index 00000000..84b1b1c1
--- /dev/null
+++ b/lib/friendly-errors/transformers/vue-unactivated-loader-error.js
@@ -0,0 +1,39 @@
+/*
+ * This file is part of the Symfony 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 TYPE = 'vue-unactivated-loader-error';
+
+function isVueUnactivatedLoaderError(e) {
+ if (e.name !== 'ModuleBuildError') {
+ return false;
+ }
+
+ if (e.message.indexOf('Cannot process lang=') === -1) {
+ return false;
+ }
+
+ return true;
+}
+
+function transform(error) {
+ if (!isVueUnactivatedLoaderError(error)) {
+ return error;
+ }
+
+ error = Object.assign({}, error);
+
+ error.type = TYPE;
+ error.severity = 900;
+
+ return error;
+}
+
+module.exports = transform;
diff --git a/lib/loader-features.js b/lib/loader-features.js
index d3a3f74c..3023688e 100644
--- a/lib/loader-features.js
+++ b/lib/loader-features.js
@@ -35,6 +35,13 @@ const loaderFeatures = {
method: 'enableReactPreset()',
packages: ['babel-preset-react'],
description: 'process React JS files'
+ },
+ vue: {
+ method: 'enableVueLoader()',
+ // vue is needed so the end-user can do things
+ // vue-template-compiler is a peer dep of vue-loader
+ packages: ['vue', 'vue-loader', 'vue-template-compiler'],
+ description: 'load VUE files'
}
};
diff --git a/lib/loaders/css.js b/lib/loaders/css.js
index 22f6d338..002e2e46 100644
--- a/lib/loaders/css.js
+++ b/lib/loaders/css.js
@@ -13,10 +13,11 @@ const loaderFeatures = require('../loader-features');
/**
* @param {WebpackConfig} webpackConfig
+ * @param {bool} ignorePostCssLoader If true, postcss-loader will never be added
* @return {Array} of loaders to use for CSS files
*/
module.exports = {
- getLoaders(webpackConfig) {
+ getLoaders(webpackConfig, skipPostCssLoader) {
const cssLoaders = [
{
loader: 'css-loader',
@@ -27,7 +28,7 @@ module.exports = {
},
];
- if (webpackConfig.usePostCssLoader) {
+ if (webpackConfig.usePostCssLoader && !skipPostCssLoader) {
loaderFeatures.ensureLoaderPackagesExist('postcss');
cssLoaders.push({
diff --git a/lib/loaders/extract-text.js b/lib/loaders/extract-text.js
new file mode 100644
index 00000000..99c9b595
--- /dev/null
+++ b/lib/loaders/extract-text.js
@@ -0,0 +1,29 @@
+/*
+ * This file is part of the Symfony 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 ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+/**
+ * Wraps style loaders with the ExtractTextPlugin.
+ *
+ * @param {WebpackConfig} webpackConfig
+ * @param {Array} loaders An array of some style loaders
+ * @param {bool} useVueStyleLoader
+ * @return {Array}
+ */
+module.exports = {
+ extract(webpackConfig, loaders, useVueStyleLoader = false) {
+ return ExtractTextPlugin.extract({
+ fallback: (useVueStyleLoader ? 'vue-style-loader' : 'style-loader') + (webpackConfig.useSourceMaps ? '?sourceMap' : ''),
+ use: loaders
+ });
+ }
+};
diff --git a/lib/loaders/less.js b/lib/loaders/less.js
index 58f653a3..1324f1a7 100644
--- a/lib/loaders/less.js
+++ b/lib/loaders/less.js
@@ -14,14 +14,15 @@ const cssLoader = require('./css');
/**
* @param {WebpackConfig} webpackConfig
+ * @param {bool} ignorePostCssLoader If true, postcss-loader will never be added
* @return {Array} of loaders to use for Less files
*/
module.exports = {
- getLoaders(webpackConfig) {
+ getLoaders(webpackConfig, ignorePostCssLoader = false) {
loaderFeatures.ensureLoaderPackagesExist('less');
return [
- ...cssLoader.getLoaders(webpackConfig),
+ ...cssLoader.getLoaders(webpackConfig, ignorePostCssLoader),
{
loader: 'less-loader',
options: {
diff --git a/lib/loaders/sass.js b/lib/loaders/sass.js
index b4d6fd5b..308946ba 100644
--- a/lib/loaders/sass.js
+++ b/lib/loaders/sass.js
@@ -14,13 +14,15 @@ const cssLoader = require('./css');
/**
* @param {WebpackConfig} webpackConfig
+ * @param {Object} sassOption Options to pass to the loader
+ * @param {bool} ignorePostCssLoader If true, postcss-loader will never be added
* @return {Array} of loaders to use for Sass files
*/
module.exports = {
- getLoaders(webpackConfig) {
+ getLoaders(webpackConfig, sassOptions = {}, ignorePostCssLoader = false) {
loaderFeatures.ensureLoaderPackagesExist('sass');
- const sassLoaders = [...cssLoader.getLoaders(webpackConfig)];
+ const sassLoaders = [...cssLoader.getLoaders(webpackConfig, ignorePostCssLoader)];
if (true === webpackConfig.sassOptions.resolve_url_loader) {
// responsible for resolving SASS url() paths
// without this, all url() paths must be relative to the
@@ -35,10 +37,10 @@ module.exports = {
sassLoaders.push({
loader: 'sass-loader',
- options: {
+ options: Object.assign({}, sassOptions, {
// needed by the resolve-url-loader
sourceMap: (true === webpackConfig.sassOptions.resolve_url_loader) || webpackConfig.useSourceMaps
- }
+ }),
});
return sassLoaders;
diff --git a/lib/loaders/vue-unactivated-loader.js b/lib/loaders/vue-unactivated-loader.js
new file mode 100644
index 00000000..b61b5e24
--- /dev/null
+++ b/lib/loaders/vue-unactivated-loader.js
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the Symfony 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 loaderUtils = require('loader-utils');
+
+/**
+ * A "fake" loader that's set inside vue-loader for languages
+ * when they are not activated in Encore.
+ *
+ * For example, if the user has not called enableSassLoader(),
+ * then this loader is added, so that the user gets an error if
+ * they try to use lang="scss" inside Vue.
+ *
+ * This is necessary because vue-loader *always* automatically
+ * processes new lang values through a loader (e.g. lang="foo"
+ * will automatically try to use a foo-loader). If we did *not*
+ * register this as a loader for scss (for example), then the
+ * user *would* still be able to use lang="scss"... but it would
+ * not use our custom sass-loader configuration.
+ *
+ * @return {function}
+ */
+module.exports = function() {
+ const options = loaderUtils.getOptions(this) || {};
+
+ // the vue-unactivated-loader-error transformer expects some of this language
+ throw new Error(`Cannot process lang="${options.lang}" inside ${this.resourcePath}: the ${options.loaderName} is not activated. Call ${options.featureCommand} in webpack.config.js to enable it.`);
+};
diff --git a/lib/loaders/vue.js b/lib/loaders/vue.js
new file mode 100644
index 00000000..739e701c
--- /dev/null
+++ b/lib/loaders/vue.js
@@ -0,0 +1,133 @@
+/*
+ * This file is part of the Symfony 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('../loader-features');
+const cssLoaderUtil = require('./css');
+const sassLoaderUtil = require('./sass');
+const lessLoaderUtil = require('./less');
+const babelLoaderUtil = require('./babel');
+const extractText = require('./extract-text');
+
+/**
+ * @param {WebpackConfig} webpackConfig
+ * @param {function} vueLoaderOptionsCallback
+ * @return {Array} of loaders to use for Vue files
+ */
+module.exports = {
+ getLoaders(webpackConfig, vueLoaderOptionsCallback) {
+ loaderFeatures.ensureLoaderPackagesExist('vue');
+
+ /*
+ * The vue-loader passes the contents of