Skip to content

Commit 869ea8e

Browse files
committed
feat: implement watch mode
1 parent a918172 commit 869ea8e

File tree

4 files changed

+134
-5
lines changed

4 files changed

+134
-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: 111 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,109 @@ 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+
// @ts-expect-error -- allow undefined depth
60+
.watch(wildcardDirectory.fileName, {
61+
ignored: (fileName, stats) => {
62+
// The ignored function is called twice for the same path. The first time with stats undefined,
63+
// and the second time with stats provided.
64+
// In the first call, we can't determine if the path is a directory or file.
65+
// So we include it in the watch target considering it might be a directory.
66+
if (!stats) return false;
67+
68+
// In the second call, we include directories or files that match wildcards in the watch target.
69+
// However, `dtsOutDir` is excluded from the watch target.
70+
if (stats.isDirectory()) {
71+
return fileName === project.config.dtsOutDir;
72+
} else {
73+
return !project.isWildcardMatchedFile(fileName);
74+
}
75+
},
76+
ignoreInitial: true,
77+
depth: wildcardDirectory.recursive ? undefined : 0,
78+
awaitWriteFinish: true,
79+
})
80+
.on('add', (fileName) => {
81+
try {
82+
project.addFile(fileName);
83+
} catch (e) {
84+
logger.logError(e);
85+
return;
86+
}
87+
scheduleEmitAndReportDiagnostics();
88+
})
89+
.on('change', (fileName) => {
90+
try {
91+
project.updateFile(fileName);
92+
} catch (e) {
93+
logger.logError(e);
94+
return;
95+
}
96+
scheduleEmitAndReportDiagnostics();
97+
})
98+
.on('unlink', (fileName: string) => {
99+
project.removeFile(fileName);
100+
scheduleEmitAndReportDiagnostics();
101+
})
102+
.on('error', logger.logError.bind(logger)),
103+
);
104+
}
105+
106+
function scheduleEmitAndReportDiagnostics() {
107+
// Switching between git branches results in numerous file changes occurring rapidly.
108+
// Reporting diagnostics for each file change would overwhelm users.
109+
// Therefore, we batch the processing.
110+
111+
if (emitAndReportDiagnosticsTimer !== undefined) clearTimeout(emitAndReportDiagnosticsTimer);
112+
113+
emitAndReportDiagnosticsTimer = setTimeout(() => {
114+
emitAndReportDiagnosticsTimer = undefined;
115+
emitAndReportDiagnostics().catch(logger.logError.bind(logger));
116+
}, 250);
117+
}
118+
119+
/**
120+
* @throws {WriteDtsFileError}
121+
*/
122+
async function emitAndReportDiagnostics() {
123+
logger.clearScreen();
124+
await project.emitDtsFiles();
125+
const diagnostics = project.getDiagnostics();
126+
if (diagnostics.length > 0) {
127+
logger.logDiagnostics(diagnostics);
128+
}
129+
logger.logMessage(
130+
`Found ${diagnostics.length} error${diagnostics.length === 1 ? '' : 's'}. Watching for file changes.`,
131+
{ time: true },
132+
);
133+
}
134+
135+
async function close() {
136+
await Promise.all(fsWatchers.map(async (watcher) => watcher.close()));
137+
}
138+
139+
return { close };
140+
}

0 commit comments

Comments
 (0)