refactor + tests for CSS resolver

This commit is contained in:
Evan You
2018-01-14 00:31:20 -05:00
parent 4765cc6fa1
commit 29d01f54c4
11 changed files with 249 additions and 140 deletions

View File

@@ -1,3 +0,0 @@
module.exports = {
lintOnSave: false
}

View File

@@ -1 +0,0 @@
module.exports = api => {}

View File

@@ -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')

View File

@@ -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 })
})

View File

@@ -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 {}

View File

@@ -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')

View File

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

View File

@@ -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 => {}
*/
}
}
})

View File

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

View File

@@ -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",

View File

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