diff --git a/docs/user-guide/usage/node-api.md b/docs/user-guide/usage/node-api.md index d992885f13..331ba08a6c 100644 --- a/docs/user-guide/usage/node-api.md +++ b/docs/user-guide/usage/node-api.md @@ -171,3 +171,34 @@ stylelint ``` Note that the customSyntax option also accepts a string. [Refer to the options documentation for details](./options.md#customsyntax). + +## Resolving the effective config for a file + +If you want to find out what exact configuration will be used for a file without actually linting it, you can use the `resolveConfig()` function. Given a file path, it will return a Promise that resolves with the effective configuration object: + +```js +const config = await stylelint.resolveConfig(filePath); + +// config => { +// rules: { +// 'color-no-invalid-hex': true +// }, +// extends: [ +// 'stylelint-config-standard', +// 'stylelint-config-css-modules' +// ], +// plugins: [ +// 'stylelint-scss' +// ], +// … +// } +``` + +If a configuration cannot be found for a file, `resolveConfig()` will return a Promise that resolves to `undefined`. + +You can also pass the following subset of the [options that you would normally pass to `lint()`](#options): + +- `cwd` +- `config` +- `configBasedir` +- `customSyntax` diff --git a/lib/__tests__/resolveConfig.test.js b/lib/__tests__/resolveConfig.test.js new file mode 100644 index 0000000000..f907b73ca2 --- /dev/null +++ b/lib/__tests__/resolveConfig.test.js @@ -0,0 +1,129 @@ +'use strict'; + +const path = require('path'); +const pluginWarnAboutFoo = require('./fixtures/plugin-warn-about-foo'); +const replaceBackslashes = require('../testUtils/replaceBackslashes'); +const stylelint = require('..'); + +describe('resolveConfig', () => { + it('should resolve to undefined without a path', () => { + return expect(stylelint.resolveConfig()).resolves.toBeUndefined(); + }); + + it('should use getConfigForFile to retrieve the config', async () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/getConfigForFile/a/b/foo.css'), + ); + + const config = await stylelint.resolveConfig(filepath); + + expect(config).toStrictEqual({ + plugins: [path.join(__dirname, '/fixtures/plugin-warn-about-foo.js')], + rules: { + 'block-no-empty': [true], + 'plugin/warn-about-foo': ['always'], + }, + pluginFunctions: { + 'plugin/warn-about-foo': pluginWarnAboutFoo.rule, + }, + }); + }); + + it('should apply config overrides', async () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/config-overrides/testPrintConfig/style.css'), + ); + + const config = await stylelint.resolveConfig(filepath); + + expect(config).toStrictEqual({ + rules: { + 'block-no-empty': [true], + 'color-named': ['never'], + }, + }); + }); + + it('should respect the passed config', async () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/config-overrides/testPrintConfig/style.css'), + ); + + const config = await stylelint.resolveConfig(filepath, { + config: { + rules: { + 'color-no-invalid-hex': true, + 'color-no-named': 'always', + }, + }, + }); + + expect(config).toStrictEqual({ + rules: { + 'color-no-invalid-hex': [true], + 'color-no-named': [], + }, + }); + }); + + it('should use the passed config file path', async () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/config-overrides/testPrintConfig/style.css'), + ); + + const config = await stylelint.resolveConfig(filepath, { + configFile: path.join(__dirname, 'fixtures/getConfigForFile/a/.stylelintrc'), + }); + + expect(config).toStrictEqual({ + pluginFunctions: { + 'plugin/warn-about-foo': expect.any(Function), + }, + plugins: [expect.stringMatching(/plugin-warn-about-foo/)], + rules: { + 'block-no-empty': [true], + 'plugin/warn-about-foo': ['always'], + }, + }); + }); + + it('should use the passed config base directory', async () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/config-overrides/testPrintConfig/style.css'), + ); + + const config = await stylelint.resolveConfig(filepath, { + configBasedir: path.join(__dirname, 'fixtures'), + config: { + extends: './config-extending-two', + }, + }); + + expect(config).toStrictEqual({ + rules: { + 'block-no-empty': [true], + 'color-no-invalid-hex': [true], + }, + }); + }); + + it('should use the passed cwd', async () => { + const filepath = replaceBackslashes( + path.join(__dirname, 'fixtures/config-overrides/testPrintConfig/style.css'), + ); + + const config = await stylelint.resolveConfig(filepath, { + cwd: path.join(__dirname, 'fixtures'), + config: { + extends: './config-extending-two', + }, + }); + + expect(config).toStrictEqual({ + rules: { + 'block-no-empty': [true], + 'color-no-invalid-hex': [true], + }, + }); + }); +}); diff --git a/lib/index.js b/lib/index.js index f8bca2f451..5e6c736837 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,6 +10,7 @@ const ruleMessages = require('./utils/ruleMessages'); const rules = require('./rules'); const standalone = require('./standalone'); const validateOptions = require('./utils/validateOptions'); +const resolveConfig = require('./resolveConfig'); /** @type {import('stylelint').PublicApi} */ const stylelint = Object.assign(postcssPlugin, { @@ -17,6 +18,7 @@ const stylelint = Object.assign(postcssPlugin, { rules, formatters, createPlugin, + resolveConfig, createLinter: createStylelint, utils: { report, diff --git a/lib/printConfig.js b/lib/printConfig.js index 286cf7f686..dcd05e1367 100644 --- a/lib/printConfig.js +++ b/lib/printConfig.js @@ -1,8 +1,7 @@ 'use strict'; -const createStylelint = require('./createStylelint'); +const resolveConfig = require('./resolveConfig'); const globby = require('globby'); -const path = require('path'); /** @typedef {import('stylelint').Config} StylelintConfig */ @@ -10,7 +9,7 @@ const path = require('path'); * @param {import('stylelint').LinterOptions} options * @returns {Promise} */ -module.exports = function printConfig({ +module.exports = async function printConfig({ cwd = process.cwd(), code, config, @@ -33,25 +32,12 @@ module.exports = function printConfig({ return Promise.reject(new Error('The --print-config option does not support globs.')); } - const stylelint = createStylelint({ - config, - configFile, - configBasedir, - cwd, - }); - - const globCWD = (globbyOptions && globbyOptions.cwd) || cwd; - const absoluteFilePath = !path.isAbsolute(filePath) - ? path.join(globCWD, filePath) - : path.normalize(filePath); - - const configSearchPath = stylelint._options.configFile || absoluteFilePath; - - return stylelint.getConfigForFile(configSearchPath, absoluteFilePath).then((result) => { - if (result === null) { - return result; - } - - return result.config; - }); + return ( + (await resolveConfig(filePath, { + cwd: (globbyOptions && globbyOptions.cwd) || cwd, + config, + configBasedir, + configFile, + })) || null + ); }; diff --git a/lib/resolveConfig.js b/lib/resolveConfig.js new file mode 100644 index 0000000000..3ff990ad57 --- /dev/null +++ b/lib/resolveConfig.js @@ -0,0 +1,47 @@ +'use strict'; + +const createStylelint = require('./createStylelint'); +const path = require('path'); + +/** + * Resolves the effective configuation for a given file. Resolves to `undefined` + * if no config is found. + * @param {string} filePath - The path to the file to get the config for. + * @param {Pick< + * import('stylelint').LinterOptions, + * | 'cwd' + * | 'config' + * | 'configBasedir' + * | 'configFile' + * >} [options] - The options to use when creating the Stylelint instance. + * @returns {Promise} + */ +module.exports = async function resolveConfig( + filePath, + { cwd = process.cwd(), config, configBasedir, configFile } = {}, +) { + if (!filePath) { + return undefined; + } + + const stylelint = createStylelint({ + config, + configFile, + configBasedir, + cwd, + }); + + const absoluteFilePath = !path.isAbsolute(filePath) + ? path.join(cwd, filePath) + : path.normalize(filePath); + + const configSearchPath = stylelint._options.configFile || absoluteFilePath; + + const resolved = await stylelint.getConfigForFile(configSearchPath, absoluteFilePath); + + if (!resolved) { + return undefined; + } + + return resolved.config; +}; diff --git a/types/stylelint/index.d.ts b/types/stylelint/index.d.ts index ffbc8eac36..b7621476ad 100644 --- a/types/stylelint/index.d.ts +++ b/types/stylelint/index.d.ts @@ -324,6 +324,16 @@ declare module 'stylelint' { * @internal */ createLinter: (options: LinterOptions) => InternalApi; + /** + * Resolves the effective configuation for a given file. Resolves to + * `undefined` if no config is found. + * @param filePath - The path to the file to get the config for. + * @param options - The options to use when creating the Stylelint instance. + */ + resolveConfig: ( + filePath: string, + options?: Pick, + ) => Promise; utils: { /** * Report a problem. diff --git a/types/stylelint/type-test.ts b/types/stylelint/type-test.ts index f15b027c17..2a675c37fb 100644 --- a/types/stylelint/type-test.ts +++ b/types/stylelint/type-test.ts @@ -52,6 +52,29 @@ stylelint.lint(options).then((x: LinterResult) => { } }); +stylelint.resolveConfig('path').then((config) => stylelint.lint({ config })); + +stylelint.resolveConfig('path', { config: options }).then((config) => stylelint.lint({ config })); + +stylelint + .resolveConfig('path', { configBasedir: 'path' }) + .then((config) => stylelint.lint({ config })); + +stylelint + .resolveConfig('path', { configFile: 'path' }) + .then((config) => stylelint.lint({ config })); + +stylelint.resolveConfig('path', { cwd: 'path' }).then((config) => stylelint.lint({ config })); + +stylelint + .resolveConfig('path', { + config: options, + configBasedir: 'path', + configFile: 'path', + cwd: 'path', + }) + .then((config) => stylelint.lint({ config })); + const formatter: FormatterType = 'json'; const ruleName = 'sample-rule';