UNPKG

@surface/rwx

Version:

Provides read, write and execute capabilities.

235 lines (234 loc) 8.14 kB
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; }