Skip to content

Commit 04e1b6e

Browse files
committed
feat: implement watch mode
1 parent 343a26d commit 04e1b6e

File tree

4 files changed

+133
-5
lines changed

4 files changed

+133
-5
lines changed

.changeset/modern-tables-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@css-modules-kit/codegen': minor
3+
---
4+
5+
feat: add `--watch` option

packages/codegen/bin/cmk.mjs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
#!/usr/bin/env node
22
/* eslint-disable n/no-process-exit */
33

4-
import { createLogger, parseCLIArgs, printHelpText, printVersion, runCMK, shouldBePretty } from '../dist/index.js';
4+
import {
5+
createLogger,
6+
parseCLIArgs,
7+
printHelpText,
8+
printVersion,
9+
runCMK,
10+
runCMKInWatchMode,
11+
shouldBePretty,
12+
} from '../dist/index.js';
513

614
const cwd = process.cwd();
715
let logger = createLogger(cwd, shouldBePretty(undefined));
@@ -18,9 +26,14 @@ try {
1826
process.exit(0);
1927
}
2028

21-
const success = await runCMK(args, logger);
22-
if (!success) {
23-
process.exit(1);
29+
if (args.watch) {
30+
const watcher = await runCMKInWatchMode(args, logger);
31+
process.on('SIGINT', () => watcher.close());
32+
} else {
33+
const success = await runCMK(args, logger);
34+
if (!success) {
35+
process.exit(1);
36+
}
2437
}
2538
} catch (e) {
2639
logger.logError(e);

packages/codegen/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { runCMK } from './runner.js';
1+
export { runCMK, runCMKInWatchMode } from './runner.js';
22
export { type Logger, createLogger } from './logger/logger.js';
33
export { WriteDtsFileError, ReadCSSModuleFileError } from './error.js';
44
export { parseCLIArgs, printHelpText, printVersion } from './cli.js';

packages/codegen/src/runner.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { rm } from 'node:fs/promises';
2+
import chokidar, { type FSWatcher } from 'chokidar';
23
import type { Logger } from './logger/logger.js';
34
import { createProject } from './project.js';
45

@@ -7,6 +8,10 @@ interface RunnerArgs {
78
clean: boolean;
89
}
910

11+
interface Watcher {
12+
close(): Promise<void>;
13+
}
14+
1015
/**
1116
* Run css-modules-kit .d.ts generation.
1217
* @param project The absolute path to the project directory or the path to `tsconfig.json`.
@@ -27,3 +32,108 @@ export async function runCMK(args: RunnerArgs, logger: Logger): Promise<boolean>
2732
}
2833
return true;
2934
}
35+
36+
/**
37+
* Run css-modules-kit .d.ts generation in watch mode.
38+
*
39+
* NOTE: For implementation simplicity, config file changes are not watched.
40+
* @param project The absolute path to the project directory or the path to `tsconfig.json`.
41+
* @throws {TsConfigFileNotFoundError}
42+
* @throws {ReadCSSModuleFileError}
43+
* @throws {WriteDtsFileError}
44+
*/
45+
export async function runCMKInWatchMode(args: RunnerArgs, logger: Logger): Promise<Watcher> {
46+
const fsWatchers: FSWatcher[] = [];
47+
const project = createProject(args);
48+
let emitAndReportDiagnosticsTimer: NodeJS.Timeout | undefined = undefined;
49+
50+
if (args.clean) {
51+
await rm(project.config.dtsOutDir, { recursive: true, force: true });
52+
}
53+
await emitAndReportDiagnostics();
54+
55+
// Watch project files and report diagnostics on changes
56+
for (const wildcardDirectory of project.config.wildcardDirectories) {
57+
fsWatchers.push(
58+
chokidar
59+
.watch(wildcardDirectory.fileName, {
60+
ignored: (fileName, stats) => {
61+
// The ignored function is called twice for the same path. The first time with stats undefined,
62+
// and the second time with stats provided.
63+
// In the first call, we can't determine if the path is a directory or file.
64+
// So we include it in the watch target considering it might be a directory.
65+
if (!stats) return false;
66+
67+
// In the second call, we include directories or files that match wildcards in the watch target.
68+
// However, `dtsOutDir` is excluded from the watch target.
69+
if (stats.isDirectory()) {
70+
return fileName === project.config.dtsOutDir;
71+
} else {
72+
return !project.isWildcardMatchedFile(fileName);
73+
}
74+
},
75+
ignoreInitial: true,
76+
...(wildcardDirectory.recursive ? {} : { depth: 0 }),
77+
awaitWriteFinish: true,
78+
})
79+
.on('add', (fileName) => {
80+
try {
81+
project.addFile(fileName);
82+
} catch (e) {
83+
logger.logError(e);
84+
return;
85+
}
86+
scheduleEmitAndReportDiagnostics();
87+
})
88+
.on('change', (fileName) => {
89+
try {
90+
project.updateFile(fileName);
91+
} catch (e) {
92+
logger.logError(e);
93+
return;
94+
}
95+
scheduleEmitAndReportDiagnostics();
96+
})
97+
.on('unlink', (fileName: string) => {
98+
project.removeFile(fileName);
99+
scheduleEmitAndReportDiagnostics();
100+
})
101+
.on('error', logger.logError.bind(logger)),
102+
);
103+
}
104+
105+
function scheduleEmitAndReportDiagnostics() {
106+
// Switching between git branches results in numerous file changes occurring rapidly.
107+
// Reporting diagnostics for each file change would overwhelm users.
108+
// Therefore, we batch the processing.
109+
110+
if (emitAndReportDiagnosticsTimer !== undefined) clearTimeout(emitAndReportDiagnosticsTimer);
111+
112+
emitAndReportDiagnosticsTimer = setTimeout(() => {
113+
emitAndReportDiagnosticsTimer = undefined;
114+
emitAndReportDiagnostics().catch(logger.logError.bind(logger));
115+
}, 250);
116+
}
117+
118+
/**
119+
* @throws {WriteDtsFileError}
120+
*/
121+
async function emitAndReportDiagnostics() {
122+
logger.clearScreen();
123+
await project.emitDtsFiles();
124+
const diagnostics = project.getDiagnostics();
125+
if (diagnostics.length > 0) {
126+
logger.logDiagnostics(diagnostics);
127+
}
128+
logger.logMessage(
129+
`Found ${diagnostics.length} error${diagnostics.length === 1 ? '' : 's'}. Watching for file changes.`,
130+
{ time: true },
131+
);
132+
}
133+
134+
async function close() {
135+
await Promise.all(fsWatchers.map(async (watcher) => watcher.close()));
136+
}
137+
138+
return { close };
139+
}

0 commit comments

Comments
 (0)