66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { existsSync } from 'fs' ;
10- import { join } from 'path' ;
9+ import { existsSync , readFileSync } from 'fs' ;
10+ import { dirname , join } from 'path' ;
1111import semver from 'semver' ;
1212import { ChildProcess , SpawnResult , SpawnOptions } from '../../utils/child-process.js' ;
1313import { Spinner } from '../../utils/spinner.js' ;
@@ -193,6 +193,10 @@ export abstract class ExternalCommands {
193193 /**
194194 * Invokes the `nvm install` command in order to install the correct Node.js version
195195 * as specified in a `.nvmrc` file, if present.
196+ *
197+ * We then run `nvm install` to install the correct version of node. We then run
198+ * `nvm which` to get the path to the node binary, and we update the PATH
199+ * environment variable to include this new node binary location.
196200 */
197201 static async invokeNvmInstall ( projectDir : string ) : Promise < void > ;
198202 static async invokeNvmInstall ( projectDir : string , quiet : boolean ) : Promise < void > ;
@@ -202,20 +206,50 @@ export abstract class ExternalCommands {
202206 }
203207
204208 try {
209+ const nodeVersionFromNvmrc = readFileSync ( join ( projectDir , '.nvmrc' ) , 'utf8' ) . trim ( ) ;
210+
205211 // We must source nvm.sh so the shell recognizes the 'nvm' command since nvm is not a binary
206212 // but a shell function. The dot (.) built-in and && operator require shell: true here.
207- await ChildProcess . spawn ( '. ~/.nvm/nvm.sh && nvm install' , [ ] , {
213+ // We redirect stdout of nvm install to stderr so that stdout only contains the result of nvm which.
214+ const { stdout} = await ChildProcess . spawn (
215+ '. ~/.nvm/nvm.sh && nvm install >&2 && nvm which' ,
216+ [ ] ,
217+ {
218+ cwd : projectDir ,
219+ mode : 'on-error' ,
220+ shell : true ,
221+ } ,
222+ ) ;
223+
224+ const nodeBinPath = stdout . trim ( ) ;
225+ if ( nodeBinPath ) {
226+ const nodeDir = dirname ( nodeBinPath ) ;
227+ const currentPath = process . env [ 'PATH' ] || '' ;
228+ const pathParts = currentPath . split ( ':' ) ;
229+
230+ // Only update if the requested node directory is not already the first entry in PATH.
231+ if ( pathParts [ 0 ] !== nodeDir ) {
232+ const filteredParts = pathParts . filter ( ( p ) => p !== nodeDir ) ;
233+ process . env [ 'PATH' ] = [ nodeDir , ...filteredParts ] . join ( ':' ) ;
234+ Log . debug ( `Updated process.env.PATH to include ${ nodeDir } ` ) ;
235+ }
236+ }
237+
238+ const { stdout : nodeVersionOutput } = await ChildProcess . spawn ( 'node' , [ '--version' ] , {
239+ mode : 'silent' ,
208240 cwd : projectDir ,
209- mode : 'on-error' ,
210- shell : true ,
211241 } ) ;
242+ const detectedNodeVersion = nodeVersionOutput . trim ( ) ;
243+
244+ if ( ! semver . satisfies ( detectedNodeVersion , nodeVersionFromNvmrc ) ) {
245+ Log . error ( ` ✘ Node.js version mismatch after update.\n` ) ;
246+ Log . error ( ` Expected version: ${ nodeVersionFromNvmrc } ` ) ;
247+ Log . error ( ` Actual version: ${ detectedNodeVersion } ` ) ;
248+ throw new FatalReleaseActionError ( ) ;
249+ }
212250
213251 if ( ! quiet ) {
214- const { stdout : nodeVersion } = await ChildProcess . spawn ( 'node' , [ '--version' ] , {
215- mode : 'silent' ,
216- cwd : projectDir ,
217- } ) ;
218- Log . info ( green ( ` ✓ Set node version to ${ nodeVersion } .` ) ) ;
252+ Log . info ( green ( ` ✓ Set node version to ${ detectedNodeVersion } .` ) ) ;
219253 }
220254 } catch ( e ) {
221255 Log . error ( e ) ;
0 commit comments