From d0835c6fa62e7c2c151dfc82696d6b28d3318cf6 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Fri, 30 Jun 2023 10:55:58 -0400 Subject: [PATCH 1/6] rm TeaError; add specific error classes --- src/hooks/useCellar.ts | 18 +++- src/hooks/useDownload.ts | 72 ++++++++------ src/hooks/useInventory.ts | 10 +- src/hooks/usePantry.ts | 45 +++++++-- src/plumbing/resolve.ts | 13 ++- src/plumbing/which.ts | 4 +- src/utils/error.test.ts | 52 +--------- src/utils/error.ts | 195 +------------------------------------- 8 files changed, 112 insertions(+), 297 deletions(-) diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts index 094af0c..aad5d08 100644 --- a/src/hooks/useCellar.ts +++ b/src/hooks/useCellar.ts @@ -4,6 +4,15 @@ import SemVer from "../utils/semver.ts" import useConfig from "./useConfig.ts" import Path from "../utils/Path.ts" +class InstallationNotFoundError extends Error { + pkg: Package | PackageRequirement + + constructor(pkg: Package | PackageRequirement) { + super(`not found: ${pkgutils.str(pkg)}`) + this.pkg = pkg + } +} + export default function useCellar() { const config = useConfig() @@ -14,7 +23,9 @@ export default function 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(/^not-found:/) + const has = (pkg: Package | PackageRequirement | Path) => + resolve(pkg) + .swallow((e: unknown) => e instanceof InstallationNotFoundError) return { has, @@ -70,12 +81,13 @@ export default function useCellar() { if (version) { const path = installations.find(({pkg: {version: v}}) => v.eq(version))!.path return { path, pkg: { project: pkg.project, version } } + } else { + throw new InstallationNotFoundError(pkg) } } - throw new Error(`not-found:${pkgutils.str(pkg)}`) })() if (await vacant(installation.path)) { - throw new Error(`not-found: ${pkgutils.str(installation.pkg)}`) + throw new InstallationNotFoundError(installation.pkg) } return installation } diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts index c4dd89e..71aa6d3 100644 --- a/src/hooks/useDownload.ts +++ b/src/hooks/useDownload.ts @@ -1,7 +1,7 @@ import { deno } from "../deps.ts" const { crypto: crypto_, streams: { writeAll } } = deno const { toHashString, crypto } = crypto_ -import TeaError, { panic } from "../utils/error.ts" +import { panic } from "../utils/error.ts" import useConfig from "./useConfig.ts" import useFetch from "./useFetch.ts" import Path from "../utils/Path.ts" @@ -15,42 +15,52 @@ interface DownloadOptions { logger?: (info: {src: URL, dst: Path, rcvd?: number, total?: number }) => void } +export class DownloadError extends Error { + status: number + src: URL + headers?: Record + + constructor(status: number, opts: { src: URL, headers?: Record}) { + super(`http: ${status}: ${opts.src}`) + this.name = 'DownloadError' + this.status = status + this.src = opts.src + this.headers = opts.headers + } +} + const tmpname = (dst: Path) => dst.parent().join(dst.basename() + ".incomplete") async function download(opts: DownloadOptions, chunk?: (blob: Uint8Array) => Promise): Promise { - try { - const [dst, stream] = await the_meat(opts) - - if (stream || chunk) { - const reader = stream ?? fs.createReadStream(dst.string) - - const writer = await (() => { - if (stream) { - dst.parent().mkdir('p') - return Deno.open(tmpname(dst).string, {write: true, create: true, truncate: true}) - } - })() - - for await (const blob of reader) { - const pp: Promise[] = [] - if (writer) pp.push(writeAll(writer, blob)) - if (chunk) pp.push(chunk(blob)) - await Promise.all(pp) - } + const [dst, stream] = await the_meat(opts) - if (reader instanceof fs.ReadStream) { - reader.close() - } - if (writer) { - writer.close() - tmpname(dst).mv({ to: dst, force: true }) + if (stream || chunk) { + const reader = stream ?? fs.createReadStream(dst.string) + + const writer = await (() => { + if (stream) { + dst.parent().mkdir('p') + return Deno.open(tmpname(dst).string, {write: true, create: true, truncate: true}) } + })() + + for await (const blob of reader) { + const pp: Promise[] = [] + if (writer) pp.push(writeAll(writer, blob)) + if (chunk) pp.push(chunk(blob)) + await Promise.all(pp) } - return dst - } catch (cause) { - throw new TeaError('http', {cause, ...opts}) + if (reader instanceof fs.ReadStream) { + reader.close() + } + if (writer) { + writer.close() + tmpname(dst).mv({ to: dst, force: true }) + } } + + return dst } function cache({ for: url }: {for: URL}): Path { @@ -79,7 +89,7 @@ export default function useDownload() { /// internal -async function the_meat({ src, headers, logger, dst }: DownloadOptions): Promise<[Path, ReadableStream | undefined, number | undefined]> +async function the_meat({ src, logger, headers, dst }: DownloadOptions): Promise<[Path, ReadableStream | undefined, number | undefined]> { const hash = cache({ for: src }) const mtime_entry = hash.join("mtime") @@ -136,6 +146,6 @@ async function the_meat({ src, headers, logger, dst }: DownloadOptions): Prom return [dst, undefined, sz] } default: - throw new Error(`${rsp.status}: ${src}`) + throw new DownloadError(rsp.status, { src, headers }) } } diff --git a/src/hooks/useInventory.ts b/src/hooks/useInventory.ts index d12d6ae..903fad4 100644 --- a/src/hooks/useInventory.ts +++ b/src/hooks/useInventory.ts @@ -1,5 +1,5 @@ import { Package, PackageRequirement } from "../types.ts" -import TeaError, * as error from "../utils/error.ts" +import { DownloadError } from "./useDownload.ts" import SemVer from "../utils/semver.ts" import useFetch from "./useFetch.ts" import host from "../utils/host.ts" @@ -33,8 +33,7 @@ const get = async (rq: PackageRequirement | Package) => { const rsp = await useFetch(url) if (!rsp.ok) { - const cause = new Error(`${rsp.status}: ${url}`) - throw new TeaError('http', {cause}) + throw new DownloadError(rsp.status, {src: url}) } const releases = await rsp.text() @@ -52,10 +51,7 @@ const get = async (rq: PackageRequirement | Package) => { } export default function useInventory() { - return { - select: error.wrap(select, 'http'), - get - } + return { select, get } } export const _internals = { get } diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts index 9ab7b04..410781c 100644 --- a/src/hooks/usePantry.ts +++ b/src/hooks/usePantry.ts @@ -4,7 +4,6 @@ import { validatePackageRequirement } from "../utils/hacks.ts" import { Package, Installation } from "../types.ts" import useMoustaches from "./useMoustaches.ts" import { validate } from "../utils/misc.ts" -import TeaError from "../utils/error.ts" import SemVer from "../utils/semver.ts" import useConfig from "./useConfig.ts" import host from "../utils/host.ts" @@ -15,6 +14,32 @@ export interface Interpreter { args: string[] } +type PantryErrorCode = 'not-found' | 'parse-error' + +export class PantryError extends Error { + code: PantryErrorCode + + // deno-lint-ignore no-explicit-any + constructor(code: PantryErrorCode, ctx: any) { + let msg: string + + switch (code) { + case 'not-found': + if (ctx instanceof Path) { + msg = `pantry not found: ${ctx}` + } else { + msg = `pkg not found: ${ctx}` + } + break + case 'parse-error': + msg = `package.yml parse error: ${ctx.filename || ctx.project || ctx}` + } + super(msg) + this.code = code + this.cause = ctx.cause + } +} + export default function usePantry() { const config = useConfig() const prefix = config.prefix.join('tea.xyz/var/pantry/projects') @@ -35,7 +60,7 @@ export default function usePantry() { const yaml = (() => { for (const prefix of pantry_paths()) { - if (!prefix.exists()) throw new TeaError('not-found: pantry', { path: prefix.parent() }) + if (!prefix.exists()) throw new PantryError('not-found', prefix.parent()) const dir = prefix.join(project) const filename = dir.join("package.yml") if (!filename.exists()) continue @@ -44,9 +69,9 @@ export default function usePantry() { return () => memo ?? (memo = filename.readYAML() .then(validate.obj) - .catch(cause => { throw new TeaError('parser: pantry: package.yml', {cause, project, filename}) })) + .catch(cause => { throw new PantryError('parse-error', {cause, project, filename}) })) } - throw new TeaError('not-found: pantry: package.yml', {project}, ) + throw new PantryError('not-found', project) })() const companions = async () => parse_pkgs_node((await yaml())["companions"]) @@ -61,7 +86,7 @@ export default function usePantry() { let { platforms } = await yaml() if (!platforms) return true if (isString(platforms)) platforms = [platforms] - if (!isArray(platforms)) throw new Error("bad-yaml") + if (!isArray(platforms)) throw new PantryError("parse-error", {project}) return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`) } @@ -73,7 +98,7 @@ export default function usePantry() { if (isPlainObject(node)) { node = node[host().platform] } - if (!isArray(node)) throw new Error("bad-yaml") + if (!isArray(node)) throw new PantryError("parse-error", project) return node.compact(x => { if (isPlainObject(x)) { @@ -180,7 +205,7 @@ export default function usePantry() { } if (rv.length == 0) { - throw new TeaError("not-found: pantry", {path: prefix}) + throw new PantryError("not-found", prefix) } return rv @@ -253,7 +278,7 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati // deno-lint-ignore no-explicit-any function transform(value: any): string { - if (!isPrimitive(value)) throw new Error(`invalid-env-value: ${JSON.stringify(value)}`) + if (!isPrimitive(value)) throw new PantryError('parse-error', JSON.stringify(value)) if (isBoolean(value)) { return value ? "1" : "0" @@ -271,7 +296,7 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati } else if (isNumber(value)) { return value.toString() } - throw new Error("unexpected-error") + throw new Error("unexpected error", {cause: value}) } } @@ -281,7 +306,7 @@ interface LsEntry { } async function* _ls_pantry(dir: Path): AsyncGenerator { - if (!dir.isDirectory()) throw new TeaError('not-found: pantry', { path: dir }) + if (!dir.isDirectory()) throw new PantryError('not-found', dir) for await (const [path, { name, isDirectory }] of dir.ls()) { if (isDirectory) { diff --git a/src/plumbing/resolve.ts b/src/plumbing/resolve.ts index 0aaa123..a56e1e4 100644 --- a/src/plumbing/resolve.ts +++ b/src/plumbing/resolve.ts @@ -1,7 +1,7 @@ import { Package, PackageRequirement, Installation } from "../types.ts" import useInventory from "../hooks/useInventory.ts" +import { str as pkgstr } from "../utils/pkg.ts" import useCellar from "../hooks/useCellar.ts" -import TeaError from "../utils/error.ts" /// NOTE resolves to bottles /// NOTE contract there are no duplicate projects in input @@ -17,6 +17,15 @@ export interface Resolution { pending: Package[] } +class ResolveError extends Error { + pkg: Package | PackageRequirement + + constructor(pkg: Package | PackageRequirement) { + super(`not-found: pkg: ${pkgstr(pkg)}`) + this.pkg = pkg + } +} + /// resolves a list of package specifications based on what is available in /// bottle storage if `update` is false we will return already installed pkgs /// that resolve so if we are resolving `node>=12`, node 13 is installed, but @@ -35,7 +44,7 @@ export default async function resolve(reqs: (Package | PackageRequirement)[], {u } else { const version = await inventory.select(req) if (!version) { - throw new TeaError("not-found: pkg.version", {pkg: req}) + throw new ResolveError(req) } const pkg = { version, project: req.project } rv.pkgs.push(pkg) diff --git a/src/plumbing/which.ts b/src/plumbing/which.ts index e5ff643..a2a2fe6 100644 --- a/src/plumbing/which.ts +++ b/src/plumbing/which.ts @@ -1,5 +1,5 @@ import { PackageRequirement } from "../types.ts" -import usePantry from "../hooks/usePantry.ts" +import usePantry, { PantryError } from "../hooks/usePantry.ts" import * as semver from "../utils/semver.ts" export type WhichResult = PackageRequirement & { @@ -50,7 +50,7 @@ export default async function(arg0: string, opts = { providers: true }): Promise } } } - }).swallow(/^parser: pantry: package.yml/) + }).swallow((e: unknown) => e instanceof PantryError) promises.push(p) if (opts.providers) { diff --git a/src/utils/error.test.ts b/src/utils/error.test.ts index c835809..c54ef3e 100644 --- a/src/utils/error.test.ts +++ b/src/utils/error.test.ts @@ -1,54 +1,8 @@ -import { assert, assertEquals, assertThrows } from "deno/testing/asserts.ts" -import TeaError, * as error from "./error.ts" -import SemVer from "./semver.ts" +import { assertThrows } from "deno/testing/asserts.ts" +import { panic } from "../utils/error.ts" Deno.test("errors", async test => { - await test.step("package not found", () => { - const err = new TeaError("not-found: tea -X: arg0", { arg0: "foo.com" }) - assertEquals(err.code(), "spilt-tea-003") - assertEquals(err.title(), "not-found: tea -X: arg0") - assert(err.message.includes("couldn’t find a pkg to provide: \`foo.com\`"), "message should be subsititued correctly") - }) - - await test.step("project not found in pantry", () => { - const err = new TeaError("not-found: pantry: package.yml", { project: "project-name" }) - assertEquals(err.code(), "spilt-tea-007") - assertEquals(err.title(), "not found in pantry: project-name") - assert(err.message.includes("Not in pantry: project-name"), "message should be subsititued correctly") - }) - - await test.step("wrap http error", () => { - let err: TeaError | undefined - try { - error.wrap(() => { - throw new Error("wrapped error") - }, "http")() - } catch (e) { - err = e - } - - assert(err, "error should be thrown") - assertEquals(err.code(), "spilt-tea-013") - assert(err.message.includes("wrapped error"), "message should be subsititued correctly") - }) - - await test.step("wrap Tea Error", () => { - let err: TeaError | undefined - try { - error.wrap(() => { - const pkg = { project: "foo.com", version: new SemVer("1.0.0") } - throw new TeaError('not-found: pkg.version', { pkg }) - }, "http")() - } catch (e) { - err = e - } - - assert(err, "error should be thrown") - assertEquals(err.code(), "spilt-tea-006") - assert(err.message.includes("we haven’t packaged foo.com=1.0.0."), "message should be subsititued correctly") - }) - await test.step("panic", () => { - assertThrows(() => error.panic("test msg"), "test msg") + assertThrows(() => panic("test msg"), "test msg") }) }) diff --git a/src/utils/error.ts b/src/utils/error.ts index 02c55ea..9fa93b5 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,208 +1,17 @@ -import { is_what, outdent, PlainObject } from "../deps.ts" -const { isPlainObject, isRegExp, isString } = is_what -const undent = outdent.default -import * as pkg from "./pkg.ts" - -type ID = - 'not-found: tea -X: arg0' | - 'not-found: exe/md: default target' | - 'not-found: exe/md: region' | - 'not-found: pkg.version' | - 'http' | - 'not-found: pantry' | - 'not-found: pantry: package.yml' | - 'parser: pantry: package.yml' | - 'not-found: dev-env' | - // 'not-found: srcroot' | - 'not-found: arg' | - '#helpwanted' | - 'confused: interpreter' - -export default class TeaError extends Error { - id: ID - ctx: PlainObject - - code() { - // starting at 3 ∵ https://tldp.org/LDP/abs/html/exitcodes.html - // https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6/+/refs/heads/tools_r20/sysroot/usr/include/sysexits.h - switch (this.id) { - case 'not-found: tea -X: arg0': return 'spilt-tea-003' - case 'not-found: exe/md: default target': return 'spilt-tea-004' - case 'not-found: exe/md: region': return 'spilt-tea-005' - case 'not-found: pkg.version': return 'spilt-tea-006' - case 'not-found: pantry: package.yml': return 'spilt-tea-007' - case 'not-found: dev-env': return 'spilt-tea-008' - case 'not-found: pantry': return 'spilt-tea-009' - case 'not-found: arg': return 'spilt-tea-010' - case 'parser: pantry: package.yml': return 'spilt-tea-011' - case '#helpwanted': return 'spilt-tea-012' - case 'http': return 'spilt-tea-013' - case 'confused: interpreter': return 'spilt-tea-14' - default: { - const exhaustiveness_check: never = this.id - throw new Error(`unhandled id: ${exhaustiveness_check}`) - }} - } - - title() { - switch (this.id) { - case 'not-found: pantry: package.yml': - return `not found in pantry: ${this.ctx.project}` - default: - return this.id - } - } - - constructor(id: ID, ctx: PlainObject) { - let msg = '' - switch (id) { - case 'not-found: tea -X: arg0': - msg = undent` - couldn’t find a pkg to provide: \`${ctx.arg0}\` - - https://github.com/teaxyz/pantry#contributing - - ` - break - case 'not-found: exe/md: region': - msg = `markdown section for \`${ctx.script}\` has no \`\`\`sh code block` - break - case 'not-found: exe/md: default target': - if (ctx.requirementsFile) { - msg = `markdown section \`# Getting Started\` not found in \`${ctx.requirementsFile}\`` - } else { - msg = undent` - no \`README.md\` or \`package.json\` found. - ` - } - break - case 'not-found: pantry': - if (ctx.path) { - msg = `no pantry at path: ${ctx.path}, try \`tea --sync\`` - } else { - msg = 'no pantry: run `tea --sync`' - } - break - case 'http': - msg = ctx.cause?.message ?? "unknown HTTP error" - break - case 'not-found: pantry: package.yml': - msg = undent` - Not in pantry: ${ctx.project} - - https://github.com/teaxyz/pantry#contributing - ` - break - case 'parser: pantry: package.yml': - msg = undent` - pantry entry invalid. please report this bug! - - https://github.com/teaxyz/pantry/issues/new - - ----------------------------------------------------->> attachment begin - ${ctx.project}: ${ctx.cause?.message} - <<------------------------------------------------------- attachment end - ` - break - case 'not-found: dev-env': - msg = undent` - \`${ctx.cwd}\` is not a developer environment. - - a developer environment requires the presence of a file or directory - that uniquely identifies package requirements, eg. \`package.json\`. - ` - break - case 'not-found: arg': - msg = undent` - \`${ctx.arg}\` isn't a valid flag. - - see: \`tea --help\` - ` - break - case '#helpwanted': - msg = ctx.details - break - case 'not-found: pkg.version': { - const str = ctx.pkg ? pkg.str(ctx.pkg) : 'this version' - msg = undent` - we haven’t packaged ${str}. but we will… *if* you open a ticket: - - https://github.com/teaxyz/pantry/issues/new - ` - } break - case 'confused: interpreter': - msg = undent` - we’re not sure what to do with this file ¯\\_(ツ)_/¯ - ` - break - default: { - const exhaustiveness_check: never = id - throw new Error(`unhandled id: ${exhaustiveness_check}`) - }} - - const opts: {cause: unknown} = {cause: ctx.cause} - - super(msg) - //super(msg, opts) //FIXME this should be fine but `dnt` complains - - this.cause = opts.cause - //^^ FIXME shouldn't need to do this step - - this.id = id ?? msg - this.ctx = ctx - } -} - export function panic(message?: string): never { throw new Error(message) } -export const wrap = , U>(fn: (...args: T) => U, id: ID) => { - return (...args: T): U => { - try { - let foo = fn(...args) - if (foo instanceof Promise) { - foo = foo.catch(converter) as U - } - return foo - } catch (cause) { - converter(cause) - } - - function converter(cause: unknown): never { - if (cause instanceof TeaError) { - throw cause - } else { - throw new TeaError(id, {...args, cause}) - } - } - } -} - - declare global { interface Promise { swallow(err?: unknown): Promise } } -Promise.prototype.swallow = function(gristle?: unknown) { +Promise.prototype.swallow = function(gristle?: (e: unknown) => boolean) { return this.catch((err: unknown) => { - if (gristle === undefined) { - return - } - - if (err instanceof TeaError) { - err = err.id - } else if (err instanceof Error) { - err = err.message - } else if (isPlainObject(err) && isString(err.code)) { - err = err.code - } else if (isRegExp(gristle) && isString(err)) { - if (!err.match(gristle)) throw err - } else if (err !== gristle) { + if (gristle && !gristle(err)) { throw err } - return undefined }) } From bd2691e1fd14846d7905ced956b7e9021474c944 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Fri, 30 Jun 2023 11:05:52 -0400 Subject: [PATCH 2/6] wip --- mod.ts | 12 ++++++------ src/hooks/useCellar.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mod.ts b/mod.ts index 2784828..8caad5d 100644 --- a/mod.ts +++ b/mod.ts @@ -8,15 +8,15 @@ import Path from "./src/utils/Path.ts" export * as types from "./src/types.ts" import * as pkg from "./src/utils/pkg.ts" -import TeaError, { panic } from "./src/utils/error.ts" +import { panic } 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" -import useCellar from "./src/hooks/useCellar.ts" +import useCellar, { InstallationNotFoundError} from "./src/hooks/useCellar.ts" import useMoustaches from "./src/hooks/useMoustaches.ts" -import usePantry from "./src/hooks/usePantry.ts" +import usePantry, { PantryError } from "./src/hooks/usePantry.ts" import useFetch from "./src/hooks/useFetch.ts" -import useDownload from "./src/hooks/useDownload.ts" +import useDownload, { DownloadError } from "./src/hooks/useDownload.ts" import useShellEnv from "./src/hooks/useShellEnv.ts" import useInventory from "./src/hooks/useInventory.ts" import hydrate from "./src/plumbing/hydrate.ts" @@ -64,10 +64,10 @@ const hacks = { validatePackageRequirement } -export { utils, hooks, plumbing, porcelain, hacks, semver } +export { utils, hooks, plumbing, porcelain, hacks, semver, PantryError, InstallationNotFoundError, DownloadError } /// export types // we cannot add these to the above objects or they cannot be used as types -export { TeaError, Path, SemVer } +export { Path, SemVer } export * from "./src/types.ts" export type { SupportedArchitecture, SupportedPlatform } diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts index aad5d08..a3bf5b2 100644 --- a/src/hooks/useCellar.ts +++ b/src/hooks/useCellar.ts @@ -4,7 +4,7 @@ import SemVer from "../utils/semver.ts" import useConfig from "./useConfig.ts" import Path from "../utils/Path.ts" -class InstallationNotFoundError extends Error { +export class InstallationNotFoundError extends Error { pkg: Package | PackageRequirement constructor(pkg: Package | PackageRequirement) { From 7cefc06cd9dad21126a3315250f80853ecd3093d Mon Sep 17 00:00:00 2001 From: Max Howell Date: Fri, 30 Jun 2023 11:11:59 -0400 Subject: [PATCH 3/6] wip --- mod.ts | 8 ++++---- src/hooks/useCellar.ts | 3 ++- src/hooks/useDownload.ts | 4 ++-- src/hooks/usePantry.ts | 3 ++- src/plumbing/resolve.ts | 3 ++- src/porcelain/run.ts | 3 ++- src/utils/error.ts | 3 +++ 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/mod.ts b/mod.ts index 8caad5d..eb773a4 100644 --- a/mod.ts +++ b/mod.ts @@ -8,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 } from "./src/utils/error.ts" +import { panic, TeaError } 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" @@ -23,10 +23,10 @@ import hydrate from "./src/plumbing/hydrate.ts" import which from "./src/plumbing/which.ts" import link from "./src/plumbing/link.ts" import install, { ConsoleLogger } from "./src/plumbing/install.ts" -import resolve from "./src/plumbing/resolve.ts" +import resolve, { ResolveError } from "./src/plumbing/resolve.ts" import { validatePackageRequirement } from "./src/utils/hacks.ts" import useSync from "./src/hooks/useSync.ts" -import run from "./src/porcelain/run.ts" +import run, { RunError } from "./src/porcelain/run.ts" import porcelain_install from "./src/porcelain/install.ts" const utils = { @@ -64,7 +64,7 @@ const hacks = { validatePackageRequirement } -export { utils, hooks, plumbing, porcelain, hacks, semver, PantryError, InstallationNotFoundError, DownloadError } +export { utils, hooks, plumbing, porcelain, hacks, semver, TeaError, RunError, ResolveError, PantryError, InstallationNotFoundError, DownloadError } /// export types // we cannot add these to the above objects or they cannot be used as types diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts index a3bf5b2..61fa9ad 100644 --- a/src/hooks/useCellar.ts +++ b/src/hooks/useCellar.ts @@ -1,10 +1,11 @@ import { Package, PackageRequirement, Installation } from "../types.ts" +import { TeaError } from "../utils/error.ts" import * as pkgutils from "../utils/pkg.ts" import SemVer from "../utils/semver.ts" import useConfig from "./useConfig.ts" import Path from "../utils/Path.ts" -export class InstallationNotFoundError extends Error { +export class InstallationNotFoundError extends TeaError { pkg: Package | PackageRequirement constructor(pkg: Package | PackageRequirement) { diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts index 71aa6d3..0dae3f2 100644 --- a/src/hooks/useDownload.ts +++ b/src/hooks/useDownload.ts @@ -1,7 +1,7 @@ import { deno } from "../deps.ts" const { crypto: crypto_, streams: { writeAll } } = deno const { toHashString, crypto } = crypto_ -import { panic } from "../utils/error.ts" +import { TeaError, panic } from "../utils/error.ts" import useConfig from "./useConfig.ts" import useFetch from "./useFetch.ts" import Path from "../utils/Path.ts" @@ -15,7 +15,7 @@ interface DownloadOptions { logger?: (info: {src: URL, dst: Path, rcvd?: number, total?: number }) => void } -export class DownloadError extends Error { +export class DownloadError extends TeaError { status: number src: URL headers?: Record diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts index 410781c..e1a65ff 100644 --- a/src/hooks/usePantry.ts +++ b/src/hooks/usePantry.ts @@ -3,6 +3,7 @@ const { isNumber, isPlainObject, isString, isArray, isPrimitive, isBoolean } = i import { validatePackageRequirement } from "../utils/hacks.ts" import { Package, Installation } from "../types.ts" import useMoustaches from "./useMoustaches.ts" +import { TeaError } from "../utils/error.ts" import { validate } from "../utils/misc.ts" import SemVer from "../utils/semver.ts" import useConfig from "./useConfig.ts" @@ -16,7 +17,7 @@ export interface Interpreter { type PantryErrorCode = 'not-found' | 'parse-error' -export class PantryError extends Error { +export class PantryError extends TeaError { code: PantryErrorCode // deno-lint-ignore no-explicit-any diff --git a/src/plumbing/resolve.ts b/src/plumbing/resolve.ts index a56e1e4..a53d2b0 100644 --- a/src/plumbing/resolve.ts +++ b/src/plumbing/resolve.ts @@ -2,6 +2,7 @@ import { Package, PackageRequirement, Installation } from "../types.ts" import useInventory from "../hooks/useInventory.ts" import { str as pkgstr } from "../utils/pkg.ts" import useCellar from "../hooks/useCellar.ts" +import { TeaError } from "../utils/error.ts" /// NOTE resolves to bottles /// NOTE contract there are no duplicate projects in input @@ -17,7 +18,7 @@ export interface Resolution { pending: Package[] } -class ResolveError extends Error { +export class ResolveError extends TeaError { pkg: Package | PackageRequirement constructor(pkg: Package | PackageRequirement) { diff --git a/src/porcelain/run.ts b/src/porcelain/run.ts index 8f38966..67b84dd 100644 --- a/src/porcelain/run.ts +++ b/src/porcelain/run.ts @@ -3,6 +3,7 @@ import useShellEnv from '../hooks/useShellEnv.ts' import usePantry from '../hooks/usePantry.ts' import hydrate from "../plumbing/hydrate.ts" import resolve from "../plumbing/resolve.ts" +import { TeaError } from "../utils/error.ts" import { spawn } from "node:child_process" import useSync from "../hooks/useSync.ts" import which from "../plumbing/which.ts" @@ -123,7 +124,7 @@ async function setup(cmd: string, env: Record, logge type RunErrorCode = 'ENOENT' | 'EUSAGE' | 'EIO' -class RunError extends Error { +export class RunError extends TeaError { code: RunErrorCode constructor(code: RunErrorCode, message: string) { diff --git a/src/utils/error.ts b/src/utils/error.ts index 9fa93b5..07956f4 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -15,3 +15,6 @@ Promise.prototype.swallow = function(gristle?: (e: unknown) => boolean) { } }) } + +export class TeaError extends Error +{} \ No newline at end of file From 09ae07e029b947077ba844e08d249ed032baf3c8 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Sat, 1 Jul 2023 08:28:46 -0400 Subject: [PATCH 4/6] wip --- mod.ts | 13 +++++++-- src/hooks/usePantry.ts | 64 +++++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/mod.ts b/mod.ts index eb773a4..c12e8ce 100644 --- a/mod.ts +++ b/mod.ts @@ -14,7 +14,7 @@ import useOffLicense from "./src/hooks/useOffLicense.ts" import useCache from "./src/hooks/useCache.ts" import useCellar, { InstallationNotFoundError} from "./src/hooks/useCellar.ts" import useMoustaches from "./src/hooks/useMoustaches.ts" -import usePantry, { PantryError } from "./src/hooks/usePantry.ts" +import usePantry, { PantryError, PantryParseError, PantryNotFoundError, PackageNotFoundError } from "./src/hooks/usePantry.ts" import useFetch from "./src/hooks/useFetch.ts" import useDownload, { DownloadError } from "./src/hooks/useDownload.ts" import useShellEnv from "./src/hooks/useShellEnv.ts" @@ -64,7 +64,16 @@ const hacks = { validatePackageRequirement } -export { utils, hooks, plumbing, porcelain, hacks, semver, TeaError, RunError, ResolveError, PantryError, InstallationNotFoundError, DownloadError } +export { + utils, hooks, plumbing, porcelain, hacks, + semver, + TeaError, + RunError, + ResolveError, + PantryError, PantryParseError, PantryNotFoundError, PackageNotFoundError, + InstallationNotFoundError, + DownloadError +} /// export types // we cannot add these to the above objects or they cannot be used as types diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts index e1a65ff..16ffdd2 100644 --- a/src/hooks/usePantry.ts +++ b/src/hooks/usePantry.ts @@ -15,29 +15,32 @@ export interface Interpreter { args: string[] } -type PantryErrorCode = 'not-found' | 'parse-error' +export class PantryError extends TeaError +{} -export class PantryError extends TeaError { - code: PantryErrorCode +export class PantryParseError extends PantryError { + project: string + path?: Path - // deno-lint-ignore no-explicit-any - constructor(code: PantryErrorCode, ctx: any) { - let msg: string + constructor(project: string, path?: Path, cause?: unknown) { + super(`package.yml parse error: ${path ?? project}`) + this.project = project + this.path = path + this.cause = cause + } +} - switch (code) { - case 'not-found': - if (ctx instanceof Path) { - msg = `pantry not found: ${ctx}` - } else { - msg = `pkg not found: ${ctx}` - } - break - case 'parse-error': - msg = `package.yml parse error: ${ctx.filename || ctx.project || ctx}` - } - super(msg) - this.code = code - this.cause = ctx.cause +export class PackageNotFoundError extends PantryError { + project: string + constructor(project: string) { + super(`pkg not found: ${project}`) + this.project = project + } +} + +export class PantryNotFoundError extends PantryError { + constructor(path: Path) { + super(`pantry not found: ${path}`) } } @@ -61,7 +64,7 @@ export default function usePantry() { const yaml = (() => { for (const prefix of pantry_paths()) { - if (!prefix.exists()) throw new PantryError('not-found', prefix.parent()) + if (!prefix.exists()) throw new PantryNotFoundError(prefix.parent()) const dir = prefix.join(project) const filename = dir.join("package.yml") if (!filename.exists()) continue @@ -70,9 +73,9 @@ export default function usePantry() { return () => memo ?? (memo = filename.readYAML() .then(validate.obj) - .catch(cause => { throw new PantryError('parse-error', {cause, project, filename}) })) + .catch(cause => { throw new PantryParseError(project, filename, cause) })) } - throw new PantryError('not-found', project) + throw new PackageNotFoundError(project) })() const companions = async () => parse_pkgs_node((await yaml())["companions"]) @@ -87,7 +90,7 @@ export default function usePantry() { let { platforms } = await yaml() if (!platforms) return true if (isString(platforms)) platforms = [platforms] - if (!isArray(platforms)) throw new PantryError("parse-error", {project}) + if (!isArray(platforms)) throw new PantryParseError(project) return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`) } @@ -99,7 +102,7 @@ export default function usePantry() { if (isPlainObject(node)) { node = node[host().platform] } - if (!isArray(node)) throw new PantryError("parse-error", project) + if (!isArray(node)) throw new PantryParseError(project) return node.compact(x => { if (isPlainObject(x)) { @@ -206,7 +209,7 @@ export default function usePantry() { } if (rv.length == 0) { - throw new PantryError("not-found", prefix) + throw new PantryNotFoundError(prefix) } return rv @@ -279,7 +282,7 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati // deno-lint-ignore no-explicit-any function transform(value: any): string { - if (!isPrimitive(value)) throw new PantryError('parse-error', JSON.stringify(value)) + if (!isPrimitive(value)) throw new PantryParseError(pkg.project, undefined, JSON.stringify(value)) if (isBoolean(value)) { return value ? "1" : "0" @@ -297,7 +300,10 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati } else if (isNumber(value)) { return value.toString() } - throw new Error("unexpected error", {cause: value}) + + const e = new Error("unexpected error") + e.cause = value + throw e } } @@ -307,7 +313,7 @@ interface LsEntry { } async function* _ls_pantry(dir: Path): AsyncGenerator { - if (!dir.isDirectory()) throw new PantryError('not-found', dir) + if (!dir.isDirectory()) throw new PantryNotFoundError(dir) for await (const [path, { name, isDirectory }] of dir.ls()) { if (isDirectory) { From 8994d4c659d8982e87670464062f7069d169610b Mon Sep 17 00:00:00 2001 From: Max Howell Date: Sat, 1 Jul 2023 09:02:35 -0400 Subject: [PATCH 5/6] wip --- src/hooks/useCellar.test.ts | 15 ++++++++++++++- src/hooks/useCellar.ts | 4 +--- src/plumbing/which.ts | 2 +- src/utils/error.test.ts | 16 +++++++++++++++- src/utils/error.ts | 19 +++++++++++++------ 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/hooks/useCellar.test.ts b/src/hooks/useCellar.test.ts index a17172b..3ff07eb 100644 --- a/src/hooks/useCellar.test.ts +++ b/src/hooks/useCellar.test.ts @@ -1,9 +1,10 @@ +import { assert, assertEquals } from "deno/testing/asserts.ts" import SemVer, * as semver from "../utils/semver.ts" import { useTestConfig } from "./useTestConfig.ts" import install from "../plumbing/install.ts" import useCellar from "./useCellar.ts" -Deno.test("resolve()", async () => { +Deno.test("useCellar.resolve()", async () => { useTestConfig() const pkgrq = { project: "python.org", version: new SemVer("3.11.3")} @@ -14,3 +15,15 @@ Deno.test("resolve()", async () => { await useCellar().resolve({ project: "python.org", constraint: new semver.Range("^3") }) await useCellar().resolve(installation.path) }) + +Deno.test("useCellar.has()", async () => { + useTestConfig() + + const rq = { project: "beyondgrep.com", version: new SemVer("3.6.0") } + + assertEquals(await useCellar().has(rq), undefined) + + const installation = await install(rq) + + assertEquals(await useCellar().has(rq), installation) +}) diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts index 61fa9ad..ea4e016 100644 --- a/src/hooks/useCellar.ts +++ b/src/hooks/useCellar.ts @@ -24,9 +24,7 @@ export default function 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((e: unknown) => e instanceof InstallationNotFoundError) + const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(InstallationNotFoundError) return { has, diff --git a/src/plumbing/which.ts b/src/plumbing/which.ts index a2a2fe6..a42c033 100644 --- a/src/plumbing/which.ts +++ b/src/plumbing/which.ts @@ -50,7 +50,7 @@ export default async function(arg0: string, opts = { providers: true }): Promise } } } - }).swallow((e: unknown) => e instanceof PantryError) + }).swallow(PantryError) promises.push(p) if (opts.providers) { diff --git a/src/utils/error.test.ts b/src/utils/error.test.ts index c54ef3e..cf71e9a 100644 --- a/src/utils/error.test.ts +++ b/src/utils/error.test.ts @@ -1,8 +1,22 @@ -import { assertThrows } from "deno/testing/asserts.ts" +import { assertRejects, assertThrows } from "deno/testing/asserts.ts" import { panic } from "../utils/error.ts" Deno.test("errors", async test => { await test.step("panic", () => { assertThrows(() => panic("test msg"), "test msg") }) + 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)) + }) }) + +class FooError extends Error +{} + +class BarError extends Error +{} + +class BazError extends BarError +{} diff --git a/src/utils/error.ts b/src/utils/error.ts index 07956f4..15b5bc4 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,20 +1,27 @@ +// deno-lint-ignore-file no-explicit-any + export function panic(message?: string): never { throw new Error(message) } declare global { interface Promise { - swallow(err?: unknown): Promise + swallow(errorClass?: new (...args: any) => any): Promise } } -Promise.prototype.swallow = function(gristle?: (e: unknown) => boolean) { +Promise.prototype.swallow = function(errorClass?: new (...args: any) => any) { return this.catch((err: unknown) => { - if (gristle && !gristle(err)) { - throw err + if (errorClass && !(err instanceof errorClass)) { + throw err; } }) } -export class TeaError extends Error -{} \ No newline at end of file +export class TeaError extends Error { + ctx: any + constructor(msg: string, ctx?: any) { + super(msg) + this.ctx = ctx + } +} From a32078e3d07a070d2568bac92f65477cc34abae3 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Sat, 1 Jul 2023 09:55:52 -0400 Subject: [PATCH 6/6] wip --- src/hooks/useCellar.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useCellar.test.ts b/src/hooks/useCellar.test.ts index 3ff07eb..86f21cd 100644 --- a/src/hooks/useCellar.test.ts +++ b/src/hooks/useCellar.test.ts @@ -1,4 +1,4 @@ -import { assert, assertEquals } from "deno/testing/asserts.ts" +import { assertEquals } from "deno/testing/asserts.ts" import SemVer, * as semver from "../utils/semver.ts" import { useTestConfig } from "./useTestConfig.ts" import install from "../plumbing/install.ts"