@bearz/exec
Version:
The exec module makes it easy to spawn child_processes across different runtimes and different operating systems.
281 lines (280 loc) • 9.21 kB
JavaScript
/**
* The `path-finder` module provides a way to find the path of executables
* across different platforms (Windows, Darwin, Linux).
*
* @module
*/
import { equalFold } from "@bearz/strings/equal";
import { expand, get } from "@bearz/env";
import { underscore } from "@bearz/strings/underscore";
import { which, whichSync } from "./which.js";
import { isFile, isFileSync } from "@bearz/fs";
import { DARWIN, WIN } from "./globals.js";
/**
* Represents a path finder that allows storing and retrieving
* options for finding an executable and methods to find the
* executable.
*
* The path finder will use the options to look up by precendence:
*
* - If the full path to the executable is provided, it will be used.
* - If an environment variable is provided, it will be used.
* - If a cached path is provided, it will be used.
* - If the executable is found in the system path, it will be used.
* - If the executable is found in the windows paths when on Windows, it will be used.
* - If the executable is found in the darwin paths when on Darwin or linux paths, it will be used.
* - If the executable is found in the linux paths, it will be used.
*
* The paths for windows, darwin, and linux can contain environment variables e.g.
* `${USERPROFILE}`, `${USER}`, or `%USERPROFILE% that will be expanded before checking if the file exists.
* @example
* ```ts
* import { pathFinder } from "./path-finder.ts";
*
* pathFinder.set("deno", {
* name: "deno",
* envVariable: "DENO_EXE",
* windows: ["${USERPROFILE}\\.deno\\bin\\deno.exe"],
* linux: ["${USER}/.deno/bin/deno"],
* });
*
* const deno = await pathFinder.findExe("deno");
* console.log(deno);
* ```
*/
export class PathFinder {
#map;
constructor() {
this.#map = new Map();
}
/**
* Sets the path finder options for a given name.
* @param name - The name of the path finder.
* @param options - The path finder options.
*/
set(name, options) {
this.#map.set(name, options);
}
/**
* Retrieves the path finder options for a given name.
* @param name - The name of the path finder.
* @returns The path finder options, or undefined if not found.
*/
get(name) {
return this.#map.get(name);
}
/**
* Checks if a path finder with the given name exists.
* @param name - The name of the path finder.
* @returns True if the path finder exists, false otherwise.
*/
has(name) {
return this.#map.has(name);
}
/**
* Deletes the path finder with the given name.
* @param name - The name of the path finder.
* @returns True if the path finder was deleted, false otherwise.
*/
delete(name) {
return this.#map.delete(name);
}
/**
* Clears all path finders.
*/
clear() {
this.#map.clear();
}
/**
* Finds the path finder options for a given name.
* @param name - The name of the path finder.
* @returns The path finder options, or undefined if not found.
*/
find(name) {
const options = this.get(name);
if (!options) {
return;
}
for (const [key, value] of this.#map) {
if (value.name === name) {
return value;
}
if (value.cached === name) {
return value;
}
if (equalFold(key, name)) {
return value;
}
}
return undefined;
}
/**
* Finds the executable path for a given name.
* @param name - The name of the executable.
* @returns The executable path, or undefined if not found.
*/
async findExe(name) {
let options = this.find(name);
if (!options) {
options = {
name: name,
envVariable: (underscore(name) + "_EXE").toUpperCase(),
};
this.set(name, options);
}
if (options?.envVariable) {
let envPath = get(options.envVariable);
if (!options.noCache && envPath && envPath.length > 0 && options.cached === envPath) {
return envPath;
}
envPath = expand(envPath ?? "");
if (!options.noCache && envPath && envPath.length > 0 && options.cached === envPath) {
return envPath;
}
if (envPath && await isFile(envPath)) {
options.cached = envPath;
return envPath;
}
}
if (!options.noCache && options.cached) {
return options.cached;
}
const defaultPath = await which(name);
if (defaultPath) {
options.cached = defaultPath;
return defaultPath;
}
if (WIN) {
if (options.windows && options.windows.length) {
for (const path of options.windows) {
let next = path;
next = expand(next);
if (await isFile(next)) {
options.cached = next;
return next;
}
}
}
return undefined;
}
if (DARWIN) {
if (options.darwin && options.darwin.length) {
for (const path of options.darwin) {
let next = path;
next = expand(next);
if (await isFile(next)) {
options.cached = next;
return next;
}
}
}
// allow darwin to use linux paths
// do not return here
}
if (options.linux && options.linux.length) {
for (const path of options.linux) {
let next = path;
next = expand(next);
if (await isFile(next)) {
options.cached = next;
return next;
}
}
}
return undefined;
}
/**
* Synchronously finds the executable path for a given name.
* @param name - The name of the executable.
* @returns The executable path, or undefined if not found.
*/
findExeSync(name) {
let options = this.find(name);
if (!options) {
options = {
name: name,
envVariable: (underscore(name) + "_EXE").toUpperCase(),
};
this.set(name, options);
}
if (options?.envVariable) {
let envPath = get(options.envVariable);
if (!options.noCache && envPath && envPath.length > 0 && options.cached === envPath) {
return envPath;
}
envPath = expand(envPath ?? "");
if (!options.noCache && envPath && envPath.length > 0 && options.cached === envPath) {
return envPath;
}
if (envPath) {
if (envPath && isFileSync(envPath)) {
options.cached = envPath;
return envPath;
}
}
}
if (!options.noCache && options.cached) {
return options.cached;
}
const defaultPath = whichSync(name);
if (defaultPath) {
options.cached = defaultPath;
return defaultPath;
}
if (WIN) {
if (options.windows && options.windows.length) {
for (const path of options.windows) {
let next = path;
try {
next = expand(next);
} catch {
continue;
}
if (isFileSync(next)) {
options.cached = next;
return next;
}
}
}
return undefined;
}
if (DARWIN) {
if (options.darwin && options.darwin.length) {
for (const path of options.darwin) {
let next = path;
try {
next = expand(next);
} catch {
// todo: get trace/debug writer to handle
continue;
}
if (isFileSync(next)) {
options.cached = next;
return next;
}
}
}
// allow darwin to use linux paths
// do not return here
}
if (options.linux && options.linux.length) {
for (const path of options.linux) {
let next = path;
try {
next = expand(next);
} catch {
continue;
}
if (isFileSync(next)) {
options.cached = next;
return next;
}
}
}
return undefined;
}
}
/**
* The default global path finder instance.
*/
export const pathFinder = new PathFinder();