@@ -30,6 +30,7 @@ export async function* runEsBuildBuildAction(
3030 progress ?: boolean ;
3131 deleteOutputPath ?: boolean ;
3232 poll ?: number ;
33+ signal ?: AbortSignal ;
3334 } ,
3435) : AsyncIterable < ( ExecutionResult [ 'outputWithFiles' ] | ExecutionResult [ 'output' ] ) & BuilderOutput > {
3536 const {
@@ -75,75 +76,89 @@ export async function* runEsBuildBuildAction(
7576 let result : ExecutionResult ;
7677 try {
7778 result = await withProgress ( 'Building...' , ( ) => action ( ) ) ;
78-
79- if ( writeToFileSystem ) {
80- // Write output files
81- await writeResultFiles ( result . outputFiles , result . assetFiles , outputPath ) ;
82-
83- yield result . output ;
84- } else {
85- // Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
86- // eslint-disable-next-line @typescript-eslint/no-explicit-any
87- yield result . outputWithFiles as any ;
88- }
89-
90- // Finish if watch mode is not enabled
91- if ( ! watch ) {
92- return ;
93- }
9479 } finally {
9580 // Ensure Sass workers are shutdown if not watching
9681 if ( ! watch ) {
9782 shutdownSassWorkerPool ( ) ;
9883 }
9984 }
10085
101- if ( progress ) {
102- logger . info ( 'Watch mode enabled. Watching for file changes...' ) ;
86+ // Setup watcher if watch mode enabled
87+ let watcher : import ( '../../tools/esbuild/watcher' ) . BuildWatcher | undefined ;
88+ if ( watch ) {
89+ if ( progress ) {
90+ logger . info ( 'Watch mode enabled. Watching for file changes...' ) ;
91+ }
92+
93+ // Setup a watcher
94+ const { createWatcher } = await import ( '../../tools/esbuild/watcher' ) ;
95+ watcher = createWatcher ( {
96+ polling : typeof poll === 'number' ,
97+ interval : poll ,
98+ ignored : [
99+ // Ignore the output and cache paths to avoid infinite rebuild cycles
100+ outputPath ,
101+ cacheOptions . basePath ,
102+ // Ignore all node modules directories to avoid excessive file watchers.
103+ // Package changes are handled below by watching manifest and lock files.
104+ '**/node_modules/**' ,
105+ '**/.*/**' ,
106+ ] ,
107+ } ) ;
108+
109+ // Setup abort support
110+ options . signal ?. addEventListener ( 'abort' , ( ) => void watcher ?. close ( ) ) ;
111+
112+ // Temporarily watch the entire project
113+ watcher . add ( projectRoot ) ;
114+
115+ // Watch workspace for package manager changes
116+ const packageWatchFiles = [
117+ // manifest can affect module resolution
118+ 'package.json' ,
119+ // npm lock file
120+ 'package-lock.json' ,
121+ // pnpm lock file
122+ 'pnpm-lock.yaml' ,
123+ // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/)
124+ 'yarn.lock' ,
125+ '.pnp.cjs' ,
126+ '.pnp.data.json' ,
127+ ] ;
128+
129+ watcher . add ( packageWatchFiles . map ( ( file ) => path . join ( workspaceRoot , file ) ) ) ;
130+
131+ // Watch locations provided by the initial build result
132+ watcher . add ( result . watchFiles ) ;
103133 }
104134
105- // Setup a watcher
106- const { createWatcher } = await import ( '../../tools/esbuild/watcher' ) ;
107- const watcher = createWatcher ( {
108- polling : typeof poll === 'number' ,
109- interval : poll ,
110- ignored : [
111- // Ignore the output and cache paths to avoid infinite rebuild cycles
112- outputPath ,
113- cacheOptions . basePath ,
114- // Ignore all node modules directories to avoid excessive file watchers.
115- // Package changes are handled below by watching manifest and lock files.
116- '**/node_modules/**' ,
117- '**/.*/**' ,
118- ] ,
119- } ) ;
120-
121- // Temporarily watch the entire project
122- watcher . add ( projectRoot ) ;
123-
124- // Watch workspace for package manager changes
125- const packageWatchFiles = [
126- // manifest can affect module resolution
127- 'package.json' ,
128- // npm lock file
129- 'package-lock.json' ,
130- // pnpm lock file
131- 'pnpm-lock.yaml' ,
132- // yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/)
133- 'yarn.lock' ,
134- '.pnp.cjs' ,
135- '.pnp.data.json' ,
136- ] ;
137-
138- watcher . add ( packageWatchFiles . map ( ( file ) => path . join ( workspaceRoot , file ) ) ) ;
139-
140- // Watch locations provided by the initial build result
141- let previousWatchFiles = new Set ( result . watchFiles ) ;
142- watcher . add ( result . watchFiles ) ;
135+ // Output the first build results after setting up the watcher to ensure that any code executed
136+ // higher in the iterator call stack will trigger the watcher. This is particularly relevant for
137+ // unit tests which execute the builder and modify the file system programmatically.
138+ if ( writeToFileSystem ) {
139+ // Write output files
140+ await writeResultFiles ( result . outputFiles , result . assetFiles , outputPath ) ;
141+
142+ yield result . output ;
143+ } else {
144+ // Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
145+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146+ yield result . outputWithFiles as any ;
147+ }
148+
149+ // Finish if watch mode is not enabled
150+ if ( ! watcher ) {
151+ return ;
152+ }
143153
144154 // Wait for changes and rebuild as needed
155+ let previousWatchFiles = new Set ( result . watchFiles ) ;
145156 try {
146157 for await ( const changes of watcher ) {
158+ if ( options . signal ?. aborted ) {
159+ break ;
160+ }
161+
147162 if ( verbose ) {
148163 logger . info ( changes . toDebugString ( ) ) ;
149164 }
0 commit comments