stylelint
Version:
Modern CSS linter
195 lines (178 loc) • 6.67 kB
JavaScript
import path from "path"
import fs from "fs"
import cosmiconfig from "cosmiconfig"
import resolveFrom from "resolve-from"
import {
assign,
merge,
omit,
} from "lodash"
import { configurationError } from "./utils"
const IGNORE_FILENAME = ".stylelintignore"
const FILE_NOT_FOUND_ERROR_CODE = "ENOENT"
/**
* - Accept a raw config or look up `.stylelintrc` (using cosmiconfig).
* - Add patterns from `.stylelintignore` to the config's `ignoreFiles`.
* - Resolve plugin names to absolute paths.
* - Resolve extends by finding, augmenting, and merging
* extended configs
*
* @param {object} options - May either be an options object with a `config` property,
* or just the config object itself. All the `options` properties documented below
* are for the options object, not a config.
* @param {object} [options.config]
* @param {object} [options.configFile] - Specify a configuration file (path) instead
* @param {object} [options.configBasedir] - Specify a base directory that things should be
* considered relative to.
* @param {object} [options.configOverrides] - An object to merge on top of the
* final derived configuration object
* @param {object} [options.ignorePath] - Specify a file of ignore patterns.
* The path can be absolute or relative to `process.cwd()`.
* Defaults to `${configDir}/.stylelintignore`.
* @return {object} Object with `config` and `configDir` properties.
*/
export default function (options) {
const rawConfig = (() => {
if (options.config) return options.config
if (options.rules) return options
return null
})()
const configBasedir = options.configBasedir || process.cwd()
if (rawConfig) {
return augmentConfig(rawConfig, configBasedir, {
addIgnorePatterns: true,
ignorePath: options.ignorePath,
}).then(augmentedConfig => {
return {
config: merge(augmentedConfig, options.configOverrides),
configDir: configBasedir,
}
})
}
const cosmiconfigOptions = {
// Turn off argv option to avoid hijacking the all-too-common
// `--config` argument when stylelint is used in conjunction with other CLI's
// (e.g. webpack)
argv: false,
// Allow extensions on rc filenames
rcExtensions: true,
}
if (options.configFile) {
cosmiconfigOptions.configPath = path.resolve(process.cwd(), options.configFile)
}
let rootConfigDir
return cosmiconfig("stylelint", cosmiconfigOptions).then(result => {
if (!result) throw configurationError("No configuration found")
rootConfigDir = path.dirname(result.filepath)
return augmentConfig(result.config, rootConfigDir, {
addIgnorePatterns: true,
ignorePath: options.ignorePath,
})
}).then(augmentedConfig => {
const finalConfig = (options.configOverrides)
? merge({}, augmentedConfig, options.configOverrides)
: augmentedConfig
return {
config: finalConfig,
configDir: rootConfigDir,
}
})
}
/**
* Given a configuration object, return a new augmented version with
* - Plugins resolved to absolute paths
* - Extended configs merged in
*
* @param {object} config
* @param {string} configDir
* @param {object} [options]
* @param {boolean} [options.addIgnorePatterns=false] - Look for `.stylelintignore` and
* add its patterns to `config.ignoreFiles`.
* @param {string} [options.ignorePath] - See above.
* @return {object}
*/
export function augmentConfig(config, configDir, options = {}) {
const start = (options.addIgnorePatterns)
? addIgnoreFiles(config, configDir, options.ignorePath)
: Promise.resolve(config)
return start.then(configWithIgnorePatterns => {
const absolutizedConfig = absolutizePlugins(configWithIgnorePatterns, configDir)
if (absolutizedConfig.extends) {
return extendConfig(absolutizedConfig, configDir)
}
return Promise.resolve(absolutizedConfig)
})
}
export function addIgnoreFiles(config, configDir, ignorePath) {
return findIgnorePatterns(configDir, ignorePath).then(ignorePatterns => {
config.ignoreFiles = [].concat(ignorePatterns, config.ignoreFiles || [])
return config
})
}
function findIgnorePatterns(configDir, ignorePath) {
let defaultedIgnorePath = path.resolve(configDir, IGNORE_FILENAME)
if (ignorePath) {
defaultedIgnorePath = (path.isAbsolute(ignorePath))
? ignorePath
: path.resolve(process.cwd(), ignorePath)
}
return new Promise((resolve, reject) => {
fs.readFile(defaultedIgnorePath, "utf8", (err, data) => {
if (err) {
// If the file's not found, great, we'll just give an empty array
if (err.code === FILE_NOT_FOUND_ERROR_CODE) { return resolve([]) }
return reject(err)
}
const ignorePatterns = data.split(/\r?\n/g)
.filter(val => val.trim() !== "") // Remove empty lines
.filter(val => val.trim()[0] !== "#") // Remove comments
resolve(ignorePatterns)
})
})
}
// Replace all plugin lookups with absolute paths
function absolutizePlugins(config, configDir) {
if (!config.plugins) { return config }
return assign({}, config, {
plugins: config.plugins.map(lookup => getModulePath(configDir, lookup)),
})
}
function extendConfig(config, configDir) {
const extendLookups = [].concat(config.extends)
const origConfig = omit(config, "extends")
const resultPromise = extendLookups.reduce((mergeConfigs, extendLookup) => {
return mergeConfigs.then(mergedConfig => {
return loadExtendedConfig(mergedConfig, configDir, extendLookup).then(extendedConfig => {
return merge({}, mergedConfig, extendedConfig)
})
})
}, Promise.resolve(origConfig))
return resultPromise.then(mergedConfig => {
return merge({}, mergedConfig, origConfig)
})
}
function loadExtendedConfig(config, configDir, extendLookup) {
const extendPath = getModulePath(configDir, extendLookup)
const extendDir = path.dirname(extendPath)
return cosmiconfig(null, {
configPath: extendPath,
// In case `--config` was used: do not pay attention to it again
argv: false,
}).then(result => {
// Make sure to also augment the config that we're merging in
// ... but the `ignoreFiles` option only works with the
// config that is being directly invoked, not any
// extended configs
return augmentConfig(stripIgnoreFiles(result.config), extendDir)
})
}
function getModulePath(basedir, lookup) {
const path = resolveFrom(basedir, lookup)
if (path) return path
throw configurationError(
`Could not find "${lookup}". Do you need a \`configBasedir\`?`
)
}
function stripIgnoreFiles(config) {
return omit(config, "ignoreFiles")
}