@@ -5,8 +5,14 @@ import killPort from 'kill-port';
55import nodeFs from 'node:fs' ;
66import nodePath from 'node:path' ;
77import { afterAll , describe , test } from 'vitest' ;
8- import { CONFIG } from './src/config' ;
9- import { isDirectoryExists , removeDirSync , sensibleTimeoutInMs } from './src/utils' ;
8+ import { isDirectoryExists , removeDirSync } from './src/utils' ;
9+ import {
10+ getBuildSeq ,
11+ getModuleRegistrationSeq ,
12+ waitForBuildStable ,
13+ waitForModuleRegistration ,
14+ waitForNextBuild ,
15+ } from './test-utils' ;
1016
1117function main ( ) {
1218 const fixturesPath = nodePath . resolve ( __dirname , 'fixtures' ) ;
@@ -82,11 +88,20 @@ function main() {
8288
8389 await waitForPathExists ( nodeScriptPath ) ;
8490
85- let runningArtifactProcess = await runArtifactProcess ( nodeScriptPath , tmpProjectPath ) ;
91+ let runningArtifactProcess = await runArtifactProcess ( nodeScriptPath , tmpProjectPath , port ) ;
8692
8793 const hmrEditFiles = await collectHmrEditFiles ( tmpProjectPath ) ;
8894
95+ // Wait for the initial build to stabilize
96+ await waitForBuildStable ( port ) ;
97+
8998 for ( const [ index , [ step , hmrEdits ] ] of hmrEditFiles . entries ( ) ) {
99+ // Wait for the previous build's debounce window to close so the
100+ // watcher treats the next file write as a new change.
101+ if ( index !== 0 ) {
102+ await waitForBuildStable ( port ) ;
103+ }
104+
90105 console . log (
91106 `🔄 Processing HMR edit files for step ${ step } with edits: ${ JSON . stringify (
92107 hmrEdits ,
@@ -95,17 +110,6 @@ function main() {
95110 ) } `,
96111 ) ;
97112
98- // Refer to `packages/test-dev-server/src/utils/get-dev-watch-options-for-ci.ts`
99- // We used a poll-based and debounced watcher in CI, so we need to wait for certain amount of time to
100- // - Make sure different steps are not debounced together
101- // - Make sure changes are detected individually for different steps
102- // - Make sure changes in the same step are detected together
103- if ( index !== 0 ) {
104- await sensibleTimeoutInMs (
105- CONFIG . watch . debounceDuration + CONFIG . watch . debounceTickRate + 100 ,
106- ) ;
107- }
108-
109113 const hmrEditsWithContent = hmrEdits . map ( ( e ) => ( {
110114 ...e ,
111115 content : nodeFs . readFileSync ( e . replacementPath , 'utf-8' ) ,
@@ -119,6 +123,9 @@ function main() {
119123 currentArtifactContent = nodeFs . readFileSync ( nodeScriptPath ) ;
120124 }
121125
126+ // Snapshot buildSeq before writing so we can detect the resulting build
127+ const preWriteBuildSeq = await getBuildSeq ( port ) ;
128+
122129 for ( const hmrEdit of hmrEditsWithContent ) {
123130 console . log ( `🔄 Writing content to: ${ hmrEdit . targetPath } ` ) ;
124131 nodeFs . writeFileSync ( hmrEdit . targetPath , hmrEdit . content ) ;
@@ -127,16 +134,20 @@ function main() {
127134 console . log ( `⏳ Waiting for HMR to be triggered for step ${ step } ` ) ;
128135
129136 if ( needRestart || needReload ) {
130- // Waiting Reload hmr update to be triggered. If we close the process too fast, dev engine will think there're no clients.
131- // No hmr update will be triggered.
132- await sensibleTimeoutInMs ( 2000 ) ;
133137 if ( needReload ) {
138+ // For reload steps, send the 'r' signal to request a rebuild, which
139+ // calls ensureLatestBuildOutput only when the output is stale.
140+ // We don't rely on the watcher since the edited file may be new
141+ // and not yet in the build graph.
134142 console . log ( `🏃➡️ Sent rebuild message to the dev server` ) ;
135143 devServeProcess . stdin . write ( 'r' ) ;
144+ } else {
145+ // For restart steps (no reload), wait for the watcher-triggered build.
146+ await waitForNextBuild ( port , preWriteBuildSeq ) ;
136147 }
137148 await runningArtifactProcess . close ( ) ;
138149 await waitForFileToBeModified ( nodeScriptPath , currentArtifactContent ) ;
139- runningArtifactProcess = await runArtifactProcess ( nodeScriptPath , tmpProjectPath ) ;
150+ runningArtifactProcess = await runArtifactProcess ( nodeScriptPath , tmpProjectPath , port ) ;
140151 }
141152 await waitForPathExists ( nodePath . join ( tmpProjectPath , `ok-${ index } ` ) , 10 * 1000 ) ;
142153 console . log ( `✅ HMR triggered for step ${ step } ` ) ;
@@ -161,7 +172,7 @@ function main() {
161172
162173let id = 0 ;
163174
164- async function runArtifactProcess ( artifactPath : string , tmpProjectPath : string ) {
175+ async function runArtifactProcess ( artifactPath : string , tmpProjectPath : string , port : number ) {
165176 const thisId = id ;
166177 id ++ ;
167178
@@ -173,6 +184,9 @@ async function runArtifactProcess(artifactPath: string, tmpProjectPath: string)
173184 ` . trim ( ) ,
174185 ) ;
175186
187+ // Snapshot registered clients before starting the process
188+ const currentRegistered = await getModuleRegistrationSeq ( port ) ;
189+
176190 console . log ( `🔄 Starting Node.js process: ${ artifactPath } ` ) ;
177191 const artifactProcess = execa (
178192 'node' ,
@@ -183,7 +197,8 @@ async function runArtifactProcess(artifactPath: string, tmpProjectPath: string)
183197 // Wait for the Node.js process to start
184198 await waitForPathExists ( initOkFilePath ) ;
185199
186- await sensibleTimeoutInMs ( 2000 ) ; // Make sure module are registered
200+ // Wait for modules to be registered with the dev server
201+ await waitForModuleRegistration ( port , currentRegistered ) ;
187202
188203 return {
189204 process : artifactProcess ,
0 commit comments