@forwardimpact/libutil 0.1.85 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/calendar.js +10 -0
- package/src/index.js +1 -0
- package/src/runtime.js +74 -14
package/package.json
CHANGED
package/src/calendar.js
CHANGED
|
@@ -28,6 +28,16 @@ export function isoDate(input) {
|
|
|
28
28
|
return toDate(input).toISOString().slice(0, 10);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Format an input as a full ISO 8601 timestamp (`YYYY-MM-DDTHH:mm:ss.sssZ`,
|
|
33
|
+
* UTC). Pass `runtime.clock.now()` for "now"; never reads the wall clock.
|
|
34
|
+
* @param {Date|number|string} input
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function isoTimestamp(input) {
|
|
38
|
+
return toDate(input).toISOString();
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* Compute the ISO 8601 year-week for an input. `year` is the ISO week-year
|
|
33
43
|
* (not necessarily the calendar year for edge weeks).
|
package/src/index.js
CHANGED
package/src/runtime.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { spawn as nodeSpawn, execFile, spawnSync } from "node:child_process";
|
|
2
|
-
import nodeFsSync
|
|
2
|
+
import nodeFsSync, {
|
|
3
|
+
createReadStream as nodeCreateReadStream,
|
|
4
|
+
createWriteStream as nodeCreateWriteStream,
|
|
5
|
+
} from "node:fs";
|
|
3
6
|
import nodeFs from "node:fs/promises";
|
|
4
7
|
import { Finder } from "./finder.js";
|
|
5
8
|
|
|
@@ -16,27 +19,40 @@ import { Finder } from "./finder.js";
|
|
|
16
19
|
* @property {Object} fs
|
|
17
20
|
* Async filesystem surface (the `node:fs/promises` shape): `readFile`,
|
|
18
21
|
* `writeFile`, `readdir`, `stat`, `mkdir`, `access`, `copyFile`, `cp`, `rm`,
|
|
19
|
-
* `lstat`, `unlink`, `symlink`, `utimes`, `chmod
|
|
20
|
-
* `
|
|
22
|
+
* `lstat`, `unlink`, `symlink`, `utimes`, `chmod`, plus the two stream
|
|
23
|
+
* factories `createReadStream` / `createWriteStream` (the `node:fs` shape —
|
|
24
|
+
* the promises API has no stream factories, so they live on the async
|
|
25
|
+
* surface as the canonical streaming seam). A module destructures `fs` xor
|
|
26
|
+
* `fsSync`, never both (design Decision 7).
|
|
21
27
|
* @property {Object} fsSync
|
|
22
28
|
* Sync filesystem surface (the `node:fs` shape): `existsSync`,
|
|
23
29
|
* `readFileSync`, `writeFileSync`, `mkdirSync`, `readdirSync`, `statSync`,
|
|
24
|
-
* `openSync`, `closeSync`, `unlinkSync`.
|
|
30
|
+
* `openSync`, `readSync`, `closeSync`, `unlinkSync`.
|
|
25
31
|
* @property {Object} proc
|
|
26
32
|
* Process surface: `cwd()`, `env`, `argv`, `stdin`, `stdout.write`,
|
|
27
33
|
* `stderr.write`, `exit(code)`, `kill(pid, signal)` (a negative `pid`
|
|
28
|
-
* signals the process group, e.g. for daemon teardown),
|
|
29
|
-
*
|
|
34
|
+
* signals the process group, e.g. for daemon teardown), `pid` (this
|
|
35
|
+
* process's id — used to exclude self from process-group descendant scans),
|
|
36
|
+
* `platform` (the `process.platform` string — `"darwin"`/`"win32"`/`"linux"`
|
|
37
|
+
* — for per-platform path resolution), `on(event, handler)` (subscribe to
|
|
38
|
+
* process events such as `"SIGTERM"`/`"SIGINT"` so daemons register signal
|
|
39
|
+
* handlers through the collaborator instead of the global), and an
|
|
40
|
+
* `exitCode` accessor.
|
|
30
41
|
* @property {Object} clock
|
|
31
42
|
* Time surface: `now()`, `sleep(ms)`, `setTimeout(fn, ms)`,
|
|
32
|
-
* `clearTimeout(handle)`.
|
|
43
|
+
* `clearTimeout(handle)`, `setInterval(fn, ms)`, `clearInterval(handle)`.
|
|
44
|
+
* The interval handle mirrors the host timer (so callers may `.unref()` it).
|
|
33
45
|
* @property {Object} subprocess
|
|
34
46
|
* Subprocess surface: `run(cmd, args, opts) -> Promise<{stdout, stderr,
|
|
35
47
|
* exitCode}>` (async, buffered), `runSync(cmd, args, opts) -> {stdout,
|
|
36
48
|
* stderr, exitCode}` (synchronous, buffered — for the rare caller that
|
|
37
49
|
* cannot go async, e.g. a sync config accessor shelling to `gh auth
|
|
38
|
-
* token`), and `spawn(cmd, args, opts) -> {stdout, stderr, exitCode,
|
|
39
|
-
* where `stdout`/`stderr` are AsyncIterables
|
|
50
|
+
* token`), and `spawn(cmd, args, opts) -> {stdout, stderr, stdin, exitCode,
|
|
51
|
+
* signal, kill, pid}` where `stdout`/`stderr` are AsyncIterables,
|
|
52
|
+
* `exitCode`/`signal` are Promises (the terminating signal name or `null`),
|
|
53
|
+
* `stdin` is the child's writable (only when `opts.stdio` pipes stdin, else
|
|
54
|
+
* `null`), `kill(signal)` signals the child, and `pid` is its id (`undefined`
|
|
55
|
+
* on spawn failure).
|
|
40
56
|
* @property {Object} finder
|
|
41
57
|
* A constructed `Finder` (project path resolution + symlink management).
|
|
42
58
|
*/
|
|
@@ -81,6 +97,9 @@ export function createDefaultProc({ source = process, env = source.env } = {}) {
|
|
|
81
97
|
stderr: { write: (s) => source.stderr.write(s) },
|
|
82
98
|
exit: (code) => source.exit(code),
|
|
83
99
|
kill: (pid, signal) => source.kill(pid, signal),
|
|
100
|
+
pid: source.pid,
|
|
101
|
+
platform: source.platform,
|
|
102
|
+
on: (event, handler) => source.on(event, handler),
|
|
84
103
|
};
|
|
85
104
|
Object.defineProperty(proc, "exitCode", {
|
|
86
105
|
enumerable: true,
|
|
@@ -116,7 +135,7 @@ function lineIterator(stream) {
|
|
|
116
135
|
|
|
117
136
|
/**
|
|
118
137
|
* Build the clock surface backed by real timers.
|
|
119
|
-
* @returns {{now: () => number, sleep: (ms: number) => Promise<void>, setTimeout: Function, clearTimeout: Function}}
|
|
138
|
+
* @returns {{now: () => number, sleep: (ms: number) => Promise<void>, setTimeout: Function, clearTimeout: Function, setInterval: Function, clearInterval: Function}}
|
|
120
139
|
*/
|
|
121
140
|
export function createDefaultClock() {
|
|
122
141
|
return {
|
|
@@ -124,6 +143,8 @@ export function createDefaultClock() {
|
|
|
124
143
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
125
144
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
126
145
|
clearTimeout: (handle) => clearTimeout(handle),
|
|
146
|
+
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
147
|
+
clearInterval: (handle) => clearInterval(handle),
|
|
127
148
|
};
|
|
128
149
|
}
|
|
129
150
|
|
|
@@ -172,13 +193,45 @@ export function createDefaultSubprocess() {
|
|
|
172
193
|
|
|
173
194
|
const spawn = (cmd, args = [], opts = {}) => {
|
|
174
195
|
const child = nodeSpawn(cmd, args, opts);
|
|
196
|
+
let resolveSignal;
|
|
197
|
+
const signal = new Promise((r) => {
|
|
198
|
+
resolveSignal = r;
|
|
199
|
+
});
|
|
200
|
+
let resolveExit;
|
|
201
|
+
const exitCode = new Promise((r) => {
|
|
202
|
+
resolveExit = r;
|
|
203
|
+
});
|
|
204
|
+
child.on("close", (code, sig) => {
|
|
205
|
+
resolveSignal(sig ?? null);
|
|
206
|
+
resolveExit(code ?? 0);
|
|
207
|
+
});
|
|
208
|
+
// A spawn failure (e.g. ENOENT for a missing binary) emits an `error`
|
|
209
|
+
// event. With no listener Node rethrows it as an uncaughtException and
|
|
210
|
+
// crashes the whole process — even for callers that synchronously guard
|
|
211
|
+
// `pid === undefined`, because the event fires on a later tick. Mirror the
|
|
212
|
+
// run()/runSync() contract instead: resolve a 127 exit code and a null
|
|
213
|
+
// signal so the surface never rejects and never crashes. `resolveExit` is
|
|
214
|
+
// idempotent, so a `close` arriving after `error` is a no-op.
|
|
215
|
+
child.on("error", () => {
|
|
216
|
+
resolveSignal(null);
|
|
217
|
+
resolveExit(127);
|
|
218
|
+
});
|
|
175
219
|
return {
|
|
176
220
|
stdout: child.stdout ?? emptyAsyncIterable(),
|
|
177
221
|
stderr: child.stderr ?? emptyAsyncIterable(),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
222
|
+
// The child's writable stdin — present only when `opts.stdio` makes
|
|
223
|
+
// stdin a pipe (e.g. `["pipe", ...]`); `null` otherwise. A supervising
|
|
224
|
+
// caller writes its piped output into it.
|
|
225
|
+
stdin: child.stdin ?? null,
|
|
226
|
+
exitCode,
|
|
227
|
+
// Resolves with the terminating signal name (or `null` on a clean exit),
|
|
228
|
+
// alongside `exitCode`. Supervisors that distinguish a SIGTERM teardown
|
|
229
|
+
// from a crash read it; clean-exit callers can ignore it.
|
|
230
|
+
signal,
|
|
181
231
|
kill: (signal) => child.kill(signal),
|
|
232
|
+
// The child's pid — `undefined` if the spawn failed. Detached callers
|
|
233
|
+
// read it to derive the process-group id for group teardown.
|
|
234
|
+
pid: child.pid,
|
|
182
235
|
};
|
|
183
236
|
};
|
|
184
237
|
|
|
@@ -215,7 +268,14 @@ function emptyAsyncIterable() {
|
|
|
215
268
|
* @returns {Readonly<Runtime>}
|
|
216
269
|
*/
|
|
217
270
|
export function createDefaultRuntime({ env = process.env } = {}) {
|
|
218
|
-
|
|
271
|
+
// The async fs surface is `node:fs/promises` augmented with the two stream
|
|
272
|
+
// factories (which only exist on `node:fs`), so streaming consumers never
|
|
273
|
+
// import `node:fs` directly.
|
|
274
|
+
const fs = {
|
|
275
|
+
...nodeFs,
|
|
276
|
+
createReadStream: nodeCreateReadStream,
|
|
277
|
+
createWriteStream: nodeCreateWriteStream,
|
|
278
|
+
};
|
|
219
279
|
const fsSync = nodeFsSync;
|
|
220
280
|
const proc = createDefaultProc({ source: process, env });
|
|
221
281
|
const clock = createDefaultClock();
|