init project
This commit is contained in:
+170
@@ -0,0 +1,170 @@
|
||||
import fs from 'node:fs';
|
||||
import fsPromises from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// A command containing a directory separator is treated as a path and looked up directly, instead of being searched for in `PATH`. Backslash only counts as a separator on Windows, since it's a valid filename character on other platforms.
|
||||
const separatorPattern = isWindows ? /[\/\\]/v : /\//v;
|
||||
|
||||
// The standard Windows executable extensions, used when `PATHEXT` is not set.
|
||||
const defaultPathExt = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC';
|
||||
|
||||
function resolveOptions(command, options) {
|
||||
if (typeof command !== 'string' || command.length === 0) {
|
||||
throw new TypeError('Expected a non-empty string.');
|
||||
}
|
||||
|
||||
const {
|
||||
cwd = process.cwd(),
|
||||
// On Windows, `process.env.PATH` can be undefined in some contexts (for example, worker threads), where it's only exposed as `Path`.
|
||||
path: searchPath = process.env.PATH ?? (isWindows ? process.env.Path : undefined) ?? '',
|
||||
// `||` (not `??`) so an empty `PATHEXT` falls back to the default instead of disabling all lookup on Windows.
|
||||
pathExt = process.env.PATHEXT || defaultPathExt,
|
||||
} = options;
|
||||
|
||||
return {cwd, searchPath, pathExt};
|
||||
}
|
||||
|
||||
function windowsExtensions(command, pathExt) {
|
||||
const extensions = pathExt.split(path.delimiter).filter(Boolean);
|
||||
const commandExtension = path.extname(command).toLowerCase();
|
||||
|
||||
// If the command already ends with a known executable extension, also try it verbatim.
|
||||
if (commandExtension !== '' && extensions.some(extension => extension.toLowerCase() === commandExtension)) {
|
||||
return ['', ...extensions];
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function * candidatePaths(command, {cwd, searchPath, pathExt}) {
|
||||
const extensions = isWindows ? windowsExtensions(command, pathExt) : [''];
|
||||
|
||||
if (separatorPattern.test(command)) {
|
||||
const base = path.resolve(cwd, command);
|
||||
for (const extension of extensions) {
|
||||
yield base + extension;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const directories = [
|
||||
// Windows searches the current directory before `PATH`.
|
||||
...(isWindows ? [cwd] : []),
|
||||
...searchPath.split(path.delimiter),
|
||||
];
|
||||
|
||||
for (const directory of directories) {
|
||||
// `PATH` entries can be wrapped in double quotes on Windows.
|
||||
const unquoted = isWindows && directory.length > 1 && directory.startsWith('"') && directory.endsWith('"') ? directory.slice(1, -1) : directory;
|
||||
|
||||
// Skip empty entries. Unlike a POSIX shell, an empty entry is not treated as the current directory, since implicitly searching the current directory is a security risk. Checked after unquoting so a quoted-empty entry (`""`) is skipped too.
|
||||
if (unquoted === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const base = path.resolve(cwd, unquoted, command);
|
||||
for (const extension of extensions) {
|
||||
yield base + extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function isExecutable(filePath) {
|
||||
let stats;
|
||||
try {
|
||||
stats = await fsPromises.stat(filePath);
|
||||
} catch (error) {
|
||||
// Windows App Execution Aliases (in `WindowsApps`) throw `EACCES` on `stat`, but are valid, launchable executables.
|
||||
return isWindows && error.code === 'EACCES';
|
||||
}
|
||||
|
||||
// A directory can have the executable bit set, but it's not a command.
|
||||
if (!stats.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// On Windows, executability is determined by the file extension, which is already validated against `PATHEXT`.
|
||||
if (isWindows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await fsPromises.access(filePath, fs.constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isExecutableSync(filePath) {
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(filePath);
|
||||
} catch (error) {
|
||||
return isWindows && error.code === 'EACCES';
|
||||
}
|
||||
|
||||
if (!stats.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function whichCommand(command, options = {}) {
|
||||
const resolved = resolveOptions(command, options);
|
||||
|
||||
for (const candidate of candidatePaths(command, resolved)) {
|
||||
// Checked sequentially so the first match in `PATH` order wins and the search can short-circuit.
|
||||
if (await isExecutable(candidate)) { // eslint-disable-line no-await-in-loop
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function whichCommandSync(command, options = {}) {
|
||||
const resolved = resolveOptions(command, options);
|
||||
|
||||
for (const candidate of candidatePaths(command, resolved)) {
|
||||
if (isExecutableSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function whichCommandAll(command, options = {}) {
|
||||
const resolved = resolveOptions(command, options);
|
||||
const candidates = [...candidatePaths(command, resolved)];
|
||||
const matches = await Promise.all(candidates.map(async candidate => (await isExecutable(candidate)) ? candidate : undefined));
|
||||
return [...new Set(matches.filter(Boolean))];
|
||||
}
|
||||
|
||||
export function whichCommandAllSync(command, options = {}) {
|
||||
const resolved = resolveOptions(command, options);
|
||||
const found = [];
|
||||
|
||||
for (const candidate of candidatePaths(command, resolved)) {
|
||||
if (isExecutableSync(candidate)) {
|
||||
found.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(found)];
|
||||
}
|
||||
Reference in New Issue
Block a user