diff --git a/example/stylelint.config.js b/example/stylelint.config.js index c481b2c..e65097e 100644 --- a/example/stylelint.config.js +++ b/example/stylelint.config.js @@ -3,5 +3,6 @@ export default { plugins: ['stylelint-plugin-honey-css-modules'], rules: { 'honey-css-modules/no-unused-class-names': true, + 'honey-css-modules/no-missing-ts-file': true, }, }; diff --git a/packages/stylelint-plugin/src/index.ts b/packages/stylelint-plugin/src/index.ts index b75f465..dd52328 100644 --- a/packages/stylelint-plugin/src/index.ts +++ b/packages/stylelint-plugin/src/index.ts @@ -1,3 +1,4 @@ +import { noMissingTsFile } from './rules/no-missing-ts-file.js'; import { noUnusedClassNames } from './rules/no-unused-class-names.js'; -export = [noUnusedClassNames]; +export = [noUnusedClassNames, noMissingTsFile]; diff --git a/packages/stylelint-plugin/src/rules/no-missing-ts-file.test.ts b/packages/stylelint-plugin/src/rules/no-missing-ts-file.test.ts new file mode 100644 index 0000000..ba11ab0 --- /dev/null +++ b/packages/stylelint-plugin/src/rules/no-missing-ts-file.test.ts @@ -0,0 +1,59 @@ +import stylelint from 'stylelint'; +import { describe, expect, test } from 'vitest'; +import { createIFF } from '../test/fixture.js'; +import { formatLinterResult } from '../test/stylelint.js'; +import { noMissingTsFile } from './no-missing-ts-file.js'; + +async function lint(rootDir: string) { + return stylelint.lint({ + config: { + plugins: [noMissingTsFile], + rules: { + 'honey-css-modules/no-missing-ts-file': true, + }, + }, + files: ['**/*.module.css'], + cwd: rootDir, + }); +} + +describe('no-missing-ts-file', () => { + test('warns missing ts file', async () => { + const iff = await createIFF({ + 'a.module.css': '.foo {}', + }); + const results = await lint(iff.rootDir); + expect(formatLinterResult(results, iff.rootDir)).toMatchInlineSnapshot(` + [ + { + "source": "/a.module.css", + "warnings": [ + { + "column": 1, + "endColumn": 2, + "endLine": 1, + "line": 1, + "rule": "honey-css-modules/no-missing-ts-file", + "text": "The corresponding TypeScript file is not found. (honey-css-modules/no-missing-ts-file)", + }, + ], + }, + ] + `); + }); + test('does not warn when ts file exists', async () => { + const iff = await createIFF({ + 'a.module.css': '', + 'a.ts': '', + }); + const results = await lint(iff.rootDir); + expect(formatLinterResult(results, iff.rootDir)).toMatchInlineSnapshot(` + [ + { + "source": "/a.module.css", + "warnings": [], + }, + ] + `); + }); +}); diff --git a/packages/stylelint-plugin/src/rules/no-missing-ts-file.ts b/packages/stylelint-plugin/src/rules/no-missing-ts-file.ts new file mode 100644 index 0000000..0bbafc1 --- /dev/null +++ b/packages/stylelint-plugin/src/rules/no-missing-ts-file.ts @@ -0,0 +1,44 @@ +import type { Rule } from 'stylelint'; +import stylelint from 'stylelint'; +import { readTsFile } from '../util.js'; + +// TODO: Report cjs-module-lexer compatibility problem to stylelint +const { createPlugin, utils } = stylelint; + +const ruleName = 'honey-css-modules/no-missing-ts-file'; + +const messages = utils.ruleMessages(ruleName, { + disallow: () => `The corresponding TypeScript file is not found.`, +}); + +const meta = { + url: 'https://github.com/mizdra/honey-css-modules/blob/main/packages/stylelint-plugin-honey-css-modules/docs/rules/no-missing-ts-file.md', +}; + +const ruleFunction: Rule = (_primaryOptions, _secondaryOptions, _context) => { + return async (root, result) => { + if (root.source?.input.file === undefined) return; + const cssModulePath = root.source.input.file; + + if (!cssModulePath.endsWith('.module.css')) return; + + const tsFile = await readTsFile(cssModulePath); + + if (tsFile === undefined) { + utils.report({ + result, + ruleName, + message: messages.disallow(), + node: root, + index: 0, + endIndex: 0, + }); + } + }; +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export const noMissingTsFile = createPlugin(ruleName, ruleFunction);