From 29d01f54c4147af2b0599a11bcab62f489b67382 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 14 Jan 2018 00:31:20 -0500 Subject: [PATCH] refactor + tests for CSS resolver --- .../@vue/cli-service/__mocks__/mock-config.js | 3 - .../__mocks__/vue-cli-plugin-foo.js | 1 - .../cli-service/__tests__/Service.spec.js | 4 +- .../@vue/cli-service/__tests__/css.spec.js | 160 ++++++++++++++++++ packages/@vue/cli-service/lib/Service.js | 15 +- packages/@vue/cli-service/lib/config/base.js | 2 +- packages/@vue/cli-service/lib/config/css.js | 25 +-- packages/@vue/cli-service/lib/options.js | 38 +++-- .../lib/webpack/CSSLoaderResolver.js | 136 ++++----------- packages/@vue/cli-service/package.json | 1 + yarn.lock | 4 + 11 files changed, 249 insertions(+), 140 deletions(-) delete mode 100644 packages/@vue/cli-service/__mocks__/mock-config.js delete mode 100644 packages/@vue/cli-service/__mocks__/vue-cli-plugin-foo.js create mode 100644 packages/@vue/cli-service/__tests__/css.spec.js diff --git a/packages/@vue/cli-service/__mocks__/mock-config.js b/packages/@vue/cli-service/__mocks__/mock-config.js deleted file mode 100644 index 8725416a4..000000000 --- a/packages/@vue/cli-service/__mocks__/mock-config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - lintOnSave: false -} diff --git a/packages/@vue/cli-service/__mocks__/vue-cli-plugin-foo.js b/packages/@vue/cli-service/__mocks__/vue-cli-plugin-foo.js deleted file mode 100644 index edda750dd..000000000 --- a/packages/@vue/cli-service/__mocks__/vue-cli-plugin-foo.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = api => {} diff --git a/packages/@vue/cli-service/__tests__/Service.spec.js b/packages/@vue/cli-service/__tests__/Service.spec.js index de0d18e67..7e7932cd7 100644 --- a/packages/@vue/cli-service/__tests__/Service.spec.js +++ b/packages/@vue/cli-service/__tests__/Service.spec.js @@ -1,6 +1,6 @@ jest.mock('fs') -jest.mock('mock-config') -jest.mock('vue-cli-plugin-foo') +jest.mock('mock-config', () => ({ lintOnSave: false }), { virtual: true }) +jest.mock('vue-cli-plugin-foo', () => () => {}, { virtual: true }) const fs = require('fs') const path = require('path') diff --git a/packages/@vue/cli-service/__tests__/css.spec.js b/packages/@vue/cli-service/__tests__/css.spec.js new file mode 100644 index 000000000..ae8bba633 --- /dev/null +++ b/packages/@vue/cli-service/__tests__/css.spec.js @@ -0,0 +1,160 @@ +const Service = require('../lib/Service') + +const LANGS = ['css', 'sass', 'scss', 'less', 'styl', 'stylus'] + +const LOADERS = { + css: 'css', + sass: 'sass', + scss: 'sass', + less: 'less', + styl: 'stylus', + stylus: 'stylus' +} + +const genConfig = (pkg = {}, env) => { + const prevEnv = process.env.NODE_ENV + if (env) process.env.NODE_ENV = env + const config = new Service('/', { pkg }).resolveWebpackConfig() + process.env.NODE_ENV = prevEnv + return config +} + +const findRule = (config, lang) => config.module.rules.find(rule => { + const test = rule.test.toString().replace(/\\/g, '') + return test.indexOf(`${lang}$`) > -1 +}) + +const findLoaders = (config, lang) => { + const rule = findRule(config, lang) + return rule.use.map(({ loader }) => loader.replace(/-loader$/, '')) +} + +const findOptions = (config, lang, _loader) => { + const rule = findRule(config, lang) + const use = rule.use.find(({ loader }) => `${_loader}-loader` === loader) + return use.options +} + +const findUsesForVue = (config, lang) => { + const vueOptions = findOptions(config, 'vue', 'vue') + return vueOptions.loaders[lang] +} + +const findLoadersForVue = (config, lang) => { + return findUsesForVue(config, lang).map(({ loader }) => loader.replace(/-loader$/, '')) +} + +const findOptionsForVue = (config, lang, _loader) => { + const uses = findUsesForVue(config, lang) + const use = uses.find(({ loader }) => `${_loader}-loader` === loader) + return use.options +} + +const expectedCssLoaderModulesOptions = { + importLoaders: 1, + localIdentName: `[name]_[local]__[hash:base64:5]`, + minimize: false, + sourceMap: false, + modules: true +} + +test('default loaders', () => { + const config = genConfig() + + LANGS.forEach(lang => { + const loader = lang === 'css' ? [] : LOADERS[lang] + expect(findLoaders(config, lang)).toEqual(['vue-style', 'css', 'postcss'].concat(loader)) + // vue-loader loaders should not include postcss because it's built-in + expect(findLoadersForVue(config, lang)).toEqual(['vue-style', 'css'].concat(loader)) + // assert css-loader options + expect(findOptions(config, lang, 'css')).toEqual({ + minimize: false, + sourceMap: false + }) + // files ending in .module.lang + expect(findOptions(config, `module.${lang}`, 'css')).toEqual(expectedCssLoaderModulesOptions) + }) + + // sass indented syntax + expect(findOptions(config, 'sass', 'sass')).toEqual({ indentedSyntax: true, sourceMap: false }) + expect(findOptionsForVue(config, 'sass', 'sass')).toEqual({ indentedSyntax: true, sourceMap: false }) +}) + +test('production defaults', () => { + const config = genConfig({}, 'production') + const extractLoaderPath = require.resolve('extract-text-webpack-plugin/dist/loader') + LANGS.forEach(lang => { + const loader = lang === 'css' ? [] : LOADERS[lang] + expect(findLoaders(config, lang)).toEqual([extractLoaderPath, 'vue-style', 'css', 'postcss'].concat(loader)) + expect(findLoadersForVue(config, lang)).toEqual([extractLoaderPath, 'vue-style', 'css'].concat(loader)) + expect(findOptions(config, lang, 'css')).toEqual({ + minimize: true, + sourceMap: false + }) + }) +}) + +test('css.modules', () => { + const config = genConfig({ + vue: { + css: { + modules: true + } + } + }) + LANGS.forEach(lang => { + expect(findOptions(config, lang, 'css')).toEqual(expectedCssLoaderModulesOptions) + }) +}) + +test('css.extract', () => { + const config = genConfig({ + vue: { + css: { + extract: false + } + } + }, 'production') + const extractLoaderPath = require.resolve('extract-text-webpack-plugin/dist/loader') + LANGS.forEach(lang => { + expect(findLoaders(config, lang)).not.toContain(extractLoaderPath) + expect(findLoadersForVue(config, lang)).not.toContain(extractLoaderPath) + }) +}) + +test('css.sourceMap', () => { + const config = genConfig({ + vue: { + css: { + sourceMap: true + } + } + }) + LANGS.forEach(lang => { + expect(findOptions(config, lang, 'css').sourceMap).toBe(true) + expect(findOptions(config, lang, 'postcss').sourceMap).toBe(true) + expect(findOptions(config, lang, LOADERS[lang]).sourceMap).toBe(true) + expect(findOptionsForVue(config, lang, 'css').sourceMap).toBe(true) + expect(findOptionsForVue(config, lang, LOADERS[lang]).sourceMap).toBe(true) + }) +}) + +test('css.loaderOptions', () => { + const data = '$env: production;' + const config = genConfig({ + vue: { + css: { + loaderOptions: { + sass: { + data + } + } + } + } + }) + + expect(findOptions(config, 'scss', 'sass')).toEqual({ data, sourceMap: false }) + expect(findOptionsForVue(config, 'scss', 'sass')).toEqual({ data, sourceMap: false }) + expect(findOptions(config, 'sass', 'sass')).toEqual({ data, indentedSyntax: true, sourceMap: false }) + expect(findOptionsForVue(config, 'sass', 'sass')).toEqual({ data, indentedSyntax: true, sourceMap: false }) +}) diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index f2abe165e..69c2c8c81 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -4,6 +4,7 @@ const debug = require('debug') const chalk = require('chalk') const readPkg = require('read-pkg') const merge = require('webpack-merge') +const deepMerge = require('deepmerge') const Config = require('webpack-chain') const PluginAPI = require('./PluginAPI') const loadEnv = require('./util/loadEnv') @@ -12,7 +13,7 @@ const { warn, error } = require('@vue/cli-shared-utils') const { defaults, validate } = require('./options') module.exports = class Service { - constructor (context, { plugins, projectOptions, useBuiltIn } = {}) { + constructor (context, { plugins, pkg, projectOptions, useBuiltIn } = {}) { process.VUE_CLI_SERVICE = this this.context = context this.webpackConfig = new Config() @@ -20,9 +21,9 @@ module.exports = class Service { this.webpackRawConfigFns = [] this.devServerConfigFns = [] this.commands = {} - this.pkg = this.resolvePkg() - this.projectOptions = Object.assign( - defaults, + this.pkg = this.resolvePkg(pkg) + this.projectOptions = deepMerge( + defaults(), this.loadProjectOptions(projectOptions) ) @@ -50,8 +51,10 @@ module.exports = class Service { } } - resolvePkg () { - if (fs.existsSync(path.join(this.context, 'package.json'))) { + resolvePkg (inlinePkg) { + if (inlinePkg) { + return inlinePkg + } else if (fs.existsSync(path.join(this.context, 'package.json'))) { return readPkg.sync(this.context) } else { return {} diff --git a/packages/@vue/cli-service/lib/config/base.js b/packages/@vue/cli-service/lib/config/base.js index 7af8d54d1..e9a6bc2c4 100644 --- a/packages/@vue/cli-service/lib/config/base.js +++ b/packages/@vue/cli-service/lib/config/base.js @@ -41,7 +41,7 @@ module.exports = (api, options) => { .test(/\.vue$/) .use('vue-loader') .loader('vue-loader') - .options(Object.assign({}, options.vueLoaderOptions)) + .options(Object.assign({}, options.vueLoader)) webpackConfig.module .rule('images') diff --git a/packages/@vue/cli-service/lib/config/css.js b/packages/@vue/cli-service/lib/config/css.js index 25fd9b140..edda10329 100644 --- a/packages/@vue/cli-service/lib/config/css.js +++ b/packages/@vue/cli-service/lib/config/css.js @@ -4,22 +4,23 @@ module.exports = (api, options) => { const ExtractTextPlugin = require('extract-text-webpack-plugin') const isProd = process.env.NODE_ENV === 'production' - const extract = isProd && options.extractCSS !== false - const resolver = new CSSLoaderResolver({ - sourceMap: !!options.cssSourceMap, - cssModules: !!options.cssModules, - minimize: isProd, - extract + const userOptions = options.css || {} + const extract = isProd && userOptions.extract !== false + const baseOptions = Object.assign({}, userOptions, { + extract, + minimize: isProd }) + const resolver = new CSSLoaderResolver(baseOptions) + // apply css loaders for vue-loader webpackConfig.module .rule('vue') .use('vue-loader') .tap(options => { - // ensure user injected vueLoaderOptions take higher priority + // ensure user injected vueLoader options take higher priority options.loaders = Object.assign(resolver.vue(), options.loaders) - options.cssSourceMap = !!options.cssSourceMap + options.cssSourceMap = !!userOptions.cssSourceMap return options }) @@ -46,12 +47,14 @@ module.exports = (api, options) => { } // handle cssModules for *.module.js - resolver.set('cssModules', true) + const cssModulesResolver = new CSSLoaderResolver(Object.assign({}, baseOptions, { + modules: true + })) - const cssModulesLangs = langs.map(lang => [lang, new RegExp(`\\.module\\.${lang}`)]) + const cssModulesLangs = langs.map(lang => [lang, new RegExp(`\\.module\\.${lang}$`)]) for (const cssModulesLang of cssModulesLangs) { const [lang, test] = cssModulesLang - const rule = resolver[lang](test) + const rule = cssModulesResolver[lang](test) const context = webpackConfig.module .rule(`${lang}-module`) .test(rule.test) diff --git a/packages/@vue/cli-service/lib/options.js b/packages/@vue/cli-service/lib/options.js index a17a0885a..009470457 100644 --- a/packages/@vue/cli-service/lib/options.js +++ b/packages/@vue/cli-service/lib/options.js @@ -4,13 +4,19 @@ const schema = createSchema(joi => joi.object({ baseUrl: joi.string(), outputDir: joi.string(), compiler: joi.boolean(), - cssModules: joi.boolean(), - vueLoaderOptions: joi.object(), productionSourceMap: joi.boolean(), - cssSourceMap: joi.boolean(), - extractCSS: joi.boolean(), + vueLoader: joi.object(), + css: joi.object({ + modules: joi.boolean(), + extract: joi.boolean(), + sourceMap: joi.boolean(), + loaderOptions: joi.object({ + sass: joi.object(), + less: joi.object(), + stylus: joi.object() + }) + }), devServer: joi.object(), - // known options from offical plugins lintOnSave: joi.boolean(), pwa: joi.object() @@ -23,7 +29,7 @@ exports.validate = options => validate( { allowUnknown: true } ) -exports.defaults = { +exports.defaults = () => ({ // project deployment base baseUrl: '/', @@ -33,20 +39,20 @@ exports.defaults = { // boolean, use full build? compiler: false, - // apply css modules to CSS files that doesn't end with .module.css? - cssModules: false, - // vue-loader options - vueLoaderOptions: {}, + vueLoader: {}, // sourceMap for production build? productionSourceMap: true, - // enable css source map? - cssSourceMap: false, - - // boolean | Object, extract css? - extractCSS: true, + css: { + // boolean | Object, extract css? + extract: true, + // apply css modules to CSS files that doesn't end with .mdoule.css? + modules: false, + sourceMap: false, + loaderOptions: {} + }, // whether to use eslint-loader lintOnSave: false, @@ -62,4 +68,4 @@ exports.defaults = { before: app => {} */ } -} +}) diff --git a/packages/@vue/cli-service/lib/webpack/CSSLoaderResolver.js b/packages/@vue/cli-service/lib/webpack/CSSLoaderResolver.js index 4544e35e1..2e29dd6cf 100644 --- a/packages/@vue/cli-service/lib/webpack/CSSLoaderResolver.js +++ b/packages/@vue/cli-service/lib/webpack/CSSLoaderResolver.js @@ -2,6 +2,8 @@ * https://github.com/egoist/webpack-handle-css-loader * The MIT License (MIT) * Copyright (c) EGOIST <0x142857@gmail.com> (github.com/egoist) + * + * Modified by Yuxi Evan You */ const ExtractTextPlugin = require('extract-text-webpack-plugin') @@ -9,58 +11,42 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin') module.exports = class CSSLoaderResolver { /** * @param {Object} options - * @param {string} [options.cssLoader='css-loader'] css-loader name or path. - * @param {Object|boolean} [options.postcss=undefined] Options for postcss-loader. * @param {boolean} [options.sourceMap=undefined] Enable sourcemaps. + * @param {boolean} [options.modules=undefined] Enable CSS modules. * @param {boolean} [options.extract=undefined] Extract CSS. * @param {boolean} [options.minimize=undefined] Minimize CSS. - * @param {boolean} [options.cssModules=undefined] Enable CSS modules. + * @param {Object} [options.loaderOptions={}] Options to pass on to loaders. */ constructor ({ - postcss, sourceMap, + modules, extract, minimize, - cssModules + loaderOptions } = {}) { + this.postcss = true // true by default, turned off if generating for vue-loader this.cssLoader = 'css-loader' this.fallbackLoader = 'vue-style-loader' - this.postcss = postcss this.sourceMap = sourceMap this.extract = extract this.minimize = minimize - this.cssModules = cssModules + this.modules = modules + this.loaderOptions = loaderOptions || {} } - /** - * Set value of instance option - * @param {string} key - * @param {any} value - */ - set (key, value) { - this[key] = value - } - - /** - * Get the rule for specific loader - * @param {RegExp} [test=undefined] File matcher - * @param {RegExp} [loader=undefined] Loader name or path to it - * @param {any} [options=undefined] Options for relevant loader - * @return {Object} {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ getLoader (test, loader, options = {}) { const cssLoaderOptions = { sourceMap: this.sourceMap, minimize: this.minimize } - if (this.cssModules) { + if (this.modules) { cssLoaderOptions.modules = true cssLoaderOptions.importLoaders = 1 cssLoaderOptions.localIdentName = '[name]_[local]__[hash:base64:5]' } - if (loader === 'css-loader') { + if (loader === 'css') { Object.assign(cssLoaderOptions, options) } @@ -69,7 +55,7 @@ module.exports = class CSSLoaderResolver { options: cssLoaderOptions }] - if (loader !== 'postcss-loader' && this.postcss !== false) { + if (loader !== 'postcss' && this.postcss !== false) { use.push({ loader: 'postcss-loader', options: { @@ -78,13 +64,12 @@ module.exports = class CSSLoaderResolver { }) } - if (loader && loader !== 'css-loader') { + if (loader && loader !== 'css') { use.push({ - loader, - options: { - ...options, + loader: loader + '-loader', + options: Object.assign({}, this.loaderOptions[loader] || {}, options, { sourceMap: this.sourceMap - } + }) }) } @@ -102,92 +87,43 @@ module.exports = class CSSLoaderResolver { } } - /** - * Get the rule for css files - * @param {RegExp} [test=/\.css$/] File matcher - * @param {any} [options=undefined] Options for css-loader - * @return {Object} {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ - css (test, options) { - test = test || /\.css$/ - return this.getLoader(test, 'css-loader', options) + css (test = /\.css$/) { + return this.getLoader(test, 'css') } - /** - * Get the rule for sass files - * @param {RegExp} [test=/\.sass$/] File matcher - * @param {any} [options=undefined] Options for sass-loader, `indentedSyntax` for sass-loader is `true` here - * @return {Object} {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ - sass (test, options = {}) { - test = test || /\.sass$/ - return this.getLoader(test, 'sass-loader', { - indentedSyntax: true, - ...options + sass (test = /\.sass$/) { + return this.getLoader(test, 'sass', { + indentedSyntax: true }) } - /** - * Get the rule for scss files - * @param {RegExp} [test=/\.scss$/] File matcher - * @param {any} [options=undefined] Options for sass-loader - * @return {Object} {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ - scss (test, options) { - test = test || /\.scss$/ - return this.getLoader(test, 'sass-loader', options) + scss (test = /\.scss$/) { + return this.getLoader(test, 'sass') } - /** - * Get the rule for less files - * @param {RegExp} [test=/\.less$/] File matcher - * @param {any} [options=undefined] Options for less-loader - * @return {Object} [Rule] {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ - less (test, options) { - test = test || /\.less$/ - return this.getLoader(test, 'less-loader', options) + less (test = /\.less$/) { + return this.getLoader(test, 'less') } - /** - * Get the rule for stylus files - * @param {RegExp} [test=/\.stylus$/] File matcher - * @param {any} [options=undefined] Options for stylus-loader - * @return {Object} {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ - stylus (test, options) { - test = test || /\.stylus$/ - return this.getLoader(test, 'stylus-loader', options) + styl (test = /\.styl$/) { + return this.getLoader(test, 'stylus') } - /** - * Get the rule for styl files - * @param {RegExp} [test=/\.styl$/] File matcher - * @param {any} [options=undefined] Options for stylus-loader - * @return {Object} {@link https://webpack.js.org/configuration/module/#rule webpack Rule} - */ - styl (test, options) { - test = test || /\.styl$/ - return this.getLoader(test, 'stylus-loader', options) + stylus (test = /\.stylus$/) { + return this.getLoader(test, 'stylus') } - /** - * Get the `loaders` options for vue-loader - * @param {any} [options={}] Options for relevant loaders - * @return {Object} - * @example - * handleLoader.vue({ - * scss: {}, - * less: {} - * }) - */ - vue (options = {}) { + vue () { + const originalPostcss = this.postcss + const originalModules = this.modules this.postcss = false - this.cssModules = false + this.modules = false const loaders = {} for (const lang of ['css', 'sass', 'scss', 'less', 'stylus', 'styl']) { - loaders[lang] = this[lang](null, options[lang]).use + loaders[lang] = this[lang]().use } + this.postcss = originalPostcss + this.modules = originalModules return loaders } } diff --git a/packages/@vue/cli-service/package.json b/packages/@vue/cli-service/package.json index 54958d772..521ec577a 100644 --- a/packages/@vue/cli-service/package.json +++ b/packages/@vue/cli-service/package.json @@ -30,6 +30,7 @@ "copy-webpack-plugin": "^4.3.1", "cross-spawn": "^5.1.0", "css-loader": "^0.28.8", + "deepmerge": "^2.0.1", "escape-string-regexp": "^1.0.5", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.6", diff --git a/yarn.lock b/yarn.lock index b2a6fc2f3..96472e185 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2979,6 +2979,10 @@ deepmerge@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" +deepmerge@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312" + default-require-extensions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"