11import { rm } from 'node:fs/promises' ;
2+ import chokidar , { type FSWatcher } from 'chokidar' ;
23import type { Logger } from './logger/logger.js' ;
34import { 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