Skip to content

Commit 7e7a3f3

Browse files
committed
feat: Respect nvmrc for Node version
1 parent 8859e2a commit 7e7a3f3

File tree

9 files changed

+174
-29
lines changed

9 files changed

+174
-29
lines changed

.github/workflows/build.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,28 @@ jobs:
2727
- name: Checkout
2828
uses: actions/checkout@v4
2929

30-
- name: Install Node.js
30+
- name: Install Node.js
31+
if: runner.os != 'Linux'
3132
uses: actions/setup-node@v4
3233
with:
3334
node-version: lts/*
3435

36+
- name: Install Node.js via NVM (Linux)
37+
if: runner.os == 'Linux'
38+
shell: bash
39+
run: |
40+
export NVM_DIR="$HOME/.nvm"
41+
source "$NVM_DIR/nvm.sh"
42+
echo $NVM_DIR >> $GITHUB_PATH
43+
echo "NVM_DIR=$NVM_DIR" >> $GITHUB_ENV
44+
nvm install $(cat test-workspaces/nvm/.nvmrc)
45+
nvm install lts/*
46+
3547
- name: Install dependencies
36-
run: npm install
48+
run: |
49+
node --version
50+
npm --version
51+
npm install
3752
3853
- name: Compile
3954
run: npm run compile:test
@@ -70,11 +85,12 @@ jobs:
7085
if: always()
7186
run: npm run lint
7287

73-
- uses: dorny/test-reporter@v1
88+
- uses: dorny/test-reporter@1a288b62f8b75c0f433cbfdbc2e4800fbae50bd7
7489
if: ${{ (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository }}
7590
with:
7691
name: VS Code Test Results (${{matrix.os}}, ${{matrix.vscode-version}}, ${{matrix.vscode-platform}})
7792
path: 'test-results/*.json'
93+
use-actions-summary: 'true'
7894
reporter: mocha-json
7995

8096
- uses: actions/upload-artifact@v4

.vscode-test.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as fs from 'fs';
33
import * as path from 'path';
44
import { fileURLToPath } from 'url';
55

6-
76
const dirname = fileURLToPath(new URL('.', import.meta.url));
87
const integrationTestDir = path.join(dirname, 'out/test/integration');
98
const workspaceBaseDir = path.join(dirname, 'test-workspaces');
@@ -15,8 +14,8 @@ let extensionDevelopmentPath = '';
1514

1615
const testMode = process.env.TEST_MODE ?? 'normal';
1716

17+
const tempDir = process.env.TEST_TEMP ?? path.join(dirname, 'tmp');
1818
if (testMode === 'vsix') {
19-
const tempDir = process.env.TEST_TEMP ?? path.join(dirname, 'tmp')
2019
extensionDevelopmentPath = path.resolve(path.join(tempDir, 'vsix', 'extension'));
2120
}
2221

@@ -27,6 +26,7 @@ function createCommonOptions(label) {
2726
version: vsCodeVersion,
2827
env: {
2928
MOCHA_VSCODE_TEST: 'true',
29+
TEST_TEMP: tempDir,
3030
},
3131
mocha: {
3232
ui: 'bdd',
@@ -48,7 +48,6 @@ function createCommonOptions(label) {
4848
options.extensionDevelopmentPath = extensionDevelopmentPath;
4949
}
5050

51-
5251
return options;
5352
}
5453

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@
203203
"esbuild": "^0.24.2"
204204
},
205205
"mocha-vscode": {
206-
"version": "v1.2.3+d27e65f",
207-
"date": "2024-10-20T15:38:24.261Z"
206+
"version": "v1.2.4+9a52d28",
207+
"date": "2025-02-02T11:56:15.601Z"
208208
}
209-
}
209+
}

src/configurationFile.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import * as path from 'path';
1414
import * as vscode from 'vscode';
1515
import { DisposableStore } from './disposable';
1616
import { HumanError } from './errors';
17-
import { getPathToNode } from './node';
17+
import { getPathToNode, isNvmInstalled } from './node';
1818

1919
type 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
});

src/node.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
* https://opensource.org/licenses/MIT.
88
*/
99

10+
import fs from 'fs';
11+
import { homedir } from 'os';
12+
import path from 'path';
1013
import * as vscode from 'vscode';
1114
import which from 'which';
1215

13-
export async function getPathTo(logChannel: vscode.LogOutputChannel, bin: string, name: string) {
16+
async function getPathTo(logChannel: vscode.LogOutputChannel, bin: string, name: string) {
1417
logChannel.debug(`Resolving ${name} executable`);
1518
let pathToBin = await which(bin, { nothrow: true });
1619
if (pathToBin) {
@@ -34,11 +37,14 @@ export async function getPathToNode(logChannel: vscode.LogOutputChannel) {
3437
return pathToNode;
3538
}
3639

37-
let pathToNpm: string | null = null;
38-
39-
export async function getPathToNpm(logChannel: vscode.LogOutputChannel) {
40-
if (!pathToNpm) {
41-
pathToNpm = await getPathTo(logChannel, 'npm', 'NPM');
40+
export async function isNvmInstalled() {
41+
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L27
42+
const nvmDir = process.env.NVM_DIR || homedir();
43+
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L143
44+
try {
45+
await fs.promises.access(path.join(nvmDir, '.nvm', '.git'));
46+
return true;
47+
} catch (e) {
48+
return false;
4249
}
43-
return pathToNpm;
4450
}

src/test/integration/nvm.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (C) Daniel Kuschny (Danielku15) and contributors.
3+
* Copyright (C) Microsoft Corporation. All rights reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style
6+
* license that can be found in the LICENSE file or at
7+
* https://opensource.org/licenses/MIT.
8+
*/
9+
10+
import { expect } from 'chai';
11+
import fs from 'fs';
12+
import os from 'os';
13+
import path from 'path';
14+
import * as vscode from 'vscode';
15+
import { isNvmInstalled } from '../../node';
16+
import { captureTestRun, expectTestTree, getController, integrationTestPrepare } from '../util';
17+
18+
describe('nvm', () => {
19+
const workingDir = integrationTestPrepare('nvm');
20+
21+
it('discovers tests', async () => {
22+
const c = await getController();
23+
24+
expectTestTree(c, [['nvm.test.js', [['nvm', [['ensure-version']]]]]]);
25+
});
26+
27+
it('runs tests', async () => {
28+
const c = await getController();
29+
const profiles = c.profiles;
30+
expect(profiles).to.have.lengthOf(2);
31+
32+
const run = await captureTestRun(
33+
c,
34+
new vscode.TestRunRequest(
35+
undefined,
36+
undefined,
37+
profiles.find((p) => p.kind === vscode.TestRunProfileKind.Run),
38+
),
39+
);
40+
41+
run.expectStates({
42+
'nvm.test.js/nvm/ensure-version': ['enqueued', 'started', 'passed'],
43+
});
44+
45+
const expectedVersion = await fs.promises.readFile(path.join(workingDir, '.nvmrc'), 'utf-8');
46+
const actualVersion = await fs.promises.readFile(
47+
path.resolve(__dirname, '..', '..', '..', 'tmp', '.nvmrc-actual'),
48+
'utf-8',
49+
);
50+
51+
// nvm is only available on MacOS and Linux
52+
// so we skip it on windows.
53+
// also if NVM on local development we skip this test (for GITHUB_ACTIONS we expect it to be there).
54+
const shouldRun =
55+
os.platform() === 'linux' && ((await isNvmInstalled()) || process.env.GITHUB_ACTIONS);
56+
console.log(`Expecting node ${expectedVersion}, ran in ${actualVersion}`);
57+
if (shouldRun) {
58+
expect(process.version).to.match(new RegExp(expectedVersion + '.*'));
59+
}
60+
});
61+
});

test-workspaces/nvm/.mocharc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
spec: '**/*.test.js'
3+
};

test-workspaces/nvm/.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v20

test-workspaces/nvm/nvm.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { writeFileSync, mkdirSync } = require('node:fs');
2+
const { join } = require('node:path');
3+
4+
describe('nvm', () => {
5+
it('ensure-version', () => {
6+
mkdirSync(process.env.TEST_TEMP, { recursive: true });
7+
writeFileSync(join(process.env.TEST_TEMP, '.nvmrc-actual'), process.version);
8+
});
9+
});

0 commit comments

Comments
 (0)