UNPKG

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