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 @@ + + + + + + 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