Skip to content

Commit

Permalink
feat: Support ${env:NAME} in paths (#5078)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Dec 18, 2023
1 parent abd6bbe commit e22244e
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 441 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { createCSpellSettingsInternal as csi } from '../../../Models/CSpellSetti
import { AutoResolveCache } from '../../../util/AutoResolve.js';
import { logError, logWarning } from '../../../util/logger.js';
import { FileResolver } from '../../../util/resolveFile.js';
import { envToTemplateVars } from '../../../util/templates.js';
import {
addTrailingSlash,
cwdURL,
Expand Down Expand Up @@ -155,10 +156,13 @@ export class ConfigLoader implements IConfigLoader {
* Use `createConfigLoader`
* @param virtualFs - virtual file system to use.
*/
protected constructor(readonly fs: VFileSystem) {
protected constructor(
readonly fs: VFileSystem,
readonly templateVariables: Record<string, string> = envToTemplateVars(process.env),
) {
this.configSearch = new ConfigSearch(searchPlaces, fs);
this.cspellConfigFileReaderWriter = createReaderWriter(undefined, undefined, createIO(fs));
this.fileResolver = new FileResolver(fs);
this.fileResolver = new FileResolver(fs, this.templateVariables);
this.onReady = this.prefetchGlobalSettingsAsync();
this.subscribeToEvents();
}
Expand Down
7 changes: 4 additions & 3 deletions packages/cspell-lib/src/lib/Settings/DictionarySettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import type {
} from '../Models/CSpellSettingsInternalDef.js';
import { isDictionaryDefinitionInlineInternal } from '../Models/CSpellSettingsInternalDef.js';
import { AutoResolveWeakCache } from '../util/AutoResolve.js';
import { resolveFile } from '../util/resolveFileLegacy.js';
import { resolveRelativeTo } from '../util/resolveFile.js';
import type { RequireOptional, UnionFields } from '../util/types.js';
import { toFilePathOrHref } from '../util/url.js';
import { clean } from '../util/util.js';
import type { DictionaryReferenceCollection } from './DictionaryReferenceCollection.js';
import { createDictionaryReferenceCollection } from './DictionaryReferenceCollection.js';
Expand Down Expand Up @@ -188,12 +189,12 @@ class _DictionaryDefinitionInternalWithSource implements DictionaryFileDefinitio
const filePath = fixDicPath(relPath, file);
const name = determineName(filePath, def);

const r = resolveFile(filePath, defaultPath);
const resolvedPath = toFilePathOrHref(resolveRelativeTo(filePath, defaultPath));

const ddi: DDI = {
name,
file: undefined,
path: r.filename,
path: resolvedPath,
addWords,
description,
dictionaryInformation,
Expand Down
68 changes: 65 additions & 3 deletions packages/cspell-lib/src/lib/util/resolveFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { fileURLToPath, pathToFileURL } from 'url';
import { afterEach, describe, expect, test } from 'vitest';

import { pathRepoTestFixturesURL } from '../../test-util/index.mjs';
import { FileResolver } from './resolveFile.js';
import { FileResolver, resolveRelativeTo } from './resolveFile.js';
import { envToTemplateVars } from './templates.js';
import { isFileURL, toFilePathOrHref, toURL } from './url.js';

interface Config {
Expand Down Expand Up @@ -40,11 +41,11 @@ describe('Validate resolveFile', () => {
];
let vfs = createVirtualFS();
vfs.registerFileSystemProvider(...redirects);
let resolver = new FileResolver(vfs.fs);
let resolver = new FileResolver(vfs.fs, envToTemplateVars(process.env));

afterEach(() => {
vfs = createVirtualFS();
resolver = new FileResolver(vfs.fs);
resolver = new FileResolver(vfs.fs, envToTemplateVars(process.env));
});

interface ResolveFileTest {
Expand Down Expand Up @@ -147,6 +148,67 @@ describe('Validate resolveFile', () => {
});
});

describe('resolveRelativeTo', () => {
test('should resolve a filename to a URL', () => {
const filename = '/path/to/file.txt';
const relativeTo = 'https://example.com';
const result = resolveRelativeTo(filename, relativeTo);
expect(result.toString()).toBe('https://example.com/path/to/file.txt');
});

test('should resolve a relative path to a URL', () => {
const filename = '../file.txt';
const relativeTo = 'https://example.com/path/to/';
const result = resolveRelativeTo(filename, relativeTo);
expect(result.toString()).toBe('https://example.com/path/file.txt');
});

test('should resolve a URL to a URL', () => {
const filename = 'https://example.com/file.txt';
const relativeTo = 'https://example.com/path/to/';
const result = resolveRelativeTo(filename, relativeTo);
expect(result.toString()).toBe('https://example.com/file.txt');
});

test('should resolve a filename with environment variables', () => {
const filename = '${env:HOME}/${env:PROJECTS}/cspell/file.txt';
const relativeTo = 'https://example.com';
const result = resolveRelativeTo(
filename,
relativeTo,
envToTemplateVars({ HOME: '/user', PROJECTS: 'projects' }),
);
expect(result.toString()).toBe('https://example.com/user/projects/cspell/file.txt');
});

test('resolve a filename with a nested environment variable', () => {
const filename = '/${env:OUTSIDE}/cspell/file.txt';
const relativeTo = 'https://example.com';
const result = resolveRelativeTo(
filename,
relativeTo,
envToTemplateVars({ OUTSIDE: '${env: INSIDE}', INSIDE: '${env:HOME}' }),
);
expect(result.toString()).toBe('https://example.com/$%7Benv:%20INSIDE%7D/cspell/file.txt');
});

test('should resolve a filename with tilde (~)', () => {
const filename = '~/file.txt';
const relativeTo = pathToFileURL(import.meta.url);
const result = resolveRelativeTo(filename, relativeTo);
const absFilename = fileURLToPath(result);
expect(absFilename).toBe(path.resolve(os.homedir(), 'file.txt'));
});

test('should resolve a filename `${cwd}`', () => {
const filename = '${cwd}/file.txt';
const relativeTo = pathToFileURL(import.meta.url);
const result = resolveRelativeTo(filename, relativeTo);
const absFilename = fileURLToPath(result);
expect(absFilename).toBe(path.resolve(process.cwd(), 'file.txt'));
});
});

function readConfig(filename: string): Config {
const parsed = parse(fs.readFileSync(filename, 'utf-8'));
if (!parsed || typeof parsed !== 'object') throw new Error(`Unable to parse "${filename}"`);
Expand Down
49 changes: 44 additions & 5 deletions packages/cspell-lib/src/lib/util/resolveFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { fileURLToPath } from 'url';

import { srcDirectory } from '../../lib-cjs/pkg-info.cjs';
import { getFileSystem } from '../fileSystem.js';
import { envToTemplateVars, replaceTemplate } from './templates.js';
import {
fileURLOrPathToPath,
isDataURL,
Expand Down Expand Up @@ -42,12 +43,17 @@ export interface ResolveFileResult {
const regExpStartsWidthNodeModules = /^node_modules[/\\]/;

export class FileResolver {
constructor(private fs: VFileSystem) {}
constructor(
private fs: VFileSystem,
readonly templateReplacements: Record<string, string>,
) {}

/**
* Resolve filename to absolute paths.
* - Replaces `${env:NAME}` with the value of the environment variable `NAME`.
* - Replaces `~` with the user's home directory.
* It tries to look for local files as well as node_modules
* @param filename an absolute path, relative path, `~` path, or a node_module.
* @param filename an absolute path, relative path, `~` path, a node_module, or URL.
* @param relativeTo absolute path
*/
async resolveFile(filename: string | URL, relativeTo: string | URL): Promise<ResolveFileResult> {
Expand All @@ -72,7 +78,7 @@ export class FileResolver {
}

async _resolveFile(filename: string, relativeTo: string | URL): Promise<ResolveFileResult> {
filename = filename.replace(/^~/, os.homedir());
filename = patchFilename(filename, this.templateReplacements);
const steps: {
filename: string;
fn: (f: string, r: string | URL) => Promise<ResolveFileResult | undefined> | ResolveFileResult | undefined;
Expand Down Expand Up @@ -269,6 +275,39 @@ export class FileResolver {
};
}

export function patchFilename(filename: string, templateReplacements: Record<string, string>): string {
const defaultReplacements = {
cwd: process.cwd(),
pathSeparator: path.sep,
userHome: os.homedir(),
};

filename = filename.replace(/^~(?=[/\\])/, defaultReplacements.userHome);
filename = replaceTemplate(filename, { ...defaultReplacements, ...templateReplacements });
return filename;
}

/**
* Resolve filename to a URL
* - Replaces `${env:NAME}` with the value of the environment variable `NAME`.
* - Replaces `~` with the user's home directory.
* It will not resolve Node modules.
* @param filename - a filename, path, relative path, or URL.
* @param relativeTo - a path, or URL.
* @param env - environment variables used to patch the filename.
* @returns a URL
*/
export function resolveRelativeTo(
filename: string | URL,
relativeTo: string | URL,
templateReplacements = envToTemplateVars(process.env),
): URL {
if (filename instanceof URL) return filename;
filename = patchFilename(filename, templateReplacements);
const relativeToUrl = toFileUrl(relativeTo);
return resolveFileWithURL(filename, relativeToUrl);
}

function isRelative(filename: string | URL): boolean {
if (filename instanceof URL) return false;
if (isURLLike(filename)) return false;
Expand All @@ -291,10 +330,10 @@ function pathFromRelativeTo(relativeTo: string | URL): string {

const loaderCache = new WeakMap<VFileSystem, FileResolver>();

export function createFileResolver(fs: VFileSystem): FileResolver {
export function createFileResolver(fs: VFileSystem, templateVariables = envToTemplateVars(process.env)): FileResolver {
let loader = loaderCache.get(fs);
if (!loader) {
loader = new FileResolver(fs);
loader = new FileResolver(fs, templateVariables);
loaderCache.set(fs, loader);
}
return loader;
Expand Down
155 changes: 0 additions & 155 deletions packages/cspell-lib/src/lib/util/resolveFileLegacy.test.ts

This file was deleted.

Loading

0 comments on commit e22244e

Please sign in to comment.