Skip to content

Commit

Permalink
feat: Respect nvmrc for Node version
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielku15 committed Feb 2, 2025
1 parent 8859e2a commit 99557ea
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 23 deletions.
22 changes: 19 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,28 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
- name: Install Node.js
if: runner.os != 'Linux'
uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install Node.js via NVM (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
echo $NVM_DIR >> $GITHUB_PATH
echo "NVM_DIR=$NVM_DIR" >> $GITHUB_ENV
nvm install $(cat test-workspaces/nvm/.nvmrc)
nvm install lts/*
- name: Install dependencies
run: npm install
run: |
node --version
npm --version
npm install
- name: Compile
run: npm run compile:test
Expand Down Expand Up @@ -70,11 +85,12 @@ jobs:
if: always()
run: npm run lint

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

- uses: actions/upload-artifact@v4
Expand Down
76 changes: 63 additions & 13 deletions src/configurationFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { DisposableStore } from './disposable';
import { HumanError } from './errors';
import { getPathToNode } from './node';
import { getPathToNode, isNvmInstalled } from './node';

type OptionsModule = {
loadOptions(): IResolvedConfiguration;
Expand All @@ -41,6 +41,7 @@ export class ConfigurationFile implements vscode.Disposable {
private _optionsModule?: OptionsModule;
private _configModule?: ConfigModule;
private _pathToMocha?: string;
private _pathToNvmRc?: string;

/** Cached read promise, invalided on file change. */
private readPromise?: Promise<ConfigurationList>;
Expand Down Expand Up @@ -140,14 +141,23 @@ export class ConfigurationFile implements vscode.Disposable {

async getMochaSpawnArgs(customArgs: readonly string[]): Promise<string[]> {
this._pathToMocha ??= await this._resolveLocalMochaBinPath();
this._pathToNvmRc ??= await this._resolveNvmRc();

let nodeSpawnArgs: string[];
if (
this._pathToNvmRc &&
(await fs.promises
.access(this._pathToNvmRc)
.then(() => true)
.catch(() => false))
) {
nodeSpawnArgs = ['nvm', 'run'];
} else {
this._pathToNvmRc = undefined;
nodeSpawnArgs = [await getPathToNode(this.logChannel)];
}

return [
await getPathToNode(this.logChannel),
this._pathToMocha,
'--config',
this.uri.fsPath,
...customArgs,
];
return [...nodeSpawnArgs, this._pathToMocha, '--config', this.uri.fsPath, ...customArgs];
}

private getResolver() {
Expand Down Expand Up @@ -179,6 +189,42 @@ export class ConfigurationFile implements vscode.Disposable {
throw new HumanError(`Could not find node_modules above '${mocha}'`);
}

private async _resolveNvmRc(): Promise<string | undefined> {
// the .nvmrc file can be placed in any location up the directory tree, so we do the same
// starting from the mocha config file
// https://github.com/nvm-sh/nvm/blob/06413631029de32cd9af15b6b7f6ed77743cbd79/nvm.sh#L475-L491
try {
if (!(await isNvmInstalled())) {
return undefined;
}

let dir: string | undefined = path.dirname(this.uri.fsPath);

while (dir) {
const nvmrc = path.join(dir, '.nvmrc');
if (
await fs.promises
.access(nvmrc)
.then(() => true)
.catch(() => false)
) {
this.logChannel.debug(`Found .nvmrc at ${nvmrc}`);
return nvmrc;
}

const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
} catch (e) {
this.logChannel.error(e as Error, 'Error while searching for nvmrc');
}

return undefined;
}

private async _resolveLocalMochaBinPath(): Promise<string> {
try {
const packageJsonPath = await this._resolveLocalMochaPath('/package.json');
Expand All @@ -193,17 +239,21 @@ export class ConfigurationFile implements vscode.Disposable {
// ignore
}

this.logChannel.warn('Could not resolve mocha bin path from package.json, fallback to default');
this.logChannel.info('Could not resolve mocha bin path from package.json, fallback to default');
return await this._resolveLocalMochaPath('/bin/mocha.js');
}

private _resolveLocalMochaPath(suffix: string = ''): Promise<string> {
return this._resolve(`mocha${suffix}`);
}

private _resolve(request: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const dir = path.dirname(this.uri.fsPath);
this.logChannel.debug(`resolving 'mocha${suffix}' via ${dir}`);
this.getResolver().resolve({}, dir, 'mocha' + suffix, {}, (err, res) => {
this.logChannel.debug(`resolving '${request}' via ${dir}`);
this.getResolver().resolve({}, dir, request, {}, (err, res) => {
if (err) {
this.logChannel.error(`resolving 'mocha${suffix}' failed with error ${err}`);
this.logChannel.error(`resolving '${request}' failed with error ${err}`);
reject(
new HumanError(
`Could not find mocha in working directory '${path.dirname(
Expand All @@ -212,7 +262,7 @@ export class ConfigurationFile implements vscode.Disposable {
),
);
} else {
this.logChannel.debug(`'mocha${suffix}' resolved to '${res}'`);
this.logChannel.debug(`'${request}' resolved to '${res}'`);
resolve(res as string);
}
});
Expand Down
20 changes: 13 additions & 7 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
* https://opensource.org/licenses/MIT.
*/

import fs from 'fs';
import { homedir } from 'os';
import path from 'path';
import * as vscode from 'vscode';
import which from 'which';

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

let pathToNpm: string | null = null;

export async function getPathToNpm(logChannel: vscode.LogOutputChannel) {
if (!pathToNpm) {
pathToNpm = await getPathTo(logChannel, 'npm', 'NPM');
export async function isNvmInstalled() {
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L27
const nvmDir = process.env.NVM_DIR || homedir();
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L143
try {
await fs.promises.access(path.join(nvmDir, '.nvm', '.git'));
return true;
} catch (e) {
return false;
}
return pathToNpm;
}
41 changes: 41 additions & 0 deletions src/test/integration/nvm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (C) Daniel Kuschny (Danielku15) and contributors.
* Copyright (C) Microsoft Corporation. All rights reserved.
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

import { expect } from 'chai';
import * as vscode from 'vscode';
import { captureTestRun, expectTestTree, getController, integrationTestPrepare } from '../util';

describe('nvm', () => {
integrationTestPrepare('nvm');

it('discovers tests', async () => {
const c = await getController();

expectTestTree(c, [['nvm.test.js', [['nvm', [['ensure-version']]]]]]);
});

it('runs tests', async () => {
const c = await getController();
const profiles = c.profiles;
expect(profiles).to.have.lengthOf(2);

const run = await captureTestRun(
c,
new vscode.TestRunRequest(
undefined,
undefined,
profiles.find((p) => p.kind === vscode.TestRunProfileKind.Run),
),
);

run.expectStates({
'nvm.test.js/nvm/ensure-version': ['enqueued', 'started', 'passed'],
});
});
});
3 changes: 3 additions & 0 deletions test-workspaces/nvm/.mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
spec: '**/*.test.js'
};
1 change: 1 addition & 0 deletions test-workspaces/nvm/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20
26 changes: 26 additions & 0 deletions test-workspaces/nvm/nvm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { match } = require('node:assert');
const { existsSync } = require('node:fs');
const { platform, homedir } = require('node:os');
const { join } = require('node:path');

describe('nvm', () => {
it('ensure-version', () => {
// keep this in sync with the .nvmrc
const expectedVersion = 'v20';

// nvm is only available on MacOS and Linux
// so we skip it on windows.
// also if NVM on local development we skip this test (for GITHUB_ACTIONS we expect it to be there).
const shouldRun = platform() === 'linux' && (isNvmInstalled() || process.env.GITHUB_ACTIONS);
if (shouldRun) {
match(process.version, new RegExp(expectedVersion + '.*'));
}
});

function isNvmInstalled() {
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L27
const nvmDir = process.env.NVM_DIR || homedir();
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L143
return existsSync(join(nvmDir, '.nvm', '.git'));
}
});

0 comments on commit 99557ea

Please sign in to comment.