@surface/rwx
Version:
Provides read, write and execute capabilities.
235 lines (234 loc) • 8.14 kB
JavaScript
import child_process from "child_process";
import { existsSync, lstatSync, readdirSync, statSync } from "fs";
import { lstat, readdir, stat } from "fs/promises";
import { dirname, isAbsolute, join, resolve } from "path";
import PathMatcher from "@surface/path-matcher";
function errorHandler(error) {
if (error.code == "ENOENT" || error.code == "ENOTDIR") {
return null;
}
throw error;
}
function handler(action) {
try {
const result = action();
return result instanceof Promise ? result.catch(errorHandler) : result;
}
catch (error) {
return errorHandler(error);
}
}
function resolveRedundantPatterns(patterns, options) {
const matcher = new PathMatcher(patterns, options);
const paths = [];
let path = null;
for (const entry of Array.from(matcher.paths).sort()) {
if (path === null || !entry.startsWith(path)) {
paths.push(path = entry);
}
}
return { matcher, paths };
}
async function* internalEnumeratePaths(context, matcher) {
for (const entry of await readdir(context)) {
const path = join(context, entry);
if (!matcher.negatedPaths.has(path)) {
if (await isDirectory(path)) {
for await (const file of internalEnumeratePaths(path, matcher)) {
yield file;
}
}
else if (matcher.isMatch(path)) {
yield path;
}
}
}
}
function* internalEnumeratePathsSync(context, matcher) {
for (const entry of readdirSync(context)) {
const path = join(context, entry);
if (!matcher.negatedPaths.has(path)) {
if (isDirectorySync(path)) {
for (const file of internalEnumeratePathsSync(path, matcher)) {
yield file;
}
}
else if (matcher.isMatch(path)) {
yield path;
}
}
}
}
/**
* Asynchronous enumerate paths using given patterns.
* @param patterns Patterns to match. Strings prefixed with "!" will be negated.
* @param options Options to parse patterns.
*/
export async function* enumeratePaths(patterns, options = { base: process.cwd() }) {
const resolved = resolveRedundantPatterns(patterns, options);
for (const path of resolved.paths) {
if (await isFile(path)) {
yield path;
}
else if (await isDirectory(path)) {
for await (const iterator of internalEnumeratePaths(path, resolved.matcher)) {
yield iterator;
}
}
}
}
/**
* enumerate paths using given patterns.
* @param patterns Patterns to match. Strings prefixed with "!" will be negated.
* @param options Options to parse patterns.
*/
export function* enumeratePathsSync(patterns, options = { base: process.cwd() }) {
const resolved = resolveRedundantPatterns(patterns, options);
for (const path of resolved.paths) {
if (isFileSync(path)) {
yield path;
}
else if (isDirectorySync(path)) {
for (const iterator of internalEnumeratePathsSync(path, resolved.matcher)) {
yield iterator;
}
}
}
}
/* c8 ignore start */
/**
* Spawns a shell then executes the command within that shell.
* @param command string passed to the exec function and processed directly by the shell and special characters (vary based on shell) need to be dealt with accordingly:
*/
export async function execute(command, options) {
const output = [];
console.log(command);
const handler = (type) => (buffer) => {
if (!options?.silent) {
console[type](String(buffer).trimEnd());
}
output.push(...Buffer.from(buffer));
};
return new Promise((resolve, reject) => {
const childProcess = child_process.exec(command, options);
childProcess.stdout?.setEncoding("utf8");
childProcess.stdout?.on("data", handler("log"));
childProcess.stderr?.setEncoding("utf8");
childProcess.stderr?.on("data", handler("error"));
childProcess.on("error", reject);
childProcess.on("exit", code => (code ?? 0) != 0 ? reject(new Error(`Exit code ${code}`)) : resolve(Buffer.from(output)));
});
}
/* c8 ignore stop */
/**
* Asynchronous Verifies if a path is a directory.
* @param path Path to verify. If a URL is provided, it must use the `file:` protocol.
*/
export async function isDirectory(path) {
const stats = await handler(async () => stat(path));
return !!stats && stats.isDirectory();
}
/**
* Verifies if a path is a directory.
* @param path Path to verify. If a URL is provided, it must use the `file:` protocol.
*/
export function isDirectorySync(path) {
const stats = handler(() => statSync(path));
return !!stats && stats.isDirectory();
}
/**
* Verifies if a path is a file.
* @param path Path to verify. If a URL is provided, it must use the `file:` protocol.
*/
export async function isFile(path) {
const stats = await handler(async () => stat(path));
return !!stats && (stats.isFile() || stats.isFIFO());
}
/**
* Verifies if a path is a file.
* @param path Path to verify. If a URL is provided, it must use the `file:` protocol.
*/
export function isFileSync(path) {
const stats = handler(() => statSync(path));
return !!stats && (stats.isFile() || stats.isFIFO());
}
/**
* Verifies if a path is a symbolic link.
* @param path Path to verify. If a URL is provided, it must use the `file:` protocol.
*/
export async function isSymbolicLink(path) {
const stats = await handler(async () => lstat(path));
return !!stats && stats.isSymbolicLink();
}
/**
* Verifies if a path is a symbolic link.
* @param path Path to verify. If a URL is provided, it must use the `file:` protocol.
*/
export function isSymbolicLinkSync(path) {
const stats = handler(() => lstatSync(path));
return !!stats && stats.isSymbolicLink();
}
/**
* Asynchronous list paths using given patterns.
* @param pattern Pattern to match.
* @param cwd Working dir.
*/
export async function listPaths(pattern, options) {
const paths = [];
for await (const path of enumeratePaths(pattern, options)) {
paths.push(path);
}
return paths;
}
/**
* List paths using given patterns.
* @param pattern Pattern to match.
* @param cwd Working dir.
*/
export function listPathsSync(pattern, options) {
return Array.from(enumeratePathsSync(pattern, options));
}
/**
* Asynchronous resolve and returns the path of the first resolved file and null otherwise.
* @param files Files to look.
* @param context Context used to resolve.
*/
export async function lookup(files, context = process.cwd()) {
for (const path of files) {
const resolved = isAbsolute(path) ? path : resolve(context, path);
if (await isFile(resolved)) {
return resolved;
}
}
return null;
}
/**
* Resolve and returns the path of the first resolved file and null otherwise.
* @param files Files to look.
* @param context Context used to resolve.
*/
export function lookupSync(files, context = process.cwd()) {
for (const path of files) {
const resolved = isAbsolute(path) ? path : resolve(context, path);
if (isFileSync(resolved)) {
return resolved;
}
}
return null;
}
/**
* Looks from bottom to up for the target file/directory.
* @param startPath Path to start resolution. If a URL is provided, it must use the `file:` protocol.
* @param target Target file/directory.
*/
export function searchAbove(startPath, target) {
const path = join(startPath, target);
if (existsSync(path)) {
return path;
}
const parent = dirname(startPath);
if (parent != startPath) {
return searchAbove(parent, target);
}
return null;
}