@@ -14,7 +14,7 @@ import * as path from 'path';
1414import * as vscode from 'vscode' ;
1515import { DisposableStore } from './disposable' ;
1616import { HumanError } from './errors' ;
17- import { getPathToNode } from './node' ;
17+ import { getPathToNode , isNvmInstalled } from './node' ;
1818
1919type OptionsModule = {
2020 loadOptions ( ) : IResolvedConfiguration ;
@@ -41,6 +41,7 @@ export class ConfigurationFile implements vscode.Disposable {
4141 private _optionsModule ?: OptionsModule ;
4242 private _configModule ?: ConfigModule ;
4343 private _pathToMocha ?: string ;
44+ private _pathToNvmRc ?: string ;
4445
4546 /** Cached read promise, invalided on file change. */
4647 private readPromise ?: Promise < ConfigurationList > ;
@@ -140,14 +141,23 @@ export class ConfigurationFile implements vscode.Disposable {
140141
141142 async getMochaSpawnArgs ( customArgs : readonly string [ ] ) : Promise < string [ ] > {
142143 this . _pathToMocha ??= await this . _resolveLocalMochaBinPath ( ) ;
144+ this . _pathToNvmRc ??= await this . _resolveNvmRc ( ) ;
145+
146+ let nodeSpawnArgs : string [ ] ;
147+ if (
148+ this . _pathToNvmRc &&
149+ ( await fs . promises
150+ . access ( this . _pathToNvmRc )
151+ . then ( ( ) => true )
152+ . catch ( ( ) => false ) )
153+ ) {
154+ nodeSpawnArgs = [ 'nvm' , 'run' ] ;
155+ } else {
156+ this . _pathToNvmRc = undefined ;
157+ nodeSpawnArgs = [ await getPathToNode ( this . logChannel ) ] ;
158+ }
143159
144- return [
145- await getPathToNode ( this . logChannel ) ,
146- this . _pathToMocha ,
147- '--config' ,
148- this . uri . fsPath ,
149- ...customArgs ,
150- ] ;
160+ return [ ...nodeSpawnArgs , this . _pathToMocha , '--config' , this . uri . fsPath , ...customArgs ] ;
151161 }
152162
153163 private getResolver ( ) {
@@ -179,6 +189,42 @@ export class ConfigurationFile implements vscode.Disposable {
179189 throw new HumanError ( `Could not find node_modules above '${ mocha } '` ) ;
180190 }
181191
192+ private async _resolveNvmRc ( ) : Promise < string | undefined > {
193+ // the .nvmrc file can be placed in any location up the directory tree, so we do the same
194+ // starting from the mocha config file
195+ // https://github.com/nvm-sh/nvm/blob/06413631029de32cd9af15b6b7f6ed77743cbd79/nvm.sh#L475-L491
196+ try {
197+ if ( ! ( await isNvmInstalled ( ) ) ) {
198+ return undefined ;
199+ }
200+
201+ let dir : string | undefined = path . dirname ( this . uri . fsPath ) ;
202+
203+ while ( dir ) {
204+ const nvmrc = path . join ( dir , '.nvmrc' ) ;
205+ if (
206+ await fs . promises
207+ . access ( nvmrc )
208+ . then ( ( ) => true )
209+ . catch ( ( ) => false )
210+ ) {
211+ this . logChannel . debug ( `Found .nvmrc at ${ nvmrc } ` ) ;
212+ return nvmrc ;
213+ }
214+
215+ const parent = path . dirname ( dir ) ;
216+ if ( parent === dir ) {
217+ break ;
218+ }
219+ dir = parent ;
220+ }
221+ } catch ( e ) {
222+ this . logChannel . error ( e as Error , 'Error while searching for nvmrc' ) ;
223+ }
224+
225+ return undefined ;
226+ }
227+
182228 private async _resolveLocalMochaBinPath ( ) : Promise < string > {
183229 try {
184230 const packageJsonPath = await this . _resolveLocalMochaPath ( '/package.json' ) ;
@@ -193,17 +239,21 @@ export class ConfigurationFile implements vscode.Disposable {
193239 // ignore
194240 }
195241
196- this . logChannel . warn ( 'Could not resolve mocha bin path from package.json, fallback to default' ) ;
242+ this . logChannel . info ( 'Could not resolve mocha bin path from package.json, fallback to default' ) ;
197243 return await this . _resolveLocalMochaPath ( '/bin/mocha.js' ) ;
198244 }
199245
200246 private _resolveLocalMochaPath ( suffix : string = '' ) : Promise < string > {
247+ return this . _resolve ( `mocha${ suffix } ` ) ;
248+ }
249+
250+ private _resolve ( request : string ) : Promise < string > {
201251 return new Promise < string > ( ( resolve , reject ) => {
202252 const dir = path . dirname ( this . uri . fsPath ) ;
203- this . logChannel . debug ( `resolving 'mocha ${ suffix } ' via ${ dir } ` ) ;
204- this . getResolver ( ) . resolve ( { } , dir , 'mocha' + suffix , { } , ( err , res ) => {
253+ this . logChannel . debug ( `resolving '${ request } ' via ${ dir } ` ) ;
254+ this . getResolver ( ) . resolve ( { } , dir , request , { } , ( err , res ) => {
205255 if ( err ) {
206- this . logChannel . error ( `resolving 'mocha ${ suffix } ' failed with error ${ err } ` ) ;
256+ this . logChannel . error ( `resolving '${ request } ' failed with error ${ err } ` ) ;
207257 reject (
208258 new HumanError (
209259 `Could not find mocha in working directory '${ path . dirname (
@@ -212,7 +262,7 @@ export class ConfigurationFile implements vscode.Disposable {
212262 ) ,
213263 ) ;
214264 } else {
215- this . logChannel . debug ( `'mocha ${ suffix } ' resolved to '${ res } '` ) ;
265+ this . logChannel . debug ( `'${ request } ' resolved to '${ res } '` ) ;
216266 resolve ( res as string ) ;
217267 }
218268 } ) ;
0 commit comments