diff --git a/.github/workflows/publish-jsr.yml b/.github/workflows/publish-jsr.yml index e9cbe07..657372b 100644 --- a/.github/workflows/publish-jsr.yml +++ b/.github/workflows/publish-jsr.yml @@ -22,7 +22,7 @@ jobs: with: deno-version: v2.x - name: dry-run - run: deno publish --dry-run --allow-slow-types --no-check --unstable-fs --unstable-ffi + run: deno publish --dry-run --unstable-fs --unstable-ffi - name: publish if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' - run: deno publish --allow-slow-types --no-check --unstable-fs --unstable-ffi + run: deno publish --unstable-fs --unstable-ffi diff --git a/deno.json b/deno.json index c1c25fa..82c793a 100644 --- a/deno.json +++ b/deno.json @@ -37,17 +37,11 @@ "tasks": { "test": "deno test --parallel --unstable-fs --unstable-ffi --allow-all", "typecheck": "deno check ./mod.ts", - "publish:dry": "deno publish --dry-run --allow-slow-types --no-check --allow-dirty --unstable-fs --unstable-ffi" + "publish:dry": "deno publish --dry-run --allow-dirty --unstable-fs --unstable-ffi" }, "lint": { "include": ["src/", "mod.ts"], - "exclude": ["**/*.test.ts", "vendor/", "src/hooks/useTestConfig.ts"], - "rules": { - // Prototype extensions (Array.compact, Promise.swallow, etc.) use - // `declare global`, which JSR forbids without --allow-slow-types. - // Explicit public return types are fixed; globals are a larger refactor. - "exclude": ["no-slow-types"] - } + "exclude": ["**/*.test.ts", "vendor/", "src/hooks/useTestConfig.ts"] }, "test": { "include": ["src/"], diff --git a/mod.ts b/mod.ts index 723da6b..0267d6f 100644 --- a/mod.ts +++ b/mod.ts @@ -1,5 +1,4 @@ -import "./src/utils/misc.ts" -import { flatmap, validate } from "./src/utils/misc.ts" +import { chuzzle, compact, flatmap, insert, validate } from "./src/utils/misc.ts" import host from "./src/utils/host.ts" import type { SupportedArchitecture, SupportedPlatform } from "./src/utils/host.ts" @@ -9,7 +8,7 @@ import Path from "./src/utils/Path.ts" export * as types from "./src/types.ts" import * as pkg from "./src/utils/pkg.ts" -import { panic, PkgxError } from "./src/utils/error.ts" +import { panic, PkgxError, swallow } from "./src/utils/error.ts" import useConfig from "./src/hooks/useConfig.ts" import useOffLicense from "./src/hooks/useOffLicense.ts" import useCache from "./src/hooks/useCache.ts" @@ -30,7 +29,7 @@ import run, { RunError } from "./src/porcelain/run.ts" import porcelain_install from "./src/porcelain/install.ts" const utils = { - pkg, host, flatmap, validate, panic, ConsoleLogger + pkg, host, flatmap, validate, panic, ConsoleLogger, swallow, compact, insert, chuzzle } const hooks = { diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts index 6a80c2b..1d67485 100644 --- a/src/hooks/useCellar.ts +++ b/src/hooks/useCellar.ts @@ -1,5 +1,5 @@ import type { Package, PackageRequirement, Installation } from "../types.ts" -import { PkgxError } from "../utils/error.ts" +import { PkgxError, swallow } from "../utils/error.ts" import * as pkgutils from "../utils/pkg.ts" import SemVer from "../utils/semver.ts" import useConfig from "./useConfig.ts" @@ -32,7 +32,7 @@ export default function useCellar(): UseCellar { const keg = (pkg: Package) => shelf(pkg.project).join(`v${pkg.version}`) /// returns the `Installation` if the pkg is installed - const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(InstallationNotFoundError) + const has = (pkg: Package | PackageRequirement | Path) => swallow(resolve(pkg), InstallationNotFoundError) return { has, diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 70310d7..3606a1b 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -1,4 +1,4 @@ -import { flatmap } from "../utils/misc.ts" +import { compact, flatmap } from "../utils/misc.ts" import { deno } from "../deps.ts" import host from "../utils/host.ts" import Path from "../utils/Path.ts" @@ -54,7 +54,7 @@ export function ConfigDefault(env: Record = Deno.env.toObject()) const prefix = flatmap(env['PKGX_DIR']?.trim(), x => new Path(x)) ?? flatmap(env['XDG_DATA_HOME'], x => new Path(x).join("pkgx")) ?? home.join('.pkgx') - const pantries = env['PKGX_PANTRY_PATH']?.split(SEP).compact(x => flatmap(x.trim(), x => Path.abs(x) ?? Path.cwd().join(x))) ?? [] + const pantries = compact(env['PKGX_PANTRY_PATH']?.split(SEP) ?? [], x => flatmap(x.trim(), x => Path.abs(x) ?? Path.cwd().join(x))) const cache = ( (Deno.build.os == 'linux' ? flatmap(env["XDG_CACHE_HOME"], Path.abs) : undefined) ?? platform_cache_default(home, env) diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts index 84ce367..34032d1 100644 --- a/src/hooks/useDownload.ts +++ b/src/hooks/useDownload.ts @@ -3,11 +3,11 @@ const { crypto: crypto_, streams: { writeAll } } = deno const { crypto } = crypto_ import { encodeHex } from "@std/encoding" import { PkgxError, panic } from "../utils/error.ts" +import { chuzzle } from "../utils/misc.ts" import useConfig from "./useConfig.ts" import useFetch from "./useFetch.ts" import Path from "../utils/Path.ts" import * as fs from "node:fs" -import "../utils/misc.ts" interface DownloadOptions { src: URL @@ -119,7 +119,7 @@ async function the_meat({ src, logger, headers, dst }: DownloadOptions): Prom switch (rsp.status) { case 200: { - const sz = parseInt(rsp.headers.get("Content-Length")!).chuzzle() + const sz = chuzzle(parseInt(rsp.headers.get("Content-Length")!)) if (logger) logger({ src, dst, total: sz }) diff --git a/src/hooks/useInventory.ts b/src/hooks/useInventory.ts index cd872ff..47f4f96 100644 --- a/src/hooks/useInventory.ts +++ b/src/hooks/useInventory.ts @@ -3,7 +3,6 @@ import { DownloadError } from "./useDownload.ts" import SemVer from "../utils/semver.ts" import useFetch from "./useFetch.ts" import host from "../utils/host.ts" -import "../utils/misc.ts" import useConfig from "./useConfig.ts"; export interface Inventory { diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts index e9d4f5e..006bdd4 100644 --- a/src/hooks/usePantry.ts +++ b/src/hooks/usePantry.ts @@ -5,8 +5,8 @@ import { provides as cache_provides, available as cache_available, runtime_env a import type SemVer from "../utils/semver.ts" import * as semver from "../utils/semver.ts" import useMoustaches from "./useMoustaches.ts" -import { PkgxError } from "../utils/error.ts" -import { validate } from "../utils/misc.ts" +import { PkgxError, swallow } from "../utils/error.ts" +import { compact, insert, validate } from "../utils/misc.ts" import * as pkgutils from "../utils/pkg.ts" import useConfig from "./useConfig.ts" import host from "../utils/host.ts" @@ -79,7 +79,7 @@ export default function usePantry(): { for (const prefix of pantry_paths()) { for await (const path of _ls_pantry(prefix)) { const project = path.parent().relative({ to: prefix }) - if (seen.insert(project).inserted) { + if (insert(seen, project).inserted) { yield { project, path } } } @@ -149,7 +149,7 @@ export default function usePantry(): { } if (!isArray(node)) throw new PantryParseError(project) - return node.compact(x => { + return compact(node, x => { if (isPlainObject(x)) { x = x["executable"] } @@ -241,7 +241,7 @@ export default function usePantry(): { rv.push(proj) continue } - const yaml = await proj.yaml().swallow() + const yaml = await swallow(proj.yaml()) if (!yaml) { console.warn("warn: parse failure:", pkg.project) } else if (yaml["display-name"]?.toLowerCase() == name) { @@ -315,8 +315,7 @@ export function parse_pkgs_node(node: any): PackageRequirement[] { node = validate.obj(node) platform_reduce(node) - return Object.entries(node) - .compact(([project, constraint]) => + return compact(Object.entries(node), ([project, constraint]) => validatePackageRequirement(project, constraint)) } diff --git a/src/hooks/useShellEnv.ts b/src/hooks/useShellEnv.ts index 21919c2..169d982 100644 --- a/src/hooks/useShellEnv.ts +++ b/src/hooks/useShellEnv.ts @@ -1,4 +1,5 @@ import type { Installation } from "../types.ts" +import { insert } from "../utils/misc.ts" import usePantry from "./usePantry.ts" import host from "../utils/host.ts" @@ -64,7 +65,7 @@ async function map({installations}: Options): Promise> for (const installation of installations) { - if (!seen.insert(installation.pkg.project).inserted) { + if (!insert(seen, installation.pkg.project).inserted) { console.warn("pkgx: env is being duped:", installation.pkg.project) } diff --git a/src/plumbing/hydrate.ts b/src/plumbing/hydrate.ts index 6596a7a..e166428 100644 --- a/src/plumbing/hydrate.ts +++ b/src/plumbing/hydrate.ts @@ -1,4 +1,5 @@ import type { PackageRequirement, Package } from "../types.ts" +import { compact } from "../utils/misc.ts" import * as semver from "../utils/semver.ts" import usePantry from "../hooks/usePantry.ts" import { is_what } from "../deps.ts" @@ -144,7 +145,7 @@ export default async function hydrate( pkgs.push(...additional) //TODO strictly we need to record precisely the bootstrap version constraint - const bootstrap_required = new Set(pkgs.compact(({project}) => bootstrap.has(project) && project)) + const bootstrap_required = new Set(compact(pkgs, ({project}) => bootstrap.has(project) && project)) return { pkgs, diff --git a/src/plumbing/which.ts b/src/plumbing/which.ts index a3f69f3..1892ff9 100644 --- a/src/plumbing/which.ts +++ b/src/plumbing/which.ts @@ -1,6 +1,7 @@ import { provides as cache_provides, available as cache_available } from "../hooks/useSyncCache.ts" import usePantry, { PantryError } from "../hooks/usePantry.ts" import type { PackageRequirement } from "../types.ts" +import { swallow } from "../utils/error.ts" import * as semver from "../utils/semver.ts" export type WhichResult = PackageRequirement & { @@ -56,7 +57,7 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat for (const f of found) yield f found = [] } - const p = pantry.project(entry).provides().then(providers => { + const p = swallow(pantry.project(entry).provides().then(providers => { for (const provider of providers) { if (provider == arg0) { const constraint = new semver.Range("*") @@ -87,7 +88,7 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat } } } - }).swallow(PantryError) + }), PantryError) promises.push(p) diff --git a/src/utils/error.test.ts b/src/utils/error.test.ts index 49e7302..5986b84 100644 --- a/src/utils/error.test.ts +++ b/src/utils/error.test.ts @@ -1,5 +1,5 @@ import { assertEquals, assertRejects, assertThrows } from "@std/assert" -import { PkgxError, panic } from "../utils/error.ts" +import { PkgxError, panic, swallow } from "../utils/error.ts" Deno.test("errors", async test => { @@ -8,9 +8,9 @@ Deno.test("errors", async test => { }) await test.step("swallow", async () => { - await new Promise((_, reject) => reject(new BarError())).swallow(BarError) - await new Promise((_, reject) => reject(new BazError())).swallow(BarError) - assertRejects(() => new Promise((_, reject) => reject(new FooError())).swallow(BarError)) + await swallow(new Promise((_, reject) => reject(new BarError())), BarError) + await swallow(new Promise((_, reject) => reject(new BazError())), BarError) + assertRejects(() => swallow(new Promise((_, reject) => reject(new FooError())), BarError)) }) await test.step("new PkgxError()", () => { diff --git a/src/utils/error.ts b/src/utils/error.ts index 81030a8..04d0abc 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -4,17 +4,15 @@ export function panic(message?: string): never { throw new Error(message) } -declare global { - interface Promise { - swallow(errorClass?: new (...args: any) => any): Promise - } -} - -Promise.prototype.swallow = function(errorClass?: new (...args: any) => any) { - return this.catch((err: unknown) => { +export function swallow( + promise: Promise, + errorClass?: new (...args: any) => any, +): Promise { + return promise.catch((err: unknown) => { if (errorClass && !(err instanceof errorClass)) { - throw err; + throw err } + return undefined }) } diff --git a/src/utils/misc.test.ts b/src/utils/misc.test.ts index 8c178a2..4ebacec 100644 --- a/src/utils/misc.test.ts +++ b/src/utils/misc.test.ts @@ -1,5 +1,5 @@ import { assertEquals, assertRejects, assertThrows } from "@std/assert" -import { flatmap, validate } from "./misc.ts" +import { chuzzle, compact, flatmap, insert, validate } from "./misc.ts" import { isNumber } from "is-what" Deno.test("validate string", () => { @@ -45,17 +45,17 @@ Deno.test("async flatmap", async () => { }) Deno.test("chuzzle", () => { - assertEquals("".chuzzle(), undefined) - assertEquals("test".chuzzle(), "test") - assertEquals((1).chuzzle(), 1) - assertEquals(NaN.chuzzle(), undefined) + assertEquals(chuzzle(""), undefined) + assertEquals(chuzzle("test"), "test") + assertEquals(chuzzle(1), 1) + assertEquals(chuzzle(NaN), undefined) }) Deno.test("set insert", () => { const s = new Set([1, 2, 3]) - assertEquals(s.insert(1), {inserted: false}) - assertEquals(s.insert(4), {inserted: true}) + assertEquals(insert(s, 1), {inserted: false}) + assertEquals(insert(s, 4), {inserted: true}) assertEquals(s.size, 4) assertEquals(s.has(1), true) @@ -63,19 +63,19 @@ Deno.test("set insert", () => { }) Deno.test("array compact", () => { - assertEquals([1, 2, undefined, null, false, 3].compact(), [1, 2, 3]) - assertEquals([1, 2, undefined, null, false, 3].compact((n) => isNumber(n) && n * 2), [2, 4, 6]) + assertEquals(compact([1, 2, undefined, null, false, 3]), [1, 2, 3]) + assertEquals(compact([1, 2, undefined, null, false, 3], (n) => isNumber(n) && n * 2), [2, 4, 6]) // will fail to compile if the compiler cannot infer the type of the compact() return - assertEquals([1, 2, undefined, null, false as false | number, 3].compact()[0] + 1, 2) + assertEquals(compact([1, 2, undefined, null, false as false | number, 3])[0] + 1, 2) // verifies transforming the type gives singular type return - const foo = [1, 2, undefined, null, false, 3].compact((n) => isNumber(n) && `${n * 2}`) + const foo = compact([1, 2, undefined, null, false, 3], (n) => isNumber(n) && `${n * 2}`) assertEquals(foo, ["2", "4", "6"]) const throws = () => { throw Error("test error") } - assertEquals([()=>1, ()=>2, throws, ()=>3].compact((n) => n() * 2, { rescue: true }), [2, 4, 6]) - assertThrows(() => [()=>1, ()=>2, throws, ()=>3].compact((n) => n() * 2)) + assertEquals(compact([()=>1, ()=>2, throws, ()=>3], (n) => n() * 2, { rescue: true }), [2, 4, 6]) + assertThrows(() => compact([()=>1, ()=>2, throws, ()=>3], (n) => n() * 2)) }) diff --git a/src/utils/misc.ts b/src/utils/misc.ts index effd242..e4a73cb 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -20,6 +20,7 @@ function validate_arr(input: unknown): Array { return input } + const validate = { str: validate_str, obj: validate_plain_obj, @@ -28,36 +29,28 @@ const validate = { export { validate } -////////////////////////////////////////////////////////////// base extensions -type Falsy = false | 0 | '' | null | undefined; - -declare global { - interface Array { - compact(): Array>; - compact(body: (t: T) => S | Falsy): Array - compact(body?: (t: T) => S | T | Falsy, opts?: { rescue: boolean }): Array - } - interface Set { - insert(t: T): { inserted: boolean } - } -} +////////////////////////////////////////////////////////////// base extensions +type Falsy = false | 0 | '' | null | undefined -Set.prototype.insert = function(t: T) { - if (this.has(t)) { +export function insert(set: Set, t: T): { inserted: boolean } { + if (set.has(t)) { return {inserted: false} } else { - this.add(t) + set.add(t) return {inserted: true} } } -Array.prototype.compact = function(body?: (t: T) => S | Falsy, opts?: { rescue: boolean }): S[] { - const rv: S[] = [] - for (const e of this) { +export function compact(arr: Array): Array> +export function compact(arr: Array, body: (t: T) => S | Falsy, opts?: { rescue: boolean }): Array +export function compact(arr: Array, body?: (t: T) => S | T | Falsy, opts?: { rescue: boolean }): Array +export function compact(arr: Array, body?: (t: T) => S | Falsy, opts?: { rescue: boolean }): Array { + const rv: Array = [] + for (const e of arr) { try { const f = body ? body(e) : e - if (f) rv.push(f) + if (f) rv.push(f as S | T) } catch (err) { if (opts === undefined || opts.rescue === false) throw err } @@ -90,30 +83,13 @@ export function flatmap(t: Promise | (T | Falsy), body: (t: T) } } -// export async function async_flatmap(t: Promise, body: (t: T) => Promise, opts?: {rescue?: boolean}): Promise { -// try { -// const tt = await t -// if (tt) return await body(tt) || undefined -// } catch (err) { -// if (!opts?.rescue) throw err -// } -// } - //////////////////////////////////////////////////////// chuzzle -declare global { - interface String { - chuzzle(): string | undefined - } - - interface Number { - chuzzle(): number | undefined +export function chuzzle(input: string): string | undefined +export function chuzzle(input: number): number | undefined +export function chuzzle(input: string | number): string | number | undefined { + if (typeof input === "string") { + return input.trim() || undefined + } else { + return Number.isNaN(input) ? undefined : input } } - -String.prototype.chuzzle = function() { - return this.trim() || undefined -} - -Number.prototype.chuzzle = function() { - return Number.isNaN(this) ? undefined : this as number -}