@@ -14,7 +14,7 @@ import * as path from 'path';
14
14
import * as vscode from 'vscode' ;
15
15
import { DisposableStore } from './disposable' ;
16
16
import { HumanError } from './errors' ;
17
- import { getPathToNode } from './node' ;
17
+ import { getPathToNode , isNvmInstalled } from './node' ;
18
18
19
19
type OptionsModule = {
20
20
loadOptions ( ) : IResolvedConfiguration ;
@@ -41,6 +41,7 @@ export class ConfigurationFile implements vscode.Disposable {
41
41
private _optionsModule ?: OptionsModule ;
42
42
private _configModule ?: ConfigModule ;
43
43
private _pathToMocha ?: string ;
44
+ private _pathToNvmRc ?: string ;
44
45
45
46
/** Cached read promise, invalided on file change. */
46
47
private readPromise ?: Promise < ConfigurationList > ;
@@ -140,14 +141,23 @@ export class ConfigurationFile implements vscode.Disposable {
140
141
141
142
async getMochaSpawnArgs ( customArgs : readonly string [ ] ) : Promise < string [ ] > {
142
143
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
+ }
143
159
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 ] ;
151
161
}
152
162
153
163
private getResolver ( ) {
@@ -179,6 +189,42 @@ export class ConfigurationFile implements vscode.Disposable {
179
189
throw new HumanError ( `Could not find node_modules above '${ mocha } '` ) ;
180
190
}
181
191
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
+
182
228
private async _resolveLocalMochaBinPath ( ) : Promise < string > {
183
229
try {
184
230
const packageJsonPath = await this . _resolveLocalMochaPath ( '/package.json' ) ;
@@ -193,17 +239,21 @@ export class ConfigurationFile implements vscode.Disposable {
193
239
// ignore
194
240
}
195
241
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' ) ;
197
243
return await this . _resolveLocalMochaPath ( '/bin/mocha.js' ) ;
198
244
}
199
245
200
246
private _resolveLocalMochaPath ( suffix : string = '' ) : Promise < string > {
247
+ return this . _resolve ( `mocha${ suffix } ` ) ;
248
+ }
249
+
250
+ private _resolve ( request : string ) : Promise < string > {
201
251
return new Promise < string > ( ( resolve , reject ) => {
202
252
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 ) => {
205
255
if ( err ) {
206
- this . logChannel . error ( `resolving 'mocha ${ suffix } ' failed with error ${ err } ` ) ;
256
+ this . logChannel . error ( `resolving '${ request } ' failed with error ${ err } ` ) ;
207
257
reject (
208
258
new HumanError (
209
259
`Could not find mocha in working directory '${ path . dirname (
@@ -212,7 +262,7 @@ export class ConfigurationFile implements vscode.Disposable {
212
262
) ,
213
263
) ;
214
264
} else {
215
- this . logChannel . debug ( `'mocha ${ suffix } ' resolved to '${ res } '` ) ;
265
+ this . logChannel . debug ( `'${ request } ' resolved to '${ res } '` ) ;
216
266
resolve ( res as string ) ;
217
267
}
218
268
} ) ;
0 commit comments