cspell-lib
Version:
A library of useful functions used across various cspell tools.
312 lines • 12.6 kB
JavaScript
import { createRequire } from 'node:module';
import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveGlobal } from '@cspell/cspell-resolver';
import { importResolveModuleName } from '@cspell/dynamic-import';
import resolveFrom from 'resolve-from';
import { getFileSystem } from '../fileSystem.js';
import { srcDirectory } from '../pkg-info.mjs';
import { envToTemplateVars, replaceTemplate } from './templates.js';
import { fileURLOrPathToPath, isDataURL, isFileURL, isURLLike, resolveFileWithURL, toFileDirURL, toFilePathOrHref, toFileUrl, toURL, } from './url.js';
const regExpStartsWidthNodeModules = /^node_modules[/\\]/;
const debugMode = false;
export class FileResolver {
fs;
templateReplacements;
constructor(fs, templateReplacements) {
this.fs = fs;
this.templateReplacements = templateReplacements;
}
/**
* 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, a node_module, or URL.
* @param relativeTo absolute path
*/
async resolveFile(filename, relativeTo) {
if (filename instanceof URL) {
return {
filename: toFilePathOrHref(filename),
relativeTo: relativeTo.toString(),
found: await this.doesExist(filename),
method: 'url',
};
}
const result = await this._resolveFile(filename, relativeTo);
const match = filename.match(regExpStartsWidthNodeModules);
if (match) {
result.warning ??= `Import of '${filename}' should not start with '${match[0]}' in '${toFilePathOrHref(relativeTo)}'. Use '${filename.replace(regExpStartsWidthNodeModules, '')}' or a relative path instead.`;
}
return result;
}
async _resolveFile(filename, relativeTo) {
filename = patchFilename(filename, this.templateReplacements);
const steps = [
{ filename, fn: this.tryUrlRel },
{ filename, fn: this.tryCreateRequire },
{ filename, fn: this.tryNodeRequireResolve },
{ filename, fn: this.tryImportResolve },
{ filename, fn: this.tryResolveExists },
{ filename, fn: this.tryNodeResolveDefaultPaths },
{ filename, fn: this.tryResolveFrom },
{ filename, fn: this.tryResolveGlobal },
{ filename, fn: this.tryLegacyResolve },
];
for (const step of steps) {
const r = await step.fn(step.filename, relativeTo);
if (r?.found)
return r;
}
const result = (await this.tryUrl(filename, relativeTo)) || {
filename: isRelative(filename) ? joinWith(filename, relativeTo) : filename.toString(),
relativeTo: relativeTo.toString(),
found: false,
method: 'not found',
};
return result;
}
async doesExist(file) {
try {
const s = await this.fs.stat(file);
return s.isFile() || s.isUnknown();
}
catch {
return false;
}
}
/**
* Check to see if it is a URL.
* Note: URLs are absolute!
* If relativeTo is a non-file URL, then it will try to resolve the filename relative to it.
* @param filename - url string
* @returns ResolveFileResult
*/
tryUrlRel = async (filename, relativeToURL) => {
if (isURLLike(filename)) {
const fileURL = toURL(filename);
return {
filename: toFilePathOrHref(fileURL),
relativeTo: undefined,
found: await this.doesExist(fileURL),
method: 'tryUrl',
};
}
if (isRelative(filename) && isURLLike(relativeToURL) && !isDataURL(relativeToURL)) {
const relToURL = toURL(relativeToURL);
const url = resolveFileWithURL(filename, relToURL);
return {
filename: toFilePathOrHref(url),
relativeTo: toFilePathOrHref(relToURL),
found: await this.doesExist(url),
method: 'tryUrl',
};
}
return undefined;
};
/**
* Check to see if it is a URL.
* Note: URLs are absolute!
* If relativeTo is a non-file URL, then it will try to resolve the filename relative to it.
* @param filename - url string
* @returns ResolveFileResult
*/
tryUrl = async (filename, relativeToURL) => {
if (isURLLike(relativeToURL) && !isDataURL(relativeToURL)) {
const relToURL = toURL(relativeToURL);
const url = resolveFileWithURL(filename, relToURL);
return {
filename: toFilePathOrHref(url),
relativeTo: toFilePathOrHref(relToURL),
found: await this.doesExist(url),
method: 'tryUrl',
};
}
return undefined;
};
tryCreateRequire = (filename, relativeTo) => {
if (filename instanceof URL)
return undefined;
const rel = !isURLLike(relativeTo) || isFileURL(relativeTo) ? relativeTo : toFileDirURL('./');
try {
const require = createRequire(rel);
const r = require.resolve(filename);
return { filename: r, relativeTo: rel.toString(), found: true, method: 'tryCreateRequire' };
}
catch (error) {
if (debugMode) {
console.error('Error in tryCreateRequire: %o', { filename, rel, relativeTo, error: `${error}` });
}
return undefined;
}
};
tryNodeResolveDefaultPaths = (filename) => {
try {
// eslint-disable-next-line unicorn/prefer-module
const r = require.resolve(filename);
return { filename: r, relativeTo: undefined, found: true, method: 'tryNodeResolveDefaultPaths' };
}
catch {
return undefined;
}
};
tryNodeRequireResolve = (filenameOrURL, relativeTo) => {
if (isURLLike(relativeTo) && !isFileURL(relativeTo))
return undefined;
const filename = fileURLOrPathToPath(filenameOrURL);
const relativeToPath = pathFromRelativeTo(relativeTo);
const home = os.homedir();
function calcPaths(p) {
const paths = [p];
// Do not progress towards the root if it is a relative filename.
if (isRelative(filename)) {
return paths;
}
for (; p && path.dirname(p) !== p && p !== home; p = path.dirname(p)) {
paths.push(p);
}
return paths;
}
const paths = calcPaths(path.resolve(relativeToPath));
try {
// eslint-disable-next-line unicorn/prefer-module
const r = require.resolve(filename, { paths });
return { filename: r, relativeTo: relativeToPath, found: true, method: 'tryNodeRequireResolve' };
}
catch {
return undefined;
}
};
tryImportResolve = (filename, relativeTo) => {
try {
const paths = isRelative(filename) ? [relativeTo] : [relativeTo, srcDirectory];
const resolved = fileURLToPath(importResolveModuleName(filename, paths));
return { filename: resolved, relativeTo: relativeTo.toString(), found: true, method: 'tryImportResolve' };
}
catch {
return undefined;
}
};
tryResolveGlobal = (filename) => {
const r = resolveGlobal(filename);
return (r && { filename: r, relativeTo: undefined, found: true, method: 'tryResolveGlobal' }) || undefined;
};
tryResolveExists = async (filename, relativeTo) => {
if (filename instanceof URL || isURLLike(filename) || (isURLLike(relativeTo) && !isFileURL(relativeTo))) {
return undefined;
}
relativeTo = pathFromRelativeTo(relativeTo);
const toTry = [{ filename }, { filename: path.resolve(relativeTo, filename), relativeTo }];
for (const { filename, relativeTo } of toTry) {
const found = path.isAbsolute(filename) && (await this.doesExist(toFileUrl(filename)));
if (found)
return { filename, relativeTo: relativeTo?.toString(), found, method: 'tryResolveExists' };
}
filename = path.resolve(filename);
return {
filename,
relativeTo: path.resolve('.'),
found: await this.doesExist(toFileUrl(filename)),
method: 'tryResolveExists',
};
};
tryResolveFrom = (filename, relativeTo) => {
if (relativeTo instanceof URL)
return undefined;
try {
return {
filename: resolveFrom(pathFromRelativeTo(relativeTo), filename),
relativeTo,
found: true,
method: 'tryResolveFrom',
};
}
catch {
// Failed to resolve a relative module request
return undefined;
}
};
tryLegacyResolve = (filename, relativeTo) => {
if (filename instanceof URL || isURLLike(filename) || (isURLLike(relativeTo) && !isFileURL(relativeTo))) {
return undefined;
}
const relativeToPath = isURLLike(relativeTo) ? fileURLToPath(new URL('./', relativeTo)) : relativeTo.toString();
const match = filename.match(regExpStartsWidthNodeModules);
if (match) {
const fixedFilename = filename.replace(regExpStartsWidthNodeModules, '');
const found = this.tryImportResolve(fixedFilename, relativeToPath) ||
this.tryResolveFrom(fixedFilename, relativeToPath);
if (found?.found) {
found.method = 'tryLegacyResolve';
return found;
}
}
return undefined;
};
}
export function patchFilename(filename, templateReplacements) {
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, relativeTo, templateReplacements = envToTemplateVars(process.env)) {
if (filename instanceof URL)
return filename;
filename = patchFilename(filename, templateReplacements);
const relativeToUrl = toFileUrl(relativeTo);
return resolveFileWithURL(filename, relativeToUrl);
}
function isRelative(filename) {
if (filename instanceof URL)
return false;
if (isURLLike(filename))
return false;
if (filename.startsWith('./'))
return true;
if (filename.startsWith('../'))
return true;
if (filename.startsWith('.' + path.sep))
return true;
if (filename.startsWith('..' + path.sep))
return true;
return false;
}
function joinWith(filename, relativeTo) {
return relativeTo instanceof URL || isURLLike(relativeTo)
? toFilePathOrHref(new URL(filename, relativeTo))
: path.resolve(relativeTo, filename);
}
function pathFromRelativeTo(relativeTo) {
return relativeTo instanceof URL || isURLLike(relativeTo) ? fileURLToPath(new URL('./', relativeTo)) : relativeTo;
}
const loaderCache = new WeakMap();
export function createFileResolver(fs, templateVariables = envToTemplateVars(process.env)) {
let loader = loaderCache.get(fs);
if (!loader) {
loader = new FileResolver(fs, templateVariables);
loaderCache.set(fs, loader);
}
return loader;
}
export async function resolveFile(filename, relativeTo, fs = getFileSystem()) {
const resolver = createFileResolver(fs);
return resolver.resolveFile(filename, relativeTo);
}
//# sourceMappingURL=resolveFile.js.map