diff --git a/docs/user-guide/configure.md b/docs/user-guide/configure.md index 256457b121..24e8de3447 100644 --- a/docs/user-guide/configure.md +++ b/docs/user-guide/configure.md @@ -418,3 +418,35 @@ If the globs are absolute paths, they are used as is. If they are relative, they The `ignoreFiles` property is stripped from extended configs: only the root-level config can ignore files. _Note that this is not an efficient method for ignoring lots of files._ If you want to ignore a lot of files efficiently, use [`.stylelintignore`](ignore-code.md) or adjust your files globs. + +## `overrides` + +You can provide configurations under the `overrides` key that will only apply to files that match specific glob patterns, using the same format you would pass on the command line (e.g., `app/**/*.test.css`). + +It is possible to override settings based on file glob patterns in your configuration by using the `overrides` key. An example of using the `overrides` key is as follows: + +In your `.stylelintrc.json`: + +```json +{ + "rules": { + "string-quotes": "double" + }, + + "overrides": [ + { + "files": ["components/**/*.css", "pages/**/*.css"], + "rules": { + "string-quotes": "single" + } + } + ] +} +``` + +Here is how overrides work in a configuration file: + +- The patterns are applied against the file path relative to the directory of the config file. For example, if your config file has the path `/Users/person/workspace/any-project/.stylelintrc.js` and the file you want to lint has the path `/Users/person/workspace/any-project/components/card.css`, then the pattern provided in `.stylelintrc.js` will be executed against the relative path `components/card.css`. +- Glob pattern overrides have higher precedence than the regular configuration in the same config file. Multiple overrides within the same config are applied in order. That is, the last override block in a config file always has the highest precedence. +- A glob specific configuration works almost the same as any other stylelint config. Override blocks can contain any configuration options that are valid in a regular config. +- Multiple glob patterns can be provided within a single override block. A file must match at least one of the supplied patterns for the configuration to apply. diff --git a/lib/__tests__/applyOverrides.test.js b/lib/__tests__/applyOverrides.test.js new file mode 100644 index 0000000000..c44b3c7a2a --- /dev/null +++ b/lib/__tests__/applyOverrides.test.js @@ -0,0 +1,303 @@ +'use strict'; + +const path = require('path'); +const { applyOverrides } = require('../augmentConfig'); + +test('no overrides', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.css')); + + expect(applied).toEqual(config); +}); + +describe('single matching override', () => { + test('simple override', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.module.css'], + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + const expectedConfig = { + rules: { + 'block-no-empty': true, + 'color-no-hex': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); + + test('simple override, files is a string', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: '*.module.css', + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + const expectedConfig = { + rules: { + 'block-no-empty': true, + 'color-no-hex': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); + + test('with plugins', () => { + const config = { + plugins: ['stylelint-plugin'], + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.module.css'], + plugins: ['stylelint-plugin2'], + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + const expectedConfig = { + plugins: ['stylelint-plugin', 'stylelint-plugin2'], + rules: { + 'block-no-empty': true, + 'color-no-hex': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); +}); + +describe('two matching overrides', () => { + test('simple override', () => { + const config = { + rules: { + 'block-no-empty': true, + 'unit-disallowed-list': ['px'], + }, + overrides: [ + { + files: ['*.module.css'], + rules: { + 'color-no-hex': true, + }, + }, + { + files: ['*.css'], + rules: { + 'block-no-empty': null, + }, + }, + ], + }; + + const expectedConfig = { + rules: { + 'block-no-empty': null, + 'unit-disallowed-list': ['px'], + 'color-no-hex': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); + + test('with plugins', () => { + const config = { + plugins: ['stylelint-plugin'], + rules: { + 'block-no-empty': true, + 'unit-disallowed-list': ['px'], + }, + overrides: [ + { + files: ['*.module.css'], + plugins: ['stylelint-plugin2'], + rules: { + 'color-no-hex': true, + }, + }, + { + files: ['*.css'], + plugins: ['stylelint-plugin3'], + rules: { + 'block-no-empty': null, + }, + }, + ], + }; + + const expectedConfig = { + plugins: ['stylelint-plugin', 'stylelint-plugin2', 'stylelint-plugin3'], + rules: { + 'block-no-empty': null, + 'unit-disallowed-list': ['px'], + 'color-no-hex': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); +}); + +describe('no matching overrides', () => { + test('simple override', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.no-module.css'], + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + const expectedConfig = { + rules: { + 'block-no-empty': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); + + test('with plugins', () => { + const config = { + plugins: ['stylelint-plugin'], + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.no-module.css'], + plugins: ['stylelint-plugin2'], + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + const expectedConfig = { + plugins: ['stylelint-plugin'], + rules: { + 'block-no-empty': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + + expect(applied).toEqual(expectedConfig); + }); +}); + +test('overrides is not an array', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + overrides: { + files: ['*.module.css'], + rules: { + 'color-no-hex': true, + }, + }, + }; + + expect(() => { + applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + }).toThrowErrorMatchingInlineSnapshot( + `"The \`overrides\` configuration property should be an array, e.g. { \\"overrides\\": [{ \\"files\\": \\"*.css\\", \\"rules\\": {} }] }."`, + ); +}); + +test('`files` is missing', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + expect(() => { + applyOverrides(config, __dirname, path.join(__dirname, 'style.module.css')); + }).toThrowErrorMatchingInlineSnapshot( + `"Every object in the \`overrides\` configuration property should have a \`files\` property with globs, e.g. { \\"overrides\\": [{ \\"files\\": \\"*.css\\", \\"rules\\": {} }] }."`, + ); +}); + +test('if glob is absolute path', () => { + const config = { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: [path.join(__dirname, 'style.css')], + rules: { + 'color-no-hex': true, + }, + }, + ], + }; + + const expectedConfig = { + rules: { + 'block-no-empty': true, + 'color-no-hex': true, + }, + }; + + const applied = applyOverrides(config, __dirname, path.join(__dirname, 'style.css')); + + expect(applied).toEqual(expectedConfig); +}); diff --git a/lib/__tests__/fixtures/config-overrides/extending-plugin-and-one-rule.json b/lib/__tests__/fixtures/config-overrides/extending-plugin-and-one-rule.json new file mode 100644 index 0000000000..62f6655400 --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/extending-plugin-and-one-rule.json @@ -0,0 +1,15 @@ +{ + "extends": "./plugin-and-one-rule.json", + "plugins": ["../plugin-warn-about-bar"], + "rules": { + "plugin/warn-about-bar": "always" + }, + "overrides": [ + { + "files": ["*.css"], + "rules": { + "block-no-empty": true + } + } + ] +} diff --git a/lib/__tests__/fixtures/config-overrides/extending-simple-rule.json b/lib/__tests__/fixtures/config-overrides/extending-simple-rule.json new file mode 100644 index 0000000000..efebbe6095 --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/extending-simple-rule.json @@ -0,0 +1,14 @@ +{ + "extends": "./simple-rule", + "rules": { + "block-no-empty": true + }, + "overrides": [ + { + "files": ["*.css"], + "rules": { + "color-named": "never" + } + } + ] +} diff --git a/lib/__tests__/fixtures/config-overrides/plugin-and-one-rule.json b/lib/__tests__/fixtures/config-overrides/plugin-and-one-rule.json new file mode 100644 index 0000000000..226adcde19 --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/plugin-and-one-rule.json @@ -0,0 +1,14 @@ +{ + "plugins": ["../plugin-warn-about-foo"], + "rules": { + "plugin/warn-about-foo": "always" + }, + "overrides": [ + { + "files": ["*.css"], + "rules": { + "color-named": "never" + } + } + ] +} diff --git a/lib/__tests__/fixtures/config-overrides/simple-rule.json b/lib/__tests__/fixtures/config-overrides/simple-rule.json new file mode 100644 index 0000000000..f218b30e12 --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/simple-rule.json @@ -0,0 +1,5 @@ +{ + "rules": { + "color-named": "always-where-possible" + } +} diff --git a/lib/__tests__/fixtures/config-overrides/style.css b/lib/__tests__/fixtures/config-overrides/style.css new file mode 100644 index 0000000000..57d81184ef --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/style.css @@ -0,0 +1,5 @@ +.foo { + color: pink; +} + +.bar {} diff --git a/lib/__tests__/fixtures/config-overrides/style.module.css b/lib/__tests__/fixtures/config-overrides/style.module.css new file mode 100644 index 0000000000..b68aeb0890 --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/style.module.css @@ -0,0 +1,5 @@ +.bar {} + +.baz { + background: orange; +} diff --git a/lib/__tests__/fixtures/config-overrides/testPrintConfig/.stylelintrc b/lib/__tests__/fixtures/config-overrides/testPrintConfig/.stylelintrc new file mode 100644 index 0000000000..6686d98924 --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/testPrintConfig/.stylelintrc @@ -0,0 +1,3 @@ +{ + "extends": "../extending-simple-rule.json" +} diff --git a/lib/__tests__/fixtures/config-overrides/testPrintConfig/style.css b/lib/__tests__/fixtures/config-overrides/testPrintConfig/style.css new file mode 100644 index 0000000000..57d81184ef --- /dev/null +++ b/lib/__tests__/fixtures/config-overrides/testPrintConfig/style.css @@ -0,0 +1,5 @@ +.foo { + color: pink; +} + +.bar {} diff --git a/lib/__tests__/overrides.test.js b/lib/__tests__/overrides.test.js new file mode 100644 index 0000000000..7057556370 --- /dev/null +++ b/lib/__tests__/overrides.test.js @@ -0,0 +1,196 @@ +'use strict'; + +const path = require('path'); +const standalone = require('../standalone'); + +const fixturesPath = path.join(__dirname, 'fixtures', 'config-overrides'); + +describe('single input file. all overrides are matching', () => { + it('simple override', async () => { + const linted = await standalone({ + files: [path.join(fixturesPath, 'style.css')], + config: { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.css'], + rules: { + 'color-named': 'never', + }, + }, + ], + }, + configBasedir: fixturesPath, + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings[0].rule).toBe('block-no-empty'); + expect(linted.results[0].warnings[1].rule).toBe('color-named'); + }); + + it('override with plugins', async () => { + const linted = await standalone({ + files: [path.join(fixturesPath, 'style.css')], + config: { + plugins: ['../plugin-warn-about-foo'], + rules: { + 'plugin/warn-about-foo': 'always', + }, + overrides: [ + { + files: ['*.css'], + plugins: ['../plugin-warn-about-bar'], + rules: { + 'plugin/warn-about-bar': 'always', + }, + }, + ], + }, + configBasedir: fixturesPath, + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings[0].rule).toBe('plugin/warn-about-foo'); + expect(linted.results[0].warnings[1].rule).toBe('plugin/warn-about-bar'); + }); + + it('extend with simple overrides', async () => { + const linted = await standalone({ + files: [path.join(fixturesPath, 'style.css')], + configFile: path.join(fixturesPath, 'extending-simple-rule.json'), + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings[0].rule).toBe('block-no-empty'); + expect(linted.results[0].warnings[1].rule).toBe('color-named'); + }); + + it('extended and base configs have overrides and plugins', async () => { + const linted = await standalone({ + files: [path.join(fixturesPath, 'style.css')], + configFile: path.join(fixturesPath, 'extending-plugin-and-one-rule.json'), + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(4); + expect(linted.results[0].warnings[0].rule).toBe('plugin/warn-about-bar'); + expect(linted.results[0].warnings[1].rule).toBe('plugin/warn-about-foo'); + expect(linted.results[0].warnings[2].rule).toBe('block-no-empty'); + expect(linted.results[0].warnings[3].rule).toBe('color-named'); + }); +}); + +it('override is not matching', async () => { + const linted = await standalone({ + files: [path.join(fixturesPath, 'style.css')], + config: { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.module.css'], + rules: { + 'color-named': 'never', + }, + }, + ], + }, + configBasedir: fixturesPath, + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(1); + expect(linted.results[0].warnings[0].rule).toBe('block-no-empty'); +}); + +it('code with a filename. override is matching', async () => { + const linted = await standalone({ + code: '.foo { color: pink; } .bar {}', + codeFilename: path.join(fixturesPath, 'test.css'), + config: { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.css'], + rules: { + 'color-named': 'never', + }, + }, + ], + }, + configBasedir: fixturesPath, + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings[0].rule).toBe('block-no-empty'); + expect(linted.results[0].warnings[1].rule).toBe('color-named'); +}); + +it('code without a filename. overrides is not matching', async () => { + const linted = await standalone({ + code: '.foo { color: pink; } .bar {}', + + config: { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['*.css'], + rules: { + 'color-named': 'never', + }, + }, + ], + }, + configBasedir: fixturesPath, + }); + + expect(linted.results).toHaveLength(1); + expect(linted.results[0].warnings).toHaveLength(1); + expect(linted.results[0].warnings[0].rule).toBe('block-no-empty'); +}); + +it('two files', async () => { + const linted = await standalone({ + files: [path.join(fixturesPath, 'style.css'), path.join(fixturesPath, 'style.module.css')], + config: { + rules: { + 'block-no-empty': true, + }, + overrides: [ + { + files: ['style.css'], + rules: { + 'color-named': 'never', + }, + }, + { + files: ['*.module.css'], + rules: { + 'block-no-empty': null, + 'property-disallowed-list': ['background'], + }, + }, + ], + }, + configBasedir: fixturesPath, + }); + + expect(linted.results).toHaveLength(2); + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings[0].rule).toBe('block-no-empty'); + expect(linted.results[0].warnings[1].rule).toBe('color-named'); + + expect(linted.results[1].warnings).toHaveLength(1); + expect(linted.results[1].warnings[0].rule).toBe('property-disallowed-list'); +}); diff --git a/lib/__tests__/printConfig.test.js b/lib/__tests__/printConfig.test.js index e73b30ec29..f4012473d4 100644 --- a/lib/__tests__/printConfig.test.js +++ b/lib/__tests__/printConfig.test.js @@ -26,6 +26,23 @@ it('printConfig uses getConfigForFile to retrieve the config', () => { }); }); +it('config overrides should apply', () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/config-overrides/testPrintConfig/style.css'), + ); + + return printConfig({ + files: [filepath], + }).then((result) => { + expect(result).toEqual({ + rules: { + 'block-no-empty': [true], + 'color-named': ['never'], + }, + }); + }); +}); + it('printConfig with input css should throw', () => { return expect( printConfig({ diff --git a/lib/augmentConfig.js b/lib/augmentConfig.js index 273fc744da..3e2a35795e 100644 --- a/lib/augmentConfig.js +++ b/lib/augmentConfig.js @@ -4,13 +4,16 @@ const configurationError = require('./utils/configurationError'); const getModulePath = require('./utils/getModulePath'); const globjoin = require('globjoin'); const merge = require('deepmerge'); +const micromatch = require('micromatch'); const normalizeAllRuleSettings = require('./normalizeAllRuleSettings'); const path = require('path'); +const slash = require('slash'); /** @typedef {import('stylelint').StylelintConfigPlugins} StylelintConfigPlugins */ /** @typedef {import('stylelint').StylelintConfigProcessor} StylelintConfigProcessor */ /** @typedef {import('stylelint').StylelintConfigProcessors} StylelintConfigProcessors */ /** @typedef {import('stylelint').StylelintConfigRules} StylelintConfigRules */ +/** @typedef {import('stylelint').StylelintConfigOverride} StylelintConfigOverride */ /** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */ /** @typedef {import('stylelint').StylelintConfig} StylelintConfig */ /** @typedef {import('stylelint').CosmiconfigResult} CosmiconfigResult */ @@ -37,9 +40,10 @@ const MERGE_OPTIONS = { * @param {StylelintConfig} config * @param {string} configDir * @param {boolean} [allowOverrides] + * @param {string} [filePath] * @returns {Promise} */ -function augmentConfigBasic(stylelint, config, configDir, allowOverrides) { +function augmentConfigBasic(stylelint, config, configDir, allowOverrides, filePath) { return Promise.resolve() .then(() => { if (!allowOverrides) return config; @@ -49,6 +53,13 @@ function augmentConfigBasic(stylelint, config, configDir, allowOverrides) { .then((augmentedConfig) => { return extendConfig(stylelint, augmentedConfig, configDir); }) + .then((augmentedConfig) => { + if (filePath) { + return applyOverrides(augmentedConfig, configDir, filePath); + } + + return augmentedConfig; + }) .then((augmentedConfig) => { return absolutizePaths(augmentedConfig, configDir); }); @@ -78,10 +89,11 @@ function augmentConfigExtended(stylelint, cosmiconfigResult) { /** * @param {StylelintInternalApi} stylelint + * @param {string} [filePath] * @param {CosmiconfigResult} [cosmiconfigResult] * @returns {Promise} */ -function augmentConfigFull(stylelint, cosmiconfigResult) { +function augmentConfigFull(stylelint, filePath, cosmiconfigResult) { if (!cosmiconfigResult) return Promise.resolve(null); const config = cosmiconfigResult.config; @@ -89,7 +101,7 @@ function augmentConfigFull(stylelint, cosmiconfigResult) { const configDir = stylelint._options.configBasedir || path.dirname(filepath || ''); - return augmentConfigBasic(stylelint, config, configDir, true) + return augmentConfigBasic(stylelint, config, configDir, true, filePath) .then((augmentedConfig) => { return addPluginFunctions(augmentedConfig); }) @@ -248,13 +260,35 @@ function mergeConfigs(a, b) { } } + /** @type {{overrides: StylelintConfigOverride[]}} */ + const overridesMerger = {}; + + if (a.overrides || b.overrides) { + overridesMerger.overrides = []; + + if (a.overrides) { + overridesMerger.overrides = overridesMerger.overrides.concat(a.overrides); + } + + if (b.overrides) { + overridesMerger.overrides = [...new Set(overridesMerger.overrides.concat(b.overrides))]; + } + } + const rulesMerger = {}; if (a.rules || b.rules) { rulesMerger.rules = { ...a.rules, ...b.rules }; } - const result = { ...a, ...b, ...processorMerger, ...pluginMerger, ...rulesMerger }; + const result = { + ...a, + ...b, + ...processorMerger, + ...pluginMerger, + ...overridesMerger, + ...rulesMerger, + }; return result; } @@ -370,4 +404,52 @@ function addProcessorFunctions(config) { return config; } -module.exports = { augmentConfigExtended, augmentConfigFull }; +/** + * @param {StylelintConfig} fullConfig + * @param {string} configDir + * @param {string} filePath + * @return {StylelintConfig} + */ +function applyOverrides(fullConfig, configDir, filePath) { + let { overrides, ...config } = fullConfig; + + if (!overrides) { + return config; + } + + if (!Array.isArray(overrides)) { + throw Error( + 'The `overrides` configuration property should be an array, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.', + ); + } + + for (const override of overrides) { + const { files, ...configOverrides } = override; + + if (!files) { + throw Error( + 'Every object in the `overrides` configuration property should have a `files` property with globs, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.', + ); + } + + const filesGlobs = [files] + .flat() + .map((glob) => { + if (path.isAbsolute(glob.replace(/^!/, ''))) { + return glob; + } + + return globjoin(configDir, glob); + }) + // Glob patterns for micromatch should be in POSIX-style + .map(slash); + + if (micromatch.isMatch(filePath, filesGlobs)) { + config = mergeConfigs(config, configOverrides); + } + } + + return config; +} + +module.exports = { augmentConfigExtended, augmentConfigFull, applyOverrides }; diff --git a/lib/createStylelint.js b/lib/createStylelint.js index da236f1e89..bb07506f3e 100644 --- a/lib/createStylelint.js +++ b/lib/createStylelint.js @@ -21,7 +21,7 @@ const STOP_DIR = IS_TEST ? path.resolve(__dirname, '..') : undefined; * @param {import('stylelint').StylelintStandaloneOptions} options * @returns {StylelintInternalApi} */ -module.exports = function (options = {}) { +module.exports = function createStylelint(options = {}) { /** @type {Partial} */ const stylelint = { _options: options }; @@ -43,16 +43,6 @@ module.exports = function (options = {}) { options.configOverrides.reportDescriptionlessDisables = options.reportDescriptionlessDisables; } - // Two separate explorers so they can each have their own transform - // function whose results are cached by cosmiconfig - stylelint._fullExplorer = cosmiconfig('stylelint', { - // @ts-ignore TODO TYPES found out which cosmiconfig types are valid - transform: augmentConfig.augmentConfigFull.bind( - null, - /** @type{StylelintInternalApi} */ (stylelint), - ), - stopDir: STOP_DIR, - }); // @ts-ignore TODO TYPES found out which cosmiconfig types are valid stylelint._extendExplorer = cosmiconfig(null, { transform: augmentConfig.augmentConfigExtended.bind( diff --git a/lib/createStylelintResult.js b/lib/createStylelintResult.js index 0f40c045d2..fb899282ce 100644 --- a/lib/createStylelintResult.js +++ b/lib/createStylelintResult.js @@ -12,10 +12,15 @@ const createPartialStylelintResult = require('./createPartialStylelintResult'); * @param {import('stylelint').StylelintCssSyntaxError} [cssSyntaxError] * @return {Promise} */ -module.exports = function (stylelint, postcssResult, filePath, cssSyntaxError) { +module.exports = function createStylelintResult( + stylelint, + postcssResult, + filePath, + cssSyntaxError, +) { let stylelintResult = createPartialStylelintResult(postcssResult, cssSyntaxError); - return stylelint.getConfigForFile(filePath).then((configForFile) => { + return stylelint.getConfigForFile(filePath, filePath).then((configForFile) => { // TODO TYPES handle possible null here const config = /** @type {{ config: import('stylelint').StylelintConfig, filepath: string }} */ ( diff --git a/lib/getConfigForFile.js b/lib/getConfigForFile.js index 2a5f0d179d..3827c95610 100644 --- a/lib/getConfigForFile.js +++ b/lib/getConfigForFile.js @@ -1,19 +1,25 @@ 'use strict'; -const augmentConfigFull = require('./augmentConfig').augmentConfigFull; const configurationError = require('./utils/configurationError'); const path = require('path'); +const { augmentConfigFull } = require('./augmentConfig'); +const { cosmiconfig } = require('cosmiconfig'); +const IS_TEST = process.env.NODE_ENV === 'test'; +const STOP_DIR = IS_TEST ? path.resolve(__dirname, '..') : undefined; + +/** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */ /** @typedef {import('stylelint').StylelintConfig} StylelintConfig */ /** @typedef {import('stylelint').CosmiconfigResult} CosmiconfigResult */ /** @typedef {Promise} ConfigPromise */ /** - * @param {import('stylelint').StylelintInternalApi} stylelint + * @param {StylelintInternalApi} stylelint * @param {string} [searchPath] + * @param {string} [filePath] * @returns {ConfigPromise} */ -module.exports = function (stylelint, searchPath = process.cwd()) { +module.exports = function getConfigForFile(stylelint, searchPath = process.cwd(), filePath) { const optionsConfig = stylelint._options.config; if (optionsConfig !== undefined) { @@ -21,12 +27,13 @@ module.exports = function (stylelint, searchPath = process.cwd()) { stylelint._specifiedConfigCache.get(optionsConfig) ); - if (cached) return cached; + // If config has overrides the resulting config might be different for some files. + // Cache results only if resulted config is the same for all linted files. + if (cached && !optionsConfig.overrides) { + return cached; + } - // stylelint._fullExplorer (cosmiconfig) is already configured to - // run augmentConfigFull; but since we're making up the result here, - // we need to manually run the transform - const augmentedResult = augmentConfigFull(stylelint, { + const augmentedResult = augmentConfigFull(stylelint, filePath, { config: optionsConfig, // Add the extra path part so that we can get the directory without being // confused @@ -38,15 +45,22 @@ module.exports = function (stylelint, searchPath = process.cwd()) { return augmentedResult; } + const configExplorer = cosmiconfig('stylelint', { + // @ts-ignore TODO TYPES found out which cosmiconfig types are valid + // transform: augmentConfigFull.bind(null, stylelint, filePath), + transform: (cosmiconfigResult) => augmentConfigFull(stylelint, filePath, cosmiconfigResult), + stopDir: STOP_DIR, + }); + const searchForConfig = stylelint._options.configFile - ? stylelint._fullExplorer.load(stylelint._options.configFile) - : stylelint._fullExplorer.search(searchPath); + ? configExplorer.load(stylelint._options.configFile) + : configExplorer.search(searchPath); return /** @type {ConfigPromise} */ ( searchForConfig .then((config) => { // If no config was found, try looking from process.cwd - if (!config) return stylelint._fullExplorer.search(process.cwd()); + if (!config) return configExplorer.search(process.cwd()); return config; }) diff --git a/lib/isPathIgnored.js b/lib/isPathIgnored.js index 62343ec848..4a2b6ed693 100644 --- a/lib/isPathIgnored.js +++ b/lib/isPathIgnored.js @@ -14,7 +14,7 @@ const slash = require('slash'); * @param {string} [filePath] * @return {Promise} */ -module.exports = function (stylelint, filePath) { +module.exports = function isPathIgnored(stylelint, filePath) { if (!filePath) { return Promise.resolve(false); } @@ -22,7 +22,7 @@ module.exports = function (stylelint, filePath) { const cwd = process.cwd(); const ignorer = getFileIgnorer(stylelint._options); - return stylelint.getConfigForFile(filePath).then((result) => { + return stylelint.getConfigForFile(filePath, filePath).then((result) => { if (!result) { return true; } diff --git a/lib/lintSource.js b/lib/lintSource.js index a7bdd42ab5..7a1a26dd3e 100644 --- a/lib/lintSource.js +++ b/lib/lintSource.js @@ -53,7 +53,7 @@ module.exports = function lintSource(stylelint, options = {}) { const configSearchPath = stylelint._options.configFile || inputFilePath; - const getConfig = stylelint.getConfigForFile(configSearchPath).catch((err) => { + const getConfig = stylelint.getConfigForFile(configSearchPath, inputFilePath).catch((err) => { if (isCodeNotFile && isPathNotFoundError(err)) return stylelint.getConfigForFile(process.cwd()); diff --git a/lib/printConfig.js b/lib/printConfig.js index ca42821cd5..1b05b8af6d 100644 --- a/lib/printConfig.js +++ b/lib/printConfig.js @@ -10,7 +10,7 @@ const path = require('path'); * @param {import('stylelint').StylelintStandaloneOptions} options * @returns {Promise} */ -module.exports = function (options) { +module.exports = function printConfig(options) { const code = options.code; const config = options.config; const configBasedir = options.configBasedir; @@ -47,7 +47,7 @@ module.exports = function (options) { const configSearchPath = stylelint._options.configFile || absoluteFilePath; - return stylelint.getConfigForFile(configSearchPath).then((result) => { + return stylelint.getConfigForFile(configSearchPath, absoluteFilePath).then((result) => { if (result === null) { return result; } diff --git a/types/stylelint/index.d.ts b/types/stylelint/index.d.ts index 87fc3aaac2..eb5c14e37f 100644 --- a/types/stylelint/index.d.ts +++ b/types/stylelint/index.d.ts @@ -18,6 +18,17 @@ declare module 'stylelint' { export type StylelintConfigRules = { [ruleName: string]: StylelintConfigRuleSettings; }; + export type StylelintConfigOverride = Pick< + StylelintConfig, + | 'plugins' + | 'pluginFunctions' + | 'processors' + | 'processorFunctions' + | 'rules' + | 'defaultSeverity' + > & { + files: string | string[]; + }; export type DisableOptions = { except?: Array; @@ -44,6 +55,7 @@ declare module 'stylelint' { reportNeedlessDisables?: DisableSettings; reportInvalidScopeDisables?: DisableSettings; reportDescriptionlessDisables?: DisableSettings; + overrides?: StylelintConfigOverride[]; }; // A meta-type that returns a union over all properties of `T` whose values @@ -173,10 +185,6 @@ declare module 'stylelint' { search: (s: string) => Promise; load: (s: string) => Promise; }; - _fullExplorer: { - search: (s: string) => Promise; - load: (s: string) => Promise; - }; _configCache: Map; _specifiedConfigCache: Map; _postcssResultCache: Map; @@ -186,7 +194,10 @@ declare module 'stylelint' { _createStylelintResult: Function; _createEmptyPostcssResult?: Function; - getConfigForFile: (s?: string) => Promise<{ config: StylelintConfig; filepath: string } | null>; + getConfigForFile: ( + searchPath?: string, + filePath?: string, + ) => Promise<{ config: StylelintConfig; filepath: string } | null>; isPathIgnored: (s?: string) => Promise; lintSource: Function; };