@zyplux/util
v0.8.0
Published
Small utilities: assertions, polling, bounded-concurrency mapping, zod-validated JSON reads, repo-URL normalization, package.json/pyproject manifest schemas, and a git/gh shell harness
Maintainers
Readme
@zyplux/util
Small utilities — assertions, polling, bounded-concurrency mapping, zod-validated JSON parsing (from a string, a file, or a URL) and TOML parsing (from a string), repo-URL normalization, package.json/pyproject.toml manifest schemas, and a git/gh shell harness. Ships compiled JS with type declarations, importable from Node 26.
Install
pnpm add @zyplux/util zodUse
import { ensure } from '@zyplux/util/assert';
import { FetchError, http } from '@zyplux/util/http';
import { parseJson, readJson, readJsonSync } from '@zyplux/util/json';
import { $, readTrimmed } from '@zyplux/util/shell';
import * as z from 'zod';
const Pkg = z.object({ version: z.string() });
const { version } = await readJson(new URL('./package.json', import.meta.url), Pkg);
const { version: pinned } = readJsonSync(new URL('./package.json', import.meta.url), Pkg);
const config = parseJson(process.env['APP_CONFIG'] ?? '{}', Pkg);
const Health = z.object({ ok: z.boolean() });
try {
const health = await http.get('https://example.com/health').json(Health);
} catch (error) {
if (error instanceof FetchError && error.response.status === 404) {
// react to a missing resource
}
}
const branch = await readTrimmed($.git.revParse('HEAD', { abbrevRef: true }));
ensure(branch !== 'main', 'refusing to run on main');parseJson/parseTomlparse a string and validate it against a zod schema (throwing on bad syntax or shape), for text you already hold (a subprocess's stdout, a manifest you read);readJson/readJsonSyncdo the same from a JSON file (async vianode:fs/promises, sync vianode:fs).tryParseJson/tryParseTomlare the tolerant siblings: they parse-and-validate but return the value orundefinedon any failure (bad syntax or shape), so a caller scanning many files readsconst pkg = tryParseJson(text, Schema); if (pkg === undefined) continue;with no result-unwrapping. When you want the error rather than just the value, wrap the strict parser yourself:attempt(() => parseJson(text, Schema)).attempt(fn)/attemptAsync(fn)run a thunk and fold its return or thrown error into aSafeResult<T>—{ ok: true; data }or{ ok: false; error }; they are the engine behind the tolerant helpers and the path to take when you want the error, not just the value, without a barecatch.httpis a ky-style client (http.get(url),.post, …) whoseResponsePromiseexposes.json(schema),.text(), and.response(). It throwsFetchError(carrying theResponse) on a non-ok status and aZodErroron a bad shape, so consumers can catch or react rather than guess at a swallowedundefined..safeJson(schema)is the non-throwing sibling of.json(schema), returning aSafeResult<T>but folding the fetch and non-ok status into it too.fetchJson(url, schema)is the tolerant convenience for the common best-effort case: aGETthat resolves to the validated body orundefinedon any failure (non-ok, network, or bad shape) — for remote data whose absence is a normal outcome rather than an error.isHttpOk(url, init?)resolves to whether a request returned a 2xx status (itawaitsfetchand reads.ok), for existence/availability checks where only the status matters and no body is read; passinitfor headers or aHEADmethod.poll(probe, options?)retries an async probe.untilstops on a match;whilekeeps retrying on a match. Each accepts a value compared withObject.is, or a synchronous predicate of the probe’s resolved result. These options are mutually exclusive; literal values must match the probe’s resolved return type or beundefined. With neither option, polling waits for any defined result. Defaults are five attempts, one second apart;attemptsandintervalMsoverride them. It returns the matching result orundefinedon exhaustion, without sleeping after the final attempt.onExpirysupplies an error message to throw when attempts are exhausted. With it, a default lookup returns a defined value; an explicit condition can still successfully matchundefined. Without it, expiry returnsundefined. Probe and predicate errors propagate immediately. Check the returned result separately when completing the wait does not guarantee that the operation succeeded.mapWithConcurrencymaps over items with a fixed worker limit, preserving input order.normalizeRepoUrlreduces the many shapes a VCS url takes (git+https,git@host:owner/repo,github:owner/repo, barehost/owner/repo,…/tree/main/sub) to a canonicalhttps://host/owner/repo, orundefinedwhen the value is not a repository.$is a small shell-command harness with typedgit/ghhelpers.runPassthrough(argv, cwd?)from@zyplux/util/execshares the parent's stdin, stdout, and stderr with a command, allowing interactive prompts and terminal detection. It resolves when the command succeeds and rejects on startup failure, a nonzero exit, or termination by a signal.$.ghreads return Zod-validated objects or arrays. Select supported fields with a nonemptyjsonarray, such as$.gh.run.view(id, { json: ['status', 'conclusion'] }); only those fields appear in the inferred result. PR draft flags are booleans, PR numbers and run IDs are numbers. Run conclusions preserve the empty string while pending.$.gh.pr.reviews(slug, number)returns reviews with author logins and commit IDs.$.gh.release.exists(tag)checks an exact tag, returningfalsefor a missing release and propagating lookup errors. Actions returnPromise<void>. Raw CLI commands remain available through$.@zyplux/util/contractsexports reusable structural zod primitives (StringRecordSchema,LooseRecordSchema,StringArraySchema,UnknownArraySchema,UnknownArrayRecordSchema,IdSchema,VersionKeySchema) that other schema modules compose from.@zyplux/util/contractsexports tolerant zod schemas (PackageJsonSchema,PyProjectSchema) and inferred types for readingpackage.json(incl. pnpm workspace/catalog) andpyproject.toml(PEP 621 + PEP 735 + uv) manifests;@zyplux/util/manifestexports dependency-name extractors (npmDependencyNames,pythonRequirementNames,repositoryUrl,normalizePythonName), andfindManifests(dir), which listsgit-tracked manifests across one or many repos underdir(so.gitignoredecides what is skipped — no node_modules, no build output, no untracked clones).
Timing and source analysis
LapTimer is available from @zyplux/util/lap-timer. It measures named operations, nested laps, and concurrent operation arrays, preserving durations on failure. timings contains the named measurements; timingsWithTotal adds the elapsed total. lapAndStop completes a timer, and importTimings incorporates externally measured durations.
@zyplux/util/module-references exports collectModuleReferences(sourceFile), which reads TypeScript imports and re-exports, including type-only and value bindings. Install the optional TypeScript peer to use it:
pnpm add typescript@zyplux/util/type-dependencies exports findTypeDependencyViolations({ imports, packages, sharedTypeSurfaces }). It checks public package entries and type ownership using caller-supplied module references and package declarations. Repository-specific architecture policy stays with the consumer.
@zyplux/util/workspace-architecture reads workspace packages and the shared cerberus.toml architecture declaration. Its export-map and ownership helpers support conditional exports, blocked entries, wildcard subpaths, and nested package directories.
