diff --git a/packages/extension/src/config.ts b/packages/extension/src/config.ts index 55e605b8..60cf85ef 100644 --- a/packages/extension/src/config.ts +++ b/packages/extension/src/config.ts @@ -1,12 +1,36 @@ import type { WorkspaceConfiguration, WorkspaceFolder } from 'vscode' import { homedir } from 'node:os' -import { dirname, isAbsolute, resolve } from 'node:path' +import { dirname, isAbsolute, resolve, sep } from 'node:path' import * as vscode from 'vscode' import { configGlob } from './constants' export const extensionId = 'vitest.explorer' export const testControllerId = 'vitest' +export function substituteVariables(value: string, workspaceFolder?: WorkspaceFolder): string { + const folder = workspaceFolder ?? vscode.workspace.workspaceFolders?.[0] + return ( + value + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{workspaceFolder\}/g, folder?.uri.fsPath ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{workspaceFolderBasename\}/g, folder?.name ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{userHome\}/g, homedir()) + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{env:([^}]+)\}/g, (_, name) => process.env[name] ?? '') + // eslint-disable-next-line no-template-curly-in-string + .replace(/\$\{pathSeparator\}/g, sep) + ) +} + +function resolvePathWithSubstitution(path: string | undefined, workspaceFolder?: WorkspaceFolder) { + return resolveConfigPath( + path ? substituteVariables(path, workspaceFolder) : path, + workspaceFolder, + ) +} + export function getConfigValue( rootConfig: WorkspaceConfiguration, folderConfig: WorkspaceConfiguration, @@ -42,14 +66,9 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { get('configSearchPatternInclude', configGlob) || configGlob const vitestPackagePath = get('vitestPackagePath') - const resolvedVitestPackagePath = - workspaceFolder && vitestPackagePath - ? resolve( - workspaceFolder.uri.fsPath, - // eslint-disable-next-line no-template-curly-in-string - vitestPackagePath.replace('${workspaceFolder}', workspaceFolder.uri.fsPath), - ) - : vitestPackagePath + const resolvedVitestPackagePath = vitestPackagePath + ? resolvePathWithSubstitution(vitestPackagePath, workspaceFolder) + : vitestPackagePath const logLevel = get('logLevel', 'info') @@ -74,24 +93,24 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { env: get>('nodeEnv', null), debugEnv: get>('debugNodeEnv', null), debugExclude: get('debugExclude'), - debugOutFiles, + debugOutFiles: debugOutFiles?.map((f) => substituteVariables(f, workspaceFolder)), filesWatcherInclude, runtime, forceCancelTimeout, watchOnStartup, terminalShellArgs, - terminalShellPath, + terminalShellPath: resolvePathWithSubstitution(terminalShellPath, workspaceFolder), shellType, applyDiagnostic, cliArguments, nodeExecArgs, vitestPackagePath: resolvedVitestPackagePath, - workspaceConfig: resolveConfigPath(workspaceConfig), - rootConfig: resolveConfigPath(rootConfigFile), + workspaceConfig: resolvePathWithSubstitution(workspaceConfig, workspaceFolder), + rootConfig: resolvePathWithSubstitution(rootConfigFile, workspaceFolder), configSearchPatternInclude, configSearchPatternExclude, ignoreWorkspace, - nodeExecutable: resolveConfigPath(nodeExecutable), + nodeExecutable: resolvePathWithSubstitution(nodeExecutable, workspaceFolder), disableWorkspaceWarning: get('disableWorkspaceWarning', false), debuggerPort: get('debuggerPort') || undefined, debuggerAddress: get('debuggerAddress', undefined) || undefined, @@ -101,11 +120,13 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { } } -export function resolveConfigPath(path: string | undefined) { +export function resolveConfigPath(path: string | undefined, workspaceFolder?: WorkspaceFolder) { if (!path || isAbsolute(path)) return path if (path.startsWith('~/')) { return resolve(homedir(), path.slice(2)) } + // if a workspaceFolder was provided, resolve relative to it + if (workspaceFolder) return resolve(workspaceFolder.uri.fsPath, path) // if there is a workspace file, then it should be relative to it because // this option cannot be configured on a workspace folder level if (vscode.workspace.workspaceFile) diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts index 639ce2c7..47effe48 100644 --- a/test/unit/config.test.ts +++ b/test/unit/config.test.ts @@ -1,8 +1,61 @@ -import { resolve } from 'node:path' +import { resolve, sep } from 'node:path' import { homedir } from 'node:os' import { expect } from 'chai' -import { resolveConfigPath } from '../../packages/extension/src/config' +import type { Uri, WorkspaceFolder } from 'vscode' +import { resolveConfigPath, substituteVariables } from '../../packages/extension/src/config' + +function mockWorkspaceFolder(fsPath: string, name: string): WorkspaceFolder { + return { + uri: { fsPath } as Uri, + name, + index: 0, + } +} it('correctly resolves ~', () => { expect(resolveConfigPath('~/test')).to.equal(resolve(homedir(), 'test')) }) + +describe('substituteVariables', () => { + it('substitutes ${workspaceFolder}', () => { + const folder = mockWorkspaceFolder('/my/workspace', 'workspace') + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${workspaceFolder}/src', folder)).to.equal('/my/workspace/src') + }) + + it('substitutes ${workspaceFolderBasename}', () => { + const folder = mockWorkspaceFolder('/my/workspace', 'myproject') + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${workspaceFolderBasename}', folder)).to.equal('myproject') + }) + + it('substitutes ${userHome}', () => { + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${userHome}/projects')).to.equal(`${homedir()}/projects`) + }) + + it('substitutes ${env:NAME}', () => { + process.env.TEST_VAR_VITEST_EXT = 'hello' + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${env:TEST_VAR_VITEST_EXT}/path')).to.equal('hello/path') + delete process.env.TEST_VAR_VITEST_EXT + }) + + it('substitutes ${pathSeparator}', () => { + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('foo${pathSeparator}bar')).to.equal(`foo${sep}bar`) + }) + + it('substitutes multiple occurrences', () => { + const folder = mockWorkspaceFolder('/ws', 'myws') + // eslint-disable-next-line no-template-curly-in-string + expect( + substituteVariables('${workspaceFolder}/a/${workspaceFolderBasename}/b', folder), + ).to.equal('/ws/a/myws/b') + }) + + it('leaves unrecognized variables unchanged', () => { + // eslint-disable-next-line no-template-curly-in-string + expect(substituteVariables('${unknown}/path')).to.equal('${unknown}/path') + }) +})