diff --git a/.changeset/great-weeks-remain.md b/.changeset/great-weeks-remain.md new file mode 100644 index 00000000..9b145eac --- /dev/null +++ b/.changeset/great-weeks-remain.md @@ -0,0 +1,5 @@ +--- +'@css-modules-kit/codegen': minor +--- + +feat: exit with error when `cmkOptions.enabled` is false diff --git a/.changeset/silly-results-eat.md b/.changeset/silly-results-eat.md new file mode 100644 index 00000000..1d7db1a8 --- /dev/null +++ b/.changeset/silly-results-eat.md @@ -0,0 +1,5 @@ +--- +'@css-modules-kit/codegen': minor +--- + +feat: warn when `cmkOptions.enabled` is not set diff --git a/README.md b/README.md index 7e2eac7d..a659a918 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,23 @@ In TypeScript, the `include`/`exclude` properties specify which `*.ts` files to } ``` +### `cmkOptions.enabled` + +Type: `boolean`, Default: `true` + +Enables or disables css-modules-kit. When set to `false`, codegen will exit with an error. Currently, both codegen and the ts-plugin will work even if this option is omitted, but in the future, they will not work unless this option is set to `true`. For more details, see [#289](https://github.com/mizdra/css-modules-kit/issues/289). + +```jsonc +{ + "compilerOptions": { + // ... + }, + "cmkOptions": { + "enabled": true, + }, +} +``` + ### `cmkOptions.dtsOutDir` Type: `string`, Default: `"generated"` diff --git a/docs/get-started.md b/docs/get-started.md index 6733f7d0..f41062b7 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -47,11 +47,16 @@ Configure npm-script to run `cmk` command before building and type checking. Thi ## Configure `tsconfig.json` -Finally, you need to configure your tsconfig.json so that the ts-plugin and codegen work correctly. - -- Set the `include` option so that files like `*.module.css` are considered for type-checking: - - For example: `["src"]`, `["src/**/*"]`, or omitting the `include` option (which is equivalent to `["**/*"]`) - - Not recommended: `["src/**/*.ts"]`, `["src/index.ts"]` +Finally, you need to configure your tsconfig.json so that css-modules-kit works correctly. + +- Set `cmkOptions.enabled` to `true` to enable css-modules-kit. +- Omit the `include` options or ensure that `*.module.css` files are included when specifying them explicitly. + - ✅ Good cases: + - Omit `include` (equivalent to `["**/*"]`) + - Use patterns like `["src"]`, `["src/**/*"]` + - ❌ Bad cases: + - `["src/**/*.ts"]` + - `["src/index.ts"]` (excludes `*.module.css` files) - Set the `rootDirs` option to include both the directory containing `tsconfig.json` and the `generated` directory. - Example: `[".", "generated"]` @@ -59,38 +64,16 @@ Below is an example configuration: ```jsonc { - // Omitting the `include` option is equivalent to using `["**/*"]` "compilerOptions": { - /* Projects */ "rootDirs": [".", "generated"], - - /* Language and Environment */ - "target": "ESNext", - "lib": ["ESNext"], - - /* Modules */ - "module": "NodeNext", - "moduleResolution": "NodeNext", - - /* Emit */ - "noEmit": true, - - /* Interop Constraints */ - "verbatimModuleSyntax": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - - /* Type Checking */ - "strict": true, - - /* Completeness */ - "skipLibCheck": true, + // ... + }, + "cmkOptions": { + "enabled": true, }, } ``` -This completes the minimal setup. - ## Install linter plugin (Optional) We provide linter plugin for CSS Modules. Currently, we support the following linters: diff --git a/examples/1-basic/tsconfig.json b/examples/1-basic/tsconfig.json index 6a0acf87..a17e0773 100644 --- a/examples/1-basic/tsconfig.json +++ b/examples/1-basic/tsconfig.json @@ -11,5 +11,8 @@ "incremental": false, "rootDirs": [".", "generated"], "types": [] // Simplify tsserver.log + }, + "cmkOptions": { + "enabled": true } } diff --git a/examples/2-named-exports/tsconfig.json b/examples/2-named-exports/tsconfig.json index 89d0a027..07c3e578 100644 --- a/examples/2-named-exports/tsconfig.json +++ b/examples/2-named-exports/tsconfig.json @@ -13,6 +13,7 @@ "types": [] // Simplify tsserver.log }, "cmkOptions": { + "enabled": true, "namedExports": true } } diff --git a/examples/3-import-alias/tsconfig.json b/examples/3-import-alias/tsconfig.json index 1c4dbb9e..4a5aae48 100644 --- a/examples/3-import-alias/tsconfig.json +++ b/examples/3-import-alias/tsconfig.json @@ -13,5 +13,8 @@ "types": [], // Simplify tsserver.log "paths": { "@/*": ["./*"] } + }, + "cmkOptions": { + "enabled": true } } diff --git a/examples/4-multiple-tsconfig/tsconfig.dir-1.json b/examples/4-multiple-tsconfig/tsconfig.dir-1.json index b25fd3f0..34ef5652 100644 --- a/examples/4-multiple-tsconfig/tsconfig.dir-1.json +++ b/examples/4-multiple-tsconfig/tsconfig.dir-1.json @@ -14,5 +14,8 @@ "types": [], // Simplify tsserver.log "paths": { "@/*": ["./*"] } + }, + "cmkOptions": { + "enabled": true } } diff --git a/examples/4-multiple-tsconfig/tsconfig.dir-2.json b/examples/4-multiple-tsconfig/tsconfig.dir-2.json index 2822ea5a..c2dea210 100644 --- a/examples/4-multiple-tsconfig/tsconfig.dir-2.json +++ b/examples/4-multiple-tsconfig/tsconfig.dir-2.json @@ -14,5 +14,8 @@ "types": [], // Simplify tsserver.log "paths": { "@/*": ["./*"] } + }, + "cmkOptions": { + "enabled": true } } diff --git a/examples/5-normal-css/tsconfig.json b/examples/5-normal-css/tsconfig.json index 6a0acf87..a17e0773 100644 --- a/examples/5-normal-css/tsconfig.json +++ b/examples/5-normal-css/tsconfig.json @@ -11,5 +11,8 @@ "incremental": false, "rootDirs": [".", "generated"], "types": [] // Simplify tsserver.log + }, + "cmkOptions": { + "enabled": true } } diff --git a/examples/6-project-external-file/tsconfig.json b/examples/6-project-external-file/tsconfig.json index eb60cafe..cfe6993e 100644 --- a/examples/6-project-external-file/tsconfig.json +++ b/examples/6-project-external-file/tsconfig.json @@ -12,5 +12,8 @@ "incremental": false, "rootDirs": [".", "generated"], "types": [] // Simplify tsserver.log + }, + "cmkOptions": { + "enabled": true } } diff --git a/packages/codegen/e2e-test/index.test.ts b/packages/codegen/e2e-test/index.test.ts index b46b63af..c96db94c 100644 --- a/packages/codegen/e2e-test/index.test.ts +++ b/packages/codegen/e2e-test/index.test.ts @@ -33,7 +33,8 @@ test('generates .d.ts', async () => { "noEmit": true, "paths": { "@/*": ["./src/*"] }, "rootDirs": [".", "generated"] - } + }, + "cmkOptions": { "enabled": true } } `, }); @@ -91,7 +92,7 @@ test('prints version number', () => { test('reports CSS syntax error', async () => { const iff = await createIFF({ 'src/a.module.css': `badword`, - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', }); const cmk = spawnSync('node', [binPath, '--pretty'], { cwd: iff.rootDir }); expect(cmk.status).toBe(1); @@ -138,7 +139,8 @@ test('generates .d.ts with circular import', async () => { "lib": ["ES2015"], "noEmit": true, "rootDirs": [".", "generated"] - } + }, + "cmkOptions": { "enabled": true } } `, }); diff --git a/packages/codegen/src/error.ts b/packages/codegen/src/error.ts index fc5f62ef..f2d94cbf 100644 --- a/packages/codegen/src/error.ts +++ b/packages/codegen/src/error.ts @@ -1,4 +1,5 @@ -import { SystemError } from '@css-modules-kit/core'; +import type { CMKConfig } from '@css-modules-kit/core'; +import { relative, SystemError } from '@css-modules-kit/core'; export class ParseCLIArgsError extends SystemError { constructor(cause: unknown) { @@ -17,3 +18,12 @@ export class ReadCSSModuleFileError extends SystemError { super('READ_CSS_MODULE_FILE_ERROR', `Failed to read CSS Module file ${fileName}.`, cause); } } + +export class CMKDisabledError extends SystemError { + constructor(config: CMKConfig) { + super( + 'CMK_DISABLED_ERROR', + `css-modules-kit is disabled by configuration. Set \`"cmkOptions": { "enabled": true }\` in ${relative(config.basePath, config.configFileName)} to enable it.`, + ); + } +} diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts index 4ad7f0d6..b9a76d05 100644 --- a/packages/codegen/src/index.ts +++ b/packages/codegen/src/index.ts @@ -1,5 +1,5 @@ export { runCMK, runCMKInWatchMode } from './runner.js'; export { type Logger, createLogger } from './logger/logger.js'; -export { WriteDtsFileError, ReadCSSModuleFileError } from './error.js'; +export { WriteDtsFileError, ReadCSSModuleFileError, CMKDisabledError } from './error.js'; export { parseCLIArgs, printHelpText, printVersion } from './cli.js'; export { shouldBePretty } from './3rd-party/typescript.js'; diff --git a/packages/codegen/src/logger/logger.test.ts b/packages/codegen/src/logger/logger.test.ts index 9655f740..8f288c59 100644 --- a/packages/codegen/src/logger/logger.test.ts +++ b/packages/codegen/src/logger/logger.test.ts @@ -26,6 +26,7 @@ describe('createLogger', () => { start: { line: 1, column: 2 }, length: 3, }, + { text: 'text4', category: 'warning' }, ]; logger.logDiagnostics(diagnostics); expect(stripVTControlCharacters(stderrWriteSpy.mock.lastCall![0] as string)).toMatchInlineSnapshot(` @@ -35,6 +36,8 @@ describe('createLogger', () => { a.module.css(1,2): error: text3 + warning: text4 + " `); }); diff --git a/packages/codegen/src/project.test.ts b/packages/codegen/src/project.test.ts index 180478b3..5963b58d 100644 --- a/packages/codegen/src/project.test.ts +++ b/packages/codegen/src/project.test.ts @@ -10,7 +10,7 @@ import { createIFF } from './test/fixture.js'; describe('createProject', () => { test('creates project', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', }); const project = createProject({ project: iff.rootDir }); expect(project.config.dtsOutDir).toContain('generated'); @@ -23,7 +23,7 @@ describe('createProject', () => { 'throws ReadCSSModuleFileError when a CSS module file cannot be read', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a1 { color: red; }', }); await chmod(iff.paths['src/a.module.css'], 0o200); // Remove read permission @@ -50,7 +50,7 @@ test('isWildcardMatchedFile', async () => { describe('addFile', () => { test('The diagnostics of the added file are reported, and .d.ts file is emitted', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src': {}, }); const project = createProject({ project: iff.rootDir }); @@ -90,7 +90,7 @@ describe('addFile', () => { // - The check stage cache for files that directly import the added file should be invalidated. // - The check stage cache for files that indirectly import the added file should also be invalidated. const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/b.module.css': '@import "./a.module.css";', // directly 'src/c.module.css': '@value a_1 from "./b.module.css";', // indirectly }); @@ -134,7 +134,8 @@ describe('addFile', () => { "paths": { "@/a.module.css": ["src/a-1.module.css", "src/a-2.module.css"] } - } + }, + "cmkOptions": { "enabled": true } } `, 'src/a-2.module.css': '@value a_2: red;', @@ -165,7 +166,7 @@ describe('addFile', () => { describe('updateFile', () => { test('The new diagnostics of the changed file are reported, and new .d.ts file is emitted', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '', }); const project = createProject({ project: iff.rootDir }); @@ -226,7 +227,7 @@ describe('updateFile', () => { // - The check stage cache for files that directly import the changed file should be invalidated. // - The check stage cache for files that indirectly import the changed file should also be invalidated. const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '', 'src/b.module.css': dedent` @value a_1 from "./a.module.css"; @@ -268,7 +269,7 @@ describe('updateFile', () => { describe('removeFile', () => { test('The diagnostics of the removed file are not reported, and .d.ts file is not emitted', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', }); const project = createProject({ project: iff.rootDir }); @@ -308,7 +309,7 @@ describe('removeFile', () => { // - The check stage cache for files that directly import the changed file should be invalidated. // - The check stage cache for files that indirectly import the changed file should also be invalidated. const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '@value a_1: red;', 'src/b.module.css': '@import "./a.module.css";', // directly 'src/c.module.css': '@value a_1 from "./b.module.css";', // indirectly @@ -353,7 +354,8 @@ describe('removeFile', () => { "paths": { "@/a.module.css": ["src/a-1.module.css", "src/a-2.module.css"] } - } + }, + "cmkOptions": { "enabled": true } } `, 'src/a-1.module.css': '@value a_1: red;', @@ -385,16 +387,32 @@ describe('removeFile', () => { describe('getDiagnostics', () => { test('returns empty array when no diagnostics', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 { color: red; }', }); const project = createProject({ project: iff.rootDir }); const diagnostics = project.getDiagnostics(); expect(diagnostics).toEqual([]); }); + test('returns warning when enabled is not specified', async () => { + const iff = await createIFF({ + 'tsconfig.json': '{}', + 'src/a.module.css': '.a_1 { color: red; }', + }); + const project = createProject({ project: iff.rootDir }); + const diagnostics = project.getDiagnostics(); + expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(` + [ + { + "category": "warning", + "text": ""cmkOptions.enabled" will be required in a future version of css-modules-kit. Add \`"cmkOptions": { "enabled": true }\` to tsconfig.json. See https://github.com/mizdra/css-modules-kit/issues/289 for details.", + }, + ] + `); + }); test('returns project diagnostics', async () => { const iff = await createIFF({ - 'tsconfig.json': '{ "cmkOptions": { "dtsOutDir": 1 } }', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true, "dtsOutDir": 1 } }', }); const project = createProject({ project: iff.rootDir }); const diagnostics = project.getDiagnostics(); @@ -413,7 +431,7 @@ describe('getDiagnostics', () => { }); test('returns syntactic diagnostics', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', 'src/b.module.css': '.a_2 { color }', }); @@ -447,7 +465,7 @@ describe('getDiagnostics', () => { test('returns semantic diagnostics', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': `@import './non-existent-1.module.css';`, 'src/b.module.css': `@import './non-existent-2.module.css';`, }); @@ -480,7 +498,7 @@ describe('getDiagnostics', () => { }); test('skips semantic diagnostics when project or syntactic diagnostics exist', async () => { const iff = await createIFF({ - 'tsconfig.json': '{ "cmkOptions": { "dtsOutDir": 1 } }', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true, "dtsOutDir": 1 } }', 'src/a.module.css': '.a_1 {', 'src/b.module.css': `@import './non-existent.module.css';`, }); @@ -510,7 +528,7 @@ describe('getDiagnostics', () => { describe('emitDtsFiles', () => { test('emits .d.ts files', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a1 { color: red; }', 'src/b.module.css': '.b1 { color: blue; }', }); @@ -535,7 +553,7 @@ describe('emitDtsFiles', () => { }); test('does not emit .d.ts files for files not matched by `pattern`', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a1 { color: red; }', 'src/b.css': '.b1 { color: blue; }', }); diff --git a/packages/codegen/src/project.ts b/packages/codegen/src/project.ts index 736f01a2..8810ea57 100644 --- a/packages/codegen/src/project.ts +++ b/packages/codegen/src/project.ts @@ -10,6 +10,7 @@ import { getFileNamesByPattern, parseCSSModule, readConfigFile, + relative, } from '@css-modules-kit/core'; import ts from 'typescript'; import { writeDtsFile } from './dts-writer.js'; @@ -166,6 +167,12 @@ export function createProject(args: ProjectArgs): Project { function getProjectDiagnostics() { const diagnostics: Diagnostic[] = []; diagnostics.push(...config.diagnostics); + if (config.enabled === undefined) { + diagnostics.push({ + category: 'warning', + text: `"cmkOptions.enabled" will be required in a future version of css-modules-kit. Add \`"cmkOptions": { "enabled": true }\` to ${relative(config.basePath, config.configFileName)}. See https://github.com/mizdra/css-modules-kit/issues/289 for details.`, + }); + } if (cssModuleMap.size === 0) { diagnostics.push({ category: 'error', diff --git a/packages/codegen/src/runner.test.ts b/packages/codegen/src/runner.test.ts index 647fa1c9..7aedba0a 100644 --- a/packages/codegen/src/runner.test.ts +++ b/packages/codegen/src/runner.test.ts @@ -2,6 +2,7 @@ import { access, rm, writeFile } from 'node:fs/promises'; import { platform } from 'node:process'; import dedent from 'dedent'; import { afterEach, describe, expect, test, vi } from 'vitest'; +import { CMKDisabledError } from './error.js'; import type { Watcher } from './runner.js'; import { runCMK, runCMKInWatchMode } from './runner.js'; import { formatDiagnostics } from './test/diagnostic.js'; @@ -45,7 +46,7 @@ describe('runCMK', () => { }); test('reports diagnostics if errors are found', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', 'src/b.module.css': '.b_1 { color: red; }', }); @@ -69,7 +70,7 @@ describe('runCMK', () => { }); test('returns false if errors are found', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', }); const result1 = await runCMK(fakeParsedArgs({ project: iff.rootDir }), createLoggerSpy()); @@ -80,7 +81,7 @@ describe('runCMK', () => { }); test('emits .d.ts files even if there are diagnostics', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', 'src/b.module.css': '.b_1 { color: red; }', }); @@ -92,7 +93,7 @@ describe('runCMK', () => { }); test('removes output directory before emitting files when `clean` is true', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 { color: red; }', 'generated/src/old.module.css.d.ts': '', }); @@ -100,6 +101,13 @@ describe('runCMK', () => { await expect(access(iff.join('generated/src/a.module.css.d.ts'))).resolves.not.toThrow(); await expect(access(iff.join('generated/src/old.module.css.d.ts'))).rejects.toThrow(); }); + test('throws CMKDisabledError if enabled is false', async () => { + const iff = await createIFF({ + 'tsconfig.json': '{ "cmkOptions": { "enabled": false } }', + 'src/a.module.css': '.a_1 { color: red; }', + }); + await expect(runCMK(fakeParsedArgs({ project: iff.rootDir }), createLoggerSpy())).rejects.toThrow(CMKDisabledError); + }); }); describe('runCMKInWatchMode', () => { @@ -141,7 +149,7 @@ describe('runCMKInWatchMode', () => { }); test('reports diagnostics if errors are found', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', 'src/b.module.css': '.b_1 { color: red; }', }); @@ -165,7 +173,7 @@ describe('runCMKInWatchMode', () => { }); test('emits .d.ts files even if there are diagnostics', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 {', 'src/b.module.css': '.b_1 { color: red; }', }); @@ -177,7 +185,7 @@ describe('runCMKInWatchMode', () => { }); test('removes output directory before emitting files when `clean` is true', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 { color: red; }', 'generated/src/old.module.css.d.ts': '', }); @@ -187,7 +195,7 @@ describe('runCMKInWatchMode', () => { }); test('reports system error occurs during watching', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 { color: red; }', }); @@ -232,7 +240,7 @@ describe('runCMKInWatchMode', () => { }); test('reports diagnostics and emits files on changes', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 { color: red; }', }); const loggerSpy = createLoggerSpy(); @@ -267,7 +275,7 @@ describe('runCMKInWatchMode', () => { }); test('batches rapid file changes', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src': {}, }); const loggerSpy = createLoggerSpy(); @@ -291,7 +299,7 @@ describe('runCMKInWatchMode', () => { }); test('does not clear screen when preserveWatchOutput is true', async () => { const iff = await createIFF({ - 'tsconfig.json': '{}', + 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', 'src/a.module.css': '.a_1 { color: red; }', }); @@ -305,4 +313,13 @@ describe('runCMKInWatchMode', () => { watcher = await runCMKInWatchMode(fakeParsedArgs({ project: iff.rootDir, preserveWatchOutput: true }), loggerSpy2); expect(loggerSpy2.clearScreen).toHaveBeenCalledTimes(0); }); + test('throws CMKDisabledError if enabled is false', async () => { + const iff = await createIFF({ + 'tsconfig.json': '{ "cmkOptions": { "enabled": false } }', + 'src/a.module.css': '.a_1 { color: red; }', + }); + await expect(runCMKInWatchMode(fakeParsedArgs({ project: iff.rootDir }), createLoggerSpy())).rejects.toThrow( + CMKDisabledError, + ); + }); }); diff --git a/packages/codegen/src/runner.ts b/packages/codegen/src/runner.ts index 91a261bb..79703cc4 100644 --- a/packages/codegen/src/runner.ts +++ b/packages/codegen/src/runner.ts @@ -1,6 +1,7 @@ import type { Stats } from 'node:fs'; import { rm } from 'node:fs/promises'; import chokidar, { type FSWatcher } from 'chokidar'; +import { CMKDisabledError } from './error.js'; import type { Logger } from './logger/logger.js'; import { createProject, type Project } from './project.js'; @@ -19,12 +20,16 @@ export interface Watcher { /** * Run css-modules-kit .d.ts generation. * @param project The absolute path to the project directory or the path to `tsconfig.json`. + * @throws {CMKDisabledError} When css-modules-kit is disabled. * @throws {ReadCSSModuleFileError} When failed to read CSS Module file. * @throws {WriteDtsFileError} * @returns Whether the process succeeded without errors. */ export async function runCMK(args: RunnerArgs, logger: Logger): Promise { const project = createProject(args); + if (project.config.enabled === false) { + throw new CMKDisabledError(project.config); + } if (args.clean) { await rm(project.config.dtsOutDir, { recursive: true, force: true }); } @@ -32,7 +37,8 @@ export async function runCMK(args: RunnerArgs, logger: Logger): Promise const diagnostics = project.getDiagnostics(); if (diagnostics.length > 0) { logger.logDiagnostics(diagnostics); - return false; + const hasErrors = diagnostics.some((d) => d.category === 'error'); + return !hasErrors; } return true; } @@ -45,6 +51,7 @@ export async function runCMK(args: RunnerArgs, logger: Logger): Promise * * NOTE: For implementation simplicity, config file changes are not watched. * @param project The absolute path to the project directory or the path to `tsconfig.json`. + * @throws {CMKDisabledError} When css-modules-kit is disabled. * @throws {TsConfigFileNotFoundError} * @throws {ReadCSSModuleFileError} * @throws {WriteDtsFileError} @@ -52,6 +59,9 @@ export async function runCMK(args: RunnerArgs, logger: Logger): Promise export async function runCMKInWatchMode(args: RunnerArgs, logger: Logger): Promise { const fsWatchers: FSWatcher[] = []; const project = createProject(args); + if (project.config.enabled === false) { + throw new CMKDisabledError(project.config); + } let emitAndReportDiagnosticsTimer: NodeJS.Timeout | undefined = undefined; if (args.clean) { @@ -138,8 +148,11 @@ export async function runCMKInWatchMode(args: RunnerArgs, logger: Logger): Promi if (diagnostics.length > 0) { logger.logDiagnostics(diagnostics); } + const errorCount = diagnostics.filter((d) => d.category === 'error').length; + const warningCount = diagnostics.filter((d) => d.category === 'warning').length; + const warningPart = warningCount > 0 ? ` and ${warningCount} warning${warningCount === 1 ? '' : 's'}` : ''; logger.logMessage( - `Found ${diagnostics.length} error${diagnostics.length === 1 ? '' : 's'}. Watching for file changes.`, + `Found ${errorCount} error${errorCount === 1 ? '' : 's'}${warningPart}. Watching for file changes.`, { time: true }, ); if (args.preserveWatchOutput) { diff --git a/packages/core/src/config.test.ts b/packages/core/src/config.test.ts index f01cad3d..f625c87c 100644 --- a/packages/core/src/config.test.ts +++ b/packages/core/src/config.test.ts @@ -27,6 +27,7 @@ describe('readConfigFile', () => { namedExports: false, prioritizeNamedImports: false, keyframes: true, + enabled: undefined, compilerOptions: expect.any(Object), wildcardDirectories: [{ fileName: iff.rootDir, recursive: true }], }), @@ -46,7 +47,8 @@ describe('readConfigFile', () => { "arbitraryExtensions": true, "namedExports": true, "prioritizeNamedImports": true, - "keyframes": false + "keyframes": false, + "enabled": true } } `, @@ -60,6 +62,7 @@ describe('readConfigFile', () => { namedExports: true, prioritizeNamedImports: true, keyframes: false, + enabled: true, compilerOptions: expect.objectContaining({ module: ts.ModuleKind.ESNext, }), @@ -82,7 +85,8 @@ describe('readConfigFile', () => { "arbitraryExtensions": true, "namedExports": true, "prioritizeNamedImports": true, - "keyframes": false + "keyframes": false, + "enabled": true } } `, @@ -97,6 +101,7 @@ describe('readConfigFile', () => { namedExports: true, prioritizeNamedImports: true, keyframes: false, + enabled: true, compilerOptions: expect.objectContaining({ module: ts.ModuleKind.ESNext, }), @@ -104,6 +109,22 @@ describe('readConfigFile', () => { }), ); }); + test('enabled can be overridden by extending tsconfig', async () => { + const iff = await createIFF({ + 'tsconfig.base.json': dedent` + { + "cmkOptions": { "enabled": true } + } + `, + 'tsconfig.json': dedent` + { + "extends": "./tsconfig.base.json", + "cmkOptions": { "enabled": false } + } + `, + }); + expect(readConfigFile(iff.rootDir).enabled).toBe(false); + }); test('inherited options can be overridden in the target tsconfig', async () => { const iff = await createIFF({ 'tsconfig.base.json': dedent` diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index a071d945..dab60beb 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -13,6 +13,7 @@ const DEFAULT_INCLUDE_SPEC = '**/*'; export interface CMKConfig { includes: string[]; excludes: string[]; + enabled: boolean | undefined; dtsOutDir: string; arbitraryExtensions: boolean; namedExports: boolean; @@ -71,6 +72,7 @@ export interface CMKConfig { interface UnnormalizedRawConfig { includes?: string[]; excludes?: string[]; + enabled?: boolean; dtsOutDir?: string; arbitraryExtensions?: boolean; namedExports?: boolean; @@ -99,6 +101,7 @@ function isTsConfigFileExists(fileName: string): boolean { return ts.findConfigFile(dirname(fileName), ts.sys.fileExists.bind(ts.sys), basename(fileName)) !== undefined; } +// eslint-disable-next-line complexity function parseRawData(raw: unknown, tsConfigSourceFile: ts.TsConfigSourceFile): ParsedRawData { const result: ParsedRawData = { config: {}, @@ -124,6 +127,16 @@ function parseRawData(raw: unknown, tsConfigSourceFile: ts.TsConfigSourceFile): // MEMO: The errors for this option are reported by `tsc` or `tsserver`, so we don't need to report. } if ('cmkOptions' in raw && typeof raw.cmkOptions === 'object' && raw.cmkOptions !== null) { + if ('enabled' in raw.cmkOptions) { + if (typeof raw.cmkOptions.enabled === 'boolean') { + result.config.enabled = raw.cmkOptions.enabled; + } else { + result.diagnostics.push({ + category: 'error', + text: `\`enabled\` in ${tsConfigSourceFile.fileName} must be a boolean.`, + }); + } + } if ('dtsOutDir' in raw.cmkOptions) { if (typeof raw.cmkOptions.dtsOutDir === 'string') { result.config.dtsOutDir = raw.cmkOptions.dtsOutDir; @@ -255,6 +268,7 @@ export function readConfigFile(project: string): CMKConfig { namedExports: parsedTsConfig.config.namedExports ?? false, prioritizeNamedImports: parsedTsConfig.config.prioritizeNamedImports ?? false, keyframes: parsedTsConfig.config.keyframes ?? true, + enabled: parsedTsConfig.config.enabled, basePath, configFileName, compilerOptions: parsedTsConfig.compilerOptions, diff --git a/packages/core/src/diagnostic.ts b/packages/core/src/diagnostic.ts index 43da883e..1dd94c9b 100644 --- a/packages/core/src/diagnostic.ts +++ b/packages/core/src/diagnostic.ts @@ -11,6 +11,8 @@ function convertErrorCategory(category: DiagnosticCategory): ts.DiagnosticCatego switch (category) { case 'error': return ts.DiagnosticCategory.Error; + case 'warning': + return ts.DiagnosticCategory.Warning; default: throw new Error(`Unknown category: ${String(category)}`); } diff --git a/packages/core/src/test/faker.ts b/packages/core/src/test/faker.ts index 68bcfb42..c7174ec7 100644 --- a/packages/core/src/test/faker.ts +++ b/packages/core/src/test/faker.ts @@ -6,6 +6,7 @@ export function fakeConfig(args?: Partial): CMKConfig { return { includes: ['/app/**/*'], excludes: [], + enabled: true, dtsOutDir: 'generated', arbitraryExtensions: false, namedExports: false, diff --git a/packages/core/src/type.ts b/packages/core/src/type.ts index c37d8047..4aec55a1 100644 --- a/packages/core/src/type.ts +++ b/packages/core/src/type.ts @@ -151,7 +151,7 @@ export interface ExportBuilder { clearCache(): void; } -export type DiagnosticCategory = 'error'; +export type DiagnosticCategory = 'error' | 'warning'; export interface DiagnosticSourceFile { fileName: string; diff --git a/packages/vscode/schemas/tsconfig.schema.json b/packages/vscode/schemas/tsconfig.schema.json index d71c6823..cfe8741b 100644 --- a/packages/vscode/schemas/tsconfig.schema.json +++ b/packages/vscode/schemas/tsconfig.schema.json @@ -5,6 +5,11 @@ "type": "object", "markdownDescription": "Config options for css-modules-kit.", "properties": { + "enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Enables or disables css-modules-kit." + }, "dtsOutDir": { "type": "string", "default": "generated",