1- /* eslint-disable @typescript-eslint/no-misused-promises */
21import type { App , Page } from '@vuepress/core'
32import { colors , logger , path , picomatch } from '@vuepress/utils'
43import type { FSWatcher } from 'chokidar'
@@ -10,10 +9,44 @@ import { handlePageUnlink } from './handlePageUnlink.js'
109import { createPageDepsHelper } from './pageDepsHelper.js'
1110import { processPagePatterns } from './processPagePatterns.js'
1211
12+ type PageEventType = 'add' | 'change' | 'unlink'
13+
14+ /**
15+ * Merge pending events into final operation.
16+ * Exported for testing purposes.
17+ */
18+ export const mergeEvents = ( events : PageEventType [ ] ) : PageEventType | null => {
19+ if ( events . length === 0 ) return null
20+
21+ if ( events . length === 1 ) return events [ 0 ]
22+
23+ const first = events [ 0 ]
24+ const last = events [ events . length - 1 ]
25+
26+ // add + ... + remove: nothing
27+ if ( first === 'add' && last === 'unlink' ) return null
28+
29+ if ( first === 'add' ) return 'add'
30+ if ( last === 'unlink' ) return 'unlink'
31+
32+ return 'change'
33+ }
34+
1335/**
14- * Watch page files and deps, return file watchers
36+ * Watch page files and deps, return file watchers and cleanup function
1537 */
16- export const watchPageFiles = ( app : App ) : FSWatcher [ ] => {
38+ export const watchPageFiles = (
39+ app : App ,
40+ ) : {
41+ watchers : FSWatcher [ ]
42+ cleanup : ( ) => Promise < void >
43+ } => {
44+ // Track pending events per page - just event types, no I/O
45+ const pendingEvents = new Map < string , PageEventType [ ] > ( )
46+
47+ // Track the last promise per page for serialization
48+ const pagePromises = new Map < string , Promise < void > > ( )
49+
1750 // watch page deps
1851 const depsWatcher = chokidar . watch ( [ ] , {
1952 ignoreInitial : true ,
@@ -27,13 +60,84 @@ export const watchPageFiles = (app: App): FSWatcher[] => {
2760 const depsToRemove = depsHelper . remove ( page )
2861 depsWatcher . unwatch ( depsToRemove )
2962 }
30- const depsListener = async ( dep : string ) : Promise < void > => {
63+
64+ // Process pending events for a page, merging them into one final operation
65+ const processPageEvents = async ( filePathRelative : string ) : Promise < void > => {
66+ // Get and clear pending events for this page
67+ const events = pendingEvents . get ( filePathRelative ) ?? [ ]
68+ pendingEvents . delete ( filePathRelative )
69+
70+ // Merge events into final operation
71+ const finalEvent = mergeEvents ( events )
72+ if ( ! finalEvent ) return
73+
74+ const filePath = app . dir . source ( filePathRelative )
75+
76+ if ( finalEvent === 'add' ) {
77+ logger . info ( `page ${ colors . magenta ( filePathRelative ) } is created` )
78+ const page = await handlePageAdd ( app , filePath )
79+ if ( page === null ) return
80+ addDeps ( page )
81+ return
82+ }
83+
84+ if ( finalEvent === 'change' ) {
85+ logger . info ( `page ${ colors . magenta ( filePathRelative ) } is modified` )
86+ const result = await handlePageChange ( app , filePath )
87+ if ( result === null ) return
88+ const [ pageOld , pageNew ] = result
89+ removeDeps ( pageOld )
90+ addDeps ( pageNew )
91+ return
92+ }
93+
94+ // finalEvent is 'unlink'
95+ logger . info ( `page ${ colors . magenta ( filePathRelative ) } is removed` )
96+ const page = await handlePageUnlink ( app , filePath )
97+ if ( page === null ) return
98+ removeDeps ( page )
99+ }
100+
101+ // Handle file events - just track them, no processing yet
102+ const pageEventHandler = (
103+ filePathRelative : string ,
104+ eventType : PageEventType ,
105+ ) : void => {
106+ // Add event to pending list
107+ let events = pendingEvents . get ( filePathRelative )
108+ if ( ! events ) pendingEvents . set ( filePathRelative , ( events = [ ] ) )
109+ events . push ( eventType )
110+
111+ // Chain to existing promise to ensure serialization
112+ const existingPromise =
113+ pagePromises . get ( filePathRelative ) ?? Promise . resolve ( )
114+ const newPromise = ( async ( ) => {
115+ await existingPromise
116+ try {
117+ await processPageEvents ( filePathRelative )
118+ } catch ( error ) {
119+ logger . error (
120+ `Error while processing page events for ${ colors . magenta ( filePathRelative ) } :` ,
121+ error ,
122+ )
123+ }
124+ } ) ( )
125+ // Only delete if this promise is still the current one (compare by identity)
126+ . finally ( ( ) => {
127+ if ( pagePromises . get ( filePathRelative ) === newPromise )
128+ pagePromises . delete ( filePathRelative )
129+ } )
130+ pagePromises . set ( filePathRelative , newPromise )
131+ }
132+
133+ // When a dependency changes, find all pages that depend on it and trigger change event for them
134+ const depsListener = ( dep : string ) : void => {
31135 const pagePaths = depsHelper . get ( dep )
32136 for ( const filePathRelative of pagePaths ) {
33137 logger . info (
34138 `dependency of page ${ colors . magenta ( filePathRelative ) } is modified` ,
35139 )
36- await handlePageChange ( app , app . dir . source ( filePathRelative ) )
140+ pageEventHandler ( filePathRelative , 'change' )
37141 }
38142 }
39143 depsWatcher . on ( 'add' , depsListener )
@@ -72,26 +176,29 @@ export const watchPageFiles = (app: App): FSWatcher[] => {
72176 } ,
73177 ignoreInitial : true ,
74178 } )
75- pagesWatcher . on ( 'add' , async ( filePathRelative ) => {
76- logger . info ( `page ${ colors . magenta ( filePathRelative ) } is created` )
77- const page = await handlePageAdd ( app , app . dir . source ( filePathRelative ) )
78- if ( page === null ) return
79- addDeps ( page )
179+
180+ pagesWatcher . on ( 'add' , ( filePathRelative ) => {
181+ pageEventHandler ( filePathRelative , 'add' )
80182 } )
81- pagesWatcher . on ( 'change' , async ( filePathRelative ) => {
82- logger . info ( `page ${ colors . magenta ( filePathRelative ) } is modified` )
83- const result = await handlePageChange ( app , app . dir . source ( filePathRelative ) )
84- if ( result === null ) return
85- const [ pageOld , pageNew ] = result
86- removeDeps ( pageOld )
87- addDeps ( pageNew )
183+ pagesWatcher . on ( 'change' , ( filePathRelative ) => {
184+ pageEventHandler ( filePathRelative , 'change' )
88185 } )
89- pagesWatcher . on ( 'unlink' , async ( filePathRelative ) => {
90- logger . info ( `page ${ colors . magenta ( filePathRelative ) } is removed` )
91- const page = await handlePageUnlink ( app , app . dir . source ( filePathRelative ) )
92- if ( page === null ) return
93- removeDeps ( page )
186+ pagesWatcher . on ( 'unlink' , ( filePathRelative ) => {
187+ pageEventHandler ( filePathRelative , 'unlink' )
94188 } )
95189
96- return [ pagesWatcher , depsWatcher ]
190+ // cancel queued page events, wait for in-flight operations to finish, and reset
191+ const cleanup = async ( ) : Promise < void > => {
192+ // clear pending events
193+ pendingEvents . clear ( )
194+ // wait for all pending page operations to finish
195+ await Promise . all ( pagePromises . values ( ) )
196+ // clear pending promises
197+ pagePromises . clear ( )
198+ }
199+
200+ return {
201+ watchers : [ pagesWatcher , depsWatcher ] ,
202+ cleanup,
203+ }
97204}
0 commit comments