UNPKG

@plugjs/plug

Version:
231 lines 8.15 kB
import { mkdtempSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { BuildFailure, assert, assertPromises } from "./asserts.js"; import { requireContext } from "./async.js"; import { Files } from "./files.js"; import { rm } from "./fs.js"; import { $gry, $p, $plur, $wht, $ylw, log } from "./logging.js"; import { commonPath, getAbsoluteParent, getCurrentWorkingDirectory, resolveDirectory, resolveFile, } from "./paths.js"; import { PipeImpl } from "./pipe.js"; import { RunBuild } from "./plugs/build.js"; import { JsoncError, parseJsonc } from "./utils.js"; import { execChild } from "./utils/exec.js"; import { parseOptions } from "./utils/options.js"; import { walk } from "./utils/walk.js"; /** Return the current execution {@link Context} */ export function context() { return requireContext(); } /* Overload */ export function find(...args) { const { params: globs, options } = parseOptions(args, {}); const context = requireContext(); return new PipeImpl(context, Promise.resolve().then(async () => { const directory = options.directory ? context.resolve(options.directory) : getCurrentWorkingDirectory(); const builder = Files.builder(directory); for await (const file of walk(directory, globs, options)) { builder.add(file); } return builder.build(); })); } export async function invokeBuild(buildFile, tasksOrOptions, maybeOptions) { const [tasks, options = {}] = typeof tasksOrOptions === 'string' ? [[tasksOrOptions], maybeOptions] : Array.isArray(tasksOrOptions) ? [tasksOrOptions, maybeOptions] : typeof tasksOrOptions === 'object' ? [['default'], tasksOrOptions] : [['default'], {}]; if (tasks.length === 0) tasks.push('default'); const { coverageDir, forceModule, ...props } = options; const forkOptions = { coverageDir, forceModule }; const context = requireContext(); const file = context.resolve(buildFile); const dir = getAbsoluteParent(file); const files = Files.builder(dir).add(file).build(); return new RunBuild(tasks, props, forkOptions) .pipe(files, context) .then(() => void 0); } /** * Recursively remove the specified directory _**(use with care)**_. */ export async function rmrf(directory) { const context = requireContext(); const dir = context.resolve(directory); assert(dir !== getCurrentWorkingDirectory(), `Cowardly refusing to wipe current working directory ${$p(dir)}`); assert(dir !== context.resolve('@'), `Cowardly refusing to wipe build file directory ${$p(dir)}`); /* coverage ignore if */ if (!resolveDirectory(dir)) { log.info('Directory', $p(dir), 'not found'); return; } log.notice('Removing directory', $p(dir), 'recursively'); await rm(dir, { recursive: true }); } /** * Merge the results of several {@link Pipe}s into a single one. * * Merging is performed _in parallel_. When serial execution is to be desired, * we can merge the awaited _result_ of the {@link Pipe}. * * For example: * * ``` * const pipe: Pipe = merge([ * await this.anotherTask1(), * await this.anotherTask2(), * ]) * ``` */ export function merge(pipes) { const context = requireContext(); return new PipeImpl(context, Promise.resolve().then(async () => { // No pipes? Just send off an empty pipe... if (pipes.length === 0) return new Files(); // Await for all pipes / files / files promises const awaited = await assertPromises(pipes); const results = awaited.filter((result) => result.length); // No files in anything to be merged? Again send off an empty pipe... if (results.length === 0) return new Files(); // Find the common directory between all the Files instances const [firstDir, ...otherDirs] = results.map((f) => f.directory); const directory = commonPath(firstDir, ...otherDirs); // Build our new files instance merging all the results return Files.builder(directory).merge(...results).build(); })); } /** * Create an empty _no-op_ {@link Pipe}. * * This is useful when creating tasks with conditional pipes and returning the * correct type, for example: * * ``` * if (someCondition) { * return find(...).pipe(...) * } else { * return noop() * } * ``` */ export function noop() { const context = requireContext(); return new PipeImpl(context, Promise.resolve(new Files())); } export function using(...args) { const { options, params } = parseOptions(args, { directory: '.' }); const context = requireContext(); const directory = context.resolve(options.directory); const files = Files.builder(directory).add(...params).build(); return new PipeImpl(context, Promise.resolve(files)); } /** * Resolve a (set of) path(s) into an {@link AbsolutePath}. * * If the path (or first component thereof) starts with `@...`, then the * resolved path will be relative to the directory containing the build file * where the current task was defined, otherwise it will be relative to the * current working directory. */ export function resolve(...paths) { return requireContext().resolve(...paths); } /** * Return an absolute path of the file if it exist on disk. * * See the comments on {@link resolve} to understand how paths are resolved. */ export function isFile(...paths) { const path = requireContext().resolve(...paths); return resolveFile(path); } /** * Return an absolute path of the directory if it exist on disk. * * See the comments on {@link resolve} to understand how paths are resolved. */ export function isDirectory(...paths) { const path = requireContext().resolve(...paths); return resolveDirectory(path); } /** * Create a temporary directory and return its {@link AbsolutePath}. * * The directory will be rooted in `/tmp` or wherever `os.tmpdir()` decides. */ export function mkdtemp() { const prefix = join(tmpdir(), 'plugjs-'); const path = mkdtempSync(prefix); return resolve(path); } /** * Execute a command and await for its result from within a task. * * For example: * * ``` * import { exec } from '@plugjs/plugjs' * * export default build({ * async runme() { * await exec('ls', '-la', '/') * // or similarly letting the shell interpret the command * await exec('ls -la /', { shell: true }) * }, * }) * ``` * * @param cmd The command to execute * @param args Any additional argument for the command to execute * @param options Extra {@link ExecChildOptions | options} for process execution */ export function exec(cmd, ...args) { const { params, options } = parseOptions(args); return execChild(cmd, params, options, requireContext()); } /** * Read and parse a JSON/JSONC file, throwing an error if not found. * * @params file The JSON file to parse */ export function parseJson(file, strict = false) { const jsonFile = requireContext().resolve(file); let jsonText; try { jsonText = readFileSync(jsonFile, 'utf-8'); } catch (error) { if (error.code === 'ENOENT') log.fail(`File ${$p(jsonFile)} not found`); if (error.code === 'EACCES') log.fail(`File ${$p(jsonFile)} can not be accessed`); log.fail(`Error reading ${$p(jsonFile)}`, error); } try { return parseJsonc(jsonText, { disallowComments: strict, allowTrailingComma: !strict, }); } catch (error) { if (error instanceof JsoncError) { const errors = error.errors; log.error(`Found ${$plur(errors.length, 'error', 'errors')} parsing ${$p(jsonFile)}`); for (const e of errors) { log.error(` ${$wht(e.code)} ${$gry('at line')} ${$ylw(e.line)}${$gry(', column')} ${$ylw(e.column)}`); } throw new BuildFailure(); } else throw error; } } //# sourceMappingURL=helpers.js.map