1+ import type { Stats } from 'node:fs' ;
12import { rm } from 'node:fs/promises' ;
3+ import chokidar , { type FSWatcher } from 'chokidar' ;
24import type { Logger } from './logger/logger.js' ;
35import { createProject } from './project.js' ;
46
@@ -7,6 +9,10 @@ interface RunnerArgs {
79 clean : boolean ;
810}
911
12+ interface Watcher {
13+ close ( ) : Promise < void > ;
14+ }
15+
1016/**
1117 * Run css-modules-kit .d.ts generation.
1218 * @param project The absolute path to the project directory or the path to `tsconfig.json`.
@@ -27,3 +33,108 @@ export async function runCMK(args: RunnerArgs, logger: Logger): Promise<boolean>
2733 }
2834 return true ;
2935}
36+
37+ /**
38+ * Run css-modules-kit .d.ts generation in watch mode.
39+ *
40+ * NOTE: For implementation simplicity, config file changes are not watched.
41+ * @param project The absolute path to the project directory or the path to `tsconfig.json`.
42+ * @throws {TsConfigFileNotFoundError }
43+ * @throws {ReadCSSModuleFileError }
44+ * @throws {WriteDtsFileError }
45+ */
46+ export async function runCMKInWatchMode ( args : RunnerArgs , logger : Logger ) : Promise < Watcher > {
47+ const fsWatchers : FSWatcher [ ] = [ ] ;
48+ const project = createProject ( args ) ;
49+ let emitAndReportDiagnosticsTimer : NodeJS . Timeout | undefined = undefined ;
50+
51+ if ( args . clean ) {
52+ await rm ( project . config . dtsOutDir , { recursive : true , force : true } ) ;
53+ }
54+ await emitAndReportDiagnostics ( ) ;
55+
56+ // Watch project files and report diagnostics on changes
57+ for ( const wildcardDirectory of project . config . wildcardDirectories ) {
58+ fsWatchers . push (
59+ chokidar
60+ . watch ( wildcardDirectory . fileName , {
61+ ignored : ( fileName : string , stats ?: 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+ ...( wildcardDirectory . recursive ? { } : { depth : 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