UNPKG

4.64 kBJavaScriptView Raw
1import { exec as cpExec, spawn as cpSpawn } from "node:child_process";
2import { lstatSync, readdirSync } from "node:fs";
3import { lstat, readdir } from "node:fs/promises";
4import { posix } from "node:path";
5import { promisify } from "node:util";
6
7const internalExec = promisify(cpExec);
8
9/**
10 * Join all arguments together and normalize the resulting path. Arguments must be
11 * strings. Using Node.js built-in path.posix.join().
12 * Which forces use of Posix path separators, '/'.
13 *
14 * @param {...string} paths
15 * @returns {string}
16 */
17export function pathJoin(...paths) {
18 return posix.join(...paths);
19}
20
21/**
22 * Wrap around Node.js child_process#exec. Resolving when the sub process has exited. The
23 * resulting object contains the 'exitCode' of the sub process.
24 *
25 * @since 0.1.0
26 *
27 * @param {string} command
28 * @param {import("child_process").ExecOptions} [opts={}]
29 * @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
30 */
31export async function exec(command, opts = {}) {
32 try {
33 const promise = internalExec(command, { encoding: "utf8", ...opts });
34 const { stdout, stderr } = await promise;
35
36 return {
37 stdout,
38 stderr,
39 exitCode: promise.child.exitCode ?? 0,
40 };
41 } catch (/** @type {any} */ e) {
42 return {
43 stdout: e.stdout ?? "",
44 stderr: e.stderr ?? "",
45 exitCode: e.code ?? 1,
46 };
47 }
48}
49
50/**
51 * Wrap around Node.js child_process#spawn. Resolving when the sub process has exited. The
52 * resulting object contains the 'exitCode' of the sub process.
53 * By default 'stdio' is inherited from the current process.
54 *
55 * @since 0.1.0
56 *
57 * @param {string} command
58 * @param {string[]} args
59 * @param {import("child_process").SpawnOptions} [opts={}]
60 * @returns {Promise<{exitCode: number}>}
61 */
62export function spawn(command, args, opts = {}) {
63 return new Promise((resolve, reject) => {
64 const sp = cpSpawn(command, args, { stdio: "inherit", ...opts });
65
66 const exitHandler = (signal) => {
67 sp.kill(signal);
68 };
69
70 process.once("exit", exitHandler);
71
72 sp.once("error", (...args) => {
73 process.removeListener("exit", exitHandler);
74
75 // eslint-disable-next-line prefer-promise-reject-errors
76 return reject(...args);
77 });
78
79 sp.once("exit", (code) => {
80 process.removeListener("exit", exitHandler);
81
82 resolve({ exitCode: code ?? 0 });
83 });
84 });
85}
86
87/**
88 * Read a readable stream completely, and return as Buffer
89 *
90 * @since 0.1.0
91 *
92 * @param {NodeJS.ReadableStream} stream
93 * @returns {Promise<Buffer>}
94 */
95export async function streamToBuffer(stream) {
96 // @ts-ignore
97 if (!stream || typeof stream._read !== "function") {
98 return Buffer.from([]);
99 }
100
101 return await new Promise((resolve, reject) => {
102 const buffers = [];
103
104 stream.on("data", function (chunk) {
105 buffers.push(chunk);
106 });
107 stream.on("end", function () {
108 resolve(Buffer.concat(buffers));
109 });
110 stream.on("error", function (err) {
111 reject(err);
112 });
113 });
114}
115
116/**
117 * Recursively act on all files in a directory, awaiting on callback calls.
118 *
119 * @since 0.1.0
120 *
121 * @param {string} dir
122 * @param {(file: string) => (void|Promise<void>)} cb
123 * @param {import("../types/advanced-types.js").ProcessDirectoryOptions} [opts]
124 */
125export async function processDirectoryRecursive(
126 dir,
127 cb,
128 opts = {
129 skipDotFiles: true,
130 skipNodeModules: true,
131 },
132) {
133 for (const file of await readdir(dir, { encoding: "utf8" })) {
134 if (opts.skipNodeModules && file === "node_modules") {
135 continue;
136 }
137 if (opts.skipDotFiles && file[0] === ".") {
138 continue;
139 }
140
141 const newPath = pathJoin(dir, file);
142 const stat = await lstat(newPath);
143 if (stat.isDirectory()) {
144 await processDirectoryRecursive(newPath, cb, opts);
145 } else if (stat.isFile()) {
146 await cb(newPath);
147 }
148 }
149}
150
151/**
152 * Sync version of processDirectoryRecursive
153 *
154 * @since 0.1.0
155 *
156 * @param {string} dir
157 * @param {(file: string) => (void)} cb
158 * @param {import("../types/advanced-types.js").ProcessDirectoryOptions} [opts]
159 */
160export function processDirectoryRecursiveSync(
161 dir,
162 cb,
163 opts = {
164 skipDotFiles: true,
165 skipNodeModules: true,
166 },
167) {
168 for (const file of readdirSync(dir, { encoding: "utf8" })) {
169 if (opts.skipNodeModules && file === "node_modules") {
170 continue;
171 }
172 if (opts.skipDotFiles && file[0] === ".") {
173 continue;
174 }
175
176 const newPath = pathJoin(dir, file);
177 const stat = lstatSync(newPath);
178 if (stat.isDirectory()) {
179 processDirectoryRecursiveSync(newPath, cb, opts);
180 } else if (stat.isFile()) {
181 cb(newPath);
182 }
183 }
184}