diff --git a/.changeset/fifty-donuts-arrive.md b/.changeset/fifty-donuts-arrive.md new file mode 100644 index 0000000000..d93636f78c --- /dev/null +++ b/.changeset/fifty-donuts-arrive.md @@ -0,0 +1,5 @@ +--- +"stylelint": patch +--- + +Fixed: `customSyntax` resolution with `configBasedir` diff --git a/docs/user-guide/usage/cli.md b/docs/user-guide/usage/cli.md index dfad94e5ac..b39ab92037 100644 --- a/docs/user-guide/usage/cli.md +++ b/docs/user-guide/usage/cli.md @@ -46,7 +46,7 @@ Force enabling/disabling of color. ### `--config-basedir` -Absolute path to the directory that relative paths defining "extends" and "plugins" are _relative to_. Only necessary if these values are relative paths. [More info](options.md#configbasedir). +Absolute path to the directory that relative paths defining "extends", "plugins", and "customSyntax" are _relative to_. Only necessary if these values are relative paths. [More info](options.md#configbasedir). ### `--config` diff --git a/docs/user-guide/usage/options.md b/docs/user-guide/usage/options.md index fdafb60782..5e2ee4f67e 100644 --- a/docs/user-guide/usage/options.md +++ b/docs/user-guide/usage/options.md @@ -28,7 +28,7 @@ The path should be either absolute or relative to the directory that your proces CLI flag: `--config-basedir` -Absolute path to the directory that relative paths defining "extends" and "plugins" are _relative to_. Only necessary if these values are relative paths. +Absolute path to the directory that relative paths defining "extends", "plugins", and "customSyntax" are _relative to_. Only necessary if these values are relative paths. ## `fix` diff --git a/lib/__tests__/__snapshots__/cli.test.js.snap b/lib/__tests__/__snapshots__/cli.test.js.snap index b845138e77..a9b4fad1fa 100644 --- a/lib/__tests__/__snapshots__/cli.test.js.snap +++ b/lib/__tests__/__snapshots__/cli.test.js.snap @@ -29,9 +29,9 @@ exports[`CLI --help 1`] = ` --config-basedir - An absolute path to the directory that relative paths defining \\"extends\\" - and \\"plugins\\" are *relative to*. Only necessary if these values are - relative paths. + An absolute path to the directory that relative paths defining \\"extends\\", + \\"plugins\\", and \\"customSyntax\\" are *relative to*. Only necessary if these + values are relative paths. --print-config diff --git a/lib/__tests__/cli.test.js b/lib/__tests__/cli.test.js index fbea6d3a16..a3c15f770f 100644 --- a/lib/__tests__/cli.test.js +++ b/lib/__tests__/cli.test.js @@ -402,4 +402,34 @@ describe('CLI', () => { expect(process.stdout.write).toHaveBeenCalledTimes(1); expect(process.stdout.write).toHaveBeenCalledWith(expect.stringMatching(/block-no-empty/)); }); + + it('--custom-syntax', async () => { + await cli([ + '--custom-syntax=postcss-scss', + '--config', + fixturesPath('config-color-no-invalid-hex.json'), + fixturesPath('invalid-hex.scss'), + ]); + + expect(process.stdout.write).toHaveBeenCalledTimes(1); + expect(process.stdout.write).toHaveBeenCalledWith( + expect.stringMatching(/color-no-invalid-hex/), + ); + }); + + it('--custom-syntax and --config-basedir', async () => { + await cli([ + '--custom-syntax=./custom-syntax', + '--config-basedir', + fixturesPath(), + '--config', + fixturesPath('config-color-no-invalid-hex.json'), + fixturesPath('invalid-hex.scss'), + ]); + + expect(process.stdout.write).toHaveBeenCalledTimes(1); + expect(process.stdout.write).toHaveBeenCalledWith( + expect.stringMatching(/color-no-invalid-hex/), + ); + }); }); diff --git a/lib/__tests__/fixtures/invalid-hex.scss b/lib/__tests__/fixtures/invalid-hex.scss new file mode 100644 index 0000000000..505768655e --- /dev/null +++ b/lib/__tests__/fixtures/invalid-hex.scss @@ -0,0 +1,3 @@ +a { + color: #zzzzzz; // SCSS comment +} diff --git a/lib/__tests__/standalone-syntax.test.js b/lib/__tests__/standalone-syntax.test.js index c1bfe30b88..37eeaf0159 100644 --- a/lib/__tests__/standalone-syntax.test.js +++ b/lib/__tests__/standalone-syntax.test.js @@ -267,6 +267,28 @@ describe('customSyntax set in the config', () => { expect(results[0].warnings[0].rule).toBe('block-no-empty'); }); + it('standalone with path to custom syntax relative from "configBasedir"', async () => { + const config = { + customSyntax: './custom-syntax', + rules: { + 'block-no-empty': true, + }, + }; + + const { results } = await standalone({ + config, + configBasedir: fixturesPath, + code: '$foo: bar; // foo;\nb {}', + formatter: stringFormatter, + }); + + expect(results).toHaveLength(1); + expect(results[0].warnings).toHaveLength(1); + expect(results[0].warnings[0].line).toBe(2); + expect(results[0].warnings[0].column).toBe(3); + expect(results[0].warnings[0].rule).toBe('block-no-empty'); + }); + it('rejects on unknown custom syntax option', async () => { await expect( standalone({ @@ -280,4 +302,30 @@ describe('customSyntax set in the config', () => { 'Cannot resolve custom syntax module "unknown-module". Check that module "unknown-module" is available and spelled correctly.', ); }); + + it('rejects on invalid custom syntax object', async () => { + await expect( + standalone({ + code: '', + config: { + customSyntax: {}, + rules: { 'block-no-empty': 'wahoo' }, + }, + }), + ).rejects.toThrow( + 'An object provided to the "customSyntax" option must have a "parse" property. Ensure the "parse" property exists and its value is a function.', + ); + }); + + it('rejects on invalid custom syntax type', async () => { + await expect( + standalone({ + code: '', + config: { + customSyntax: true, + rules: { 'block-no-empty': 'wahoo' }, + }, + }), + ).rejects.toThrow('Custom syntax must be a string or a Syntax object'); + }); }); diff --git a/lib/cli.js b/lib/cli.js index f4bcbfafc4..713e37890c 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -8,7 +8,6 @@ const { isPlainObject } = require('./utils/validateTypes'); const checkInvalidCLIOptions = require('./utils/checkInvalidCLIOptions'); const getFormatterOptionsText = require('./utils/getFormatterOptionsText'); -const getModulePath = require('./utils/getModulePath'); const getStdin = require('./utils/getStdin'); const printConfig = require('./printConfig'); const resolveFrom = require('resolve-from'); @@ -119,9 +118,9 @@ const meowOptions = { --config-basedir - An absolute path to the directory that relative paths defining "extends" - and "plugins" are *relative to*. Only necessary if these values are - relative paths. + An absolute path to the directory that relative paths defining "extends", + "plugins", and "customSyntax" are *relative to*. Only necessary if these + values are relative paths. --print-config @@ -387,7 +386,7 @@ module.exports = async (argv) => { } if (cli.flags.customSyntax) { - optionsBase.customSyntax = getModulePath(process.cwd(), cli.flags.customSyntax); + optionsBase.customSyntax = cli.flags.customSyntax; } if (cli.flags.config) { diff --git a/lib/getPostcssResult.js b/lib/getPostcssResult.js index 4b00c5aa87..dc633e74fa 100644 --- a/lib/getPostcssResult.js +++ b/lib/getPostcssResult.js @@ -5,6 +5,8 @@ const path = require('path'); const { default: postcss } = require('postcss'); const { promises: fs } = require('fs'); +const getModulePath = require('./utils/getModulePath'); + /** @typedef {import('postcss').Result} Result */ /** @typedef {import('postcss').Syntax} Syntax */ /** @typedef {import('stylelint').CustomSyntax} CustomSyntax */ @@ -38,7 +40,7 @@ module.exports = async function getPostcssResult(stylelint, options = {}) { } const syntax = options.customSyntax - ? getCustomSyntax(options.customSyntax) + ? getCustomSyntax(options.customSyntax, stylelint._options.configBasedir) : cssSyntax(stylelint, options.filePath); const postcssOptions = { @@ -85,21 +87,25 @@ module.exports = async function getPostcssResult(stylelint, options = {}) { /** * @param {CustomSyntax} customSyntax + * @param {string | undefined} basedir * @returns {Syntax} */ -function getCustomSyntax(customSyntax) { - let resolved; - +function getCustomSyntax(customSyntax, basedir) { if (typeof customSyntax === 'string') { + const customSyntaxLookup = basedir ? getModulePath(basedir, customSyntax) : customSyntax; + + let resolved; + try { - resolved = require(customSyntax); + resolved = require(customSyntaxLookup); } catch (error) { if ( error && typeof error === 'object' && - // @ts-expect-error -- TS2571: Object is of type 'unknown'. + 'code' in error && error.code === 'MODULE_NOT_FOUND' && - // @ts-expect-error -- TS2571: Object is of type 'unknown'. + 'message' in error && + typeof error.message === 'string' && error.message.includes(customSyntax) ) { throw new Error( @@ -126,17 +132,15 @@ function getCustomSyntax(customSyntax) { if (typeof customSyntax === 'object') { if (typeof customSyntax.parse === 'function') { - resolved = { ...customSyntax }; - } else { - throw new TypeError( - `An object provided to the "customSyntax" option must have a "parse" property. Ensure the "parse" property exists and its value is a function.`, - ); + return { ...customSyntax }; } - return resolved; + throw new TypeError( + 'An object provided to the "customSyntax" option must have a "parse" property. Ensure the "parse" property exists and its value is a function.', + ); } - throw new Error(`Custom syntax must be a string or a Syntax object`); + throw new Error('Custom syntax must be a string or a Syntax object'); } /** @type {{ [key: string]: string }} */ diff --git a/lib/utils/getModulePath.js b/lib/utils/getModulePath.js index 8960279891..873b7a9e6b 100644 --- a/lib/utils/getModulePath.js +++ b/lib/utils/getModulePath.js @@ -25,7 +25,9 @@ module.exports = function getModulePath(basedir, lookup, cwd = process.cwd()) { } if (!path) { - throw configurationError(`Could not find "${lookup}". Do you need a \`configBasedir\`?`); + throw configurationError( + `Could not find "${lookup}". Do you need the "configBasedir" or "--config-basedir" option?`, + ); } return path;