From ba743f3d19b31d5956967becf9f95a076073a73c Mon Sep 17 00:00:00 2001 From: Max Howell Date: Thu, 18 May 2023 10:44:57 -0400 Subject: [PATCH 01/23] use libtea --- .github/workflows/ci.yml | 2 +- deno.jsonc | 16 +- import-map.json | 21 -- scripts/repair.ts | 11 +- src/app.dump.ts | 19 +- src/app.exec.ts | 22 +- src/app.help.ts | 7 +- src/app.magic.ts | 8 +- src/app.main.ts | 41 ++- src/app.provides.ts | 4 +- src/app.ts | 33 +- src/args.ts | 24 +- src/hooks/index.ts | 75 ++--- src/hooks/useCache.ts | 31 -- src/hooks/useCellar.ts | 96 ------ src/hooks/useConfig.ts | 199 +++++++----- src/hooks/useDownload.ts | 176 ---------- src/hooks/useErrorHandler.ts | 70 ++-- src/hooks/useExec.ts | 70 ++-- src/hooks/useFetch.ts | 8 - src/hooks/useInventory.ts | 59 ---- src/hooks/useLogger.ts | 59 ++-- src/hooks/useMoustaches.ts | 45 --- src/hooks/useOffLicense.ts | 28 -- src/hooks/usePackageYAML.ts | 18 +- src/hooks/usePantry.ts | 331 ------------------- src/hooks/usePrefix.ts | 16 - src/hooks/useRun.ts | 8 +- src/hooks/useShellEnv.ts | 159 --------- src/hooks/useSync.ts | 77 ----- src/hooks/useVersion.ts | 3 +- src/hooks/useVirtualEnv.ts | 46 +-- src/init.ts | 103 ------ src/prefab/README.md | 7 - src/prefab/hydrate.ts | 154 --------- src/prefab/index.ts | 11 - src/prefab/install.ts | 163 ---------- src/prefab/link.ts | 52 --- src/prefab/resolve.ts | 50 --- src/types.ts | 65 ---- src/utils/error.ts | 174 ---------- src/utils/hacks.ts | 47 --- src/utils/index.ts | 202 ------------ src/utils/pkg.ts | 55 ---- src/utils/safe-utils.ts | 9 - src/utils/semver.ts | 325 ------------------- src/vendor/Path.ts | 447 -------------------------- src/vendor/PathUtils.ts | 40 --- src/vendor/README.md | 20 -- tests/functional/args.test.ts | 64 ++-- tests/functional/devenv.test.ts | 29 +- tests/functional/exec.test.ts | 6 +- tests/functional/magic.test.ts | 8 +- tests/functional/provides.test.ts | 4 +- tests/functional/repl.test.ts | 6 +- tests/functional/script.test.ts | 5 +- tests/functional/suggestions.test.ts | 5 +- tests/functional/sync.test.ts | 11 +- tests/functional/testUtils.ts | 54 ++-- tests/integration.suite.ts | 6 +- tests/integration/magic.test.ts | 4 +- tests/integration/package.yml.test.ts | 6 +- tests/integration/tea.ln.test.ts | 12 +- tests/integration/tea.scripts.test.ts | 2 +- tests/unit/cache.test.ts | 42 --- tests/unit/error.test.ts | 51 --- tests/unit/fetch.test.ts | 27 -- tests/unit/hydrate.test.ts | 82 ----- tests/unit/path-utils.test.ts | 48 --- tests/unit/path.test.ts | 84 ----- tests/unit/pkgutils.test.ts | 94 ------ tests/unit/semver.test.ts | 216 ------------- tests/unit/useErrorHandler.test.ts | 22 +- tests/unit/utils.test.ts | 56 ---- 74 files changed, 512 insertions(+), 4138 deletions(-) delete mode 100644 import-map.json delete mode 100644 src/hooks/useCache.ts delete mode 100644 src/hooks/useCellar.ts delete mode 100644 src/hooks/useDownload.ts delete mode 100644 src/hooks/useFetch.ts delete mode 100644 src/hooks/useInventory.ts delete mode 100644 src/hooks/useMoustaches.ts delete mode 100644 src/hooks/useOffLicense.ts delete mode 100644 src/hooks/usePantry.ts delete mode 100644 src/hooks/usePrefix.ts delete mode 100644 src/hooks/useShellEnv.ts delete mode 100644 src/hooks/useSync.ts delete mode 100644 src/init.ts delete mode 100644 src/prefab/README.md delete mode 100644 src/prefab/hydrate.ts delete mode 100644 src/prefab/index.ts delete mode 100644 src/prefab/install.ts delete mode 100644 src/prefab/link.ts delete mode 100644 src/prefab/resolve.ts delete mode 100644 src/types.ts delete mode 100644 src/utils/error.ts delete mode 100644 src/utils/hacks.ts delete mode 100644 src/utils/index.ts delete mode 100644 src/utils/pkg.ts delete mode 100644 src/utils/safe-utils.ts delete mode 100644 src/utils/semver.ts delete mode 100644 src/vendor/Path.ts delete mode 100644 src/vendor/PathUtils.ts delete mode 100644 src/vendor/README.md delete mode 100644 tests/unit/cache.test.ts delete mode 100644 tests/unit/error.test.ts delete mode 100644 tests/unit/fetch.test.ts delete mode 100644 tests/unit/hydrate.test.ts delete mode 100644 tests/unit/path-utils.test.ts delete mode 100644 tests/unit/path.test.ts delete mode 100644 tests/unit/pkgutils.test.ts delete mode 100644 tests/unit/semver.test.ts delete mode 100644 tests/unit/utils.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb70be9b2..f73cfc4e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: teaxyz/setup@v0 - - run: deno lint src/*/**.ts + - run: deno lint typecheck: runs-on: ubuntu-latest diff --git a/deno.jsonc b/deno.jsonc index 03bcd7288..ea0506b8a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -23,10 +23,22 @@ ] } }, + "lint": { + "include": ["src/", "scripts/"] + }, "tea": { "dependencies": { - "deno.land": "^1.31.1" + "deno.land": "^1.33.3" } }, - "importMap": "import-map.json" + "imports": { + "tea": "https://raw.github.com/teaxyz/lib/v0/mod.ts", + "tea/": "https://raw.github.com/teaxyz/lib/v0/src/", + "hooks": "./src/hooks/index.ts", + "deno/": "https://deno.land/std@0.182.0/", + "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", + "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", + "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts", + "jsonc": "https://deno.land/x/jsonc_parser@v0.0.1/mod.ts" + } } diff --git a/import-map.json b/import-map.json deleted file mode 100644 index 437ac7994..000000000 --- a/import-map.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "imports": { - "path": "./src/vendor/Path.ts", - "path-utils": "./src/vendor/PathUtils.ts", - "types": "./src/types.ts", - "hooks": "./src/hooks/index.ts", - "hooks/": "./src/hooks/", - "prefab": "./src/prefab/index.ts", - "prefab/": "./src/prefab/", - "deno/": "https://deno.land/std@0.182.0/", - "semver": "./src/utils/semver.ts", - "utils": "./src/utils/index.ts", - "utils/": "./src/utils/", - "is_what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", - "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", - "s3": "https://deno.land/x/s3@0.5.0/mod.ts", - "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts", - "rimbu/": "https://deno.land/x/rimbu@0.13.2/", - "jsonc": "https://deno.land/x/jsonc_parser@v0.0.1/mod.ts" - } -} diff --git a/scripts/repair.ts b/scripts/repair.ts index 15e62595f..a454f75b7 100755 --- a/scripts/repair.ts +++ b/scripts/repair.ts @@ -1,13 +1,12 @@ #!/usr/bin/env -S deno run -A -import { init } from "../src/init.ts"; -import { useCellar } from "hooks" -import { Installation } from "types" -import { link } from "prefab" -import * as semver from "semver" +import { hooks, semver, Installation, prefab } from "tea" +import { useConfig } from "hooks" +const { useCellar } = hooks +const { link } = prefab if (import.meta.main) { - init() + useConfig() for (const project of Deno.args) { await repairLinks(project) diff --git a/src/app.dump.ts b/src/app.dump.ts index 2ebe241ce..fd7fcd17d 100644 --- a/src/app.dump.ts +++ b/src/app.dump.ts @@ -1,6 +1,7 @@ -import { useEnv, usePrint } from "hooks" -import { flatmap } from "utils" -import { isPlainObject } from "is_what" +import { useConfig, usePrint } from "hooks" +import { isPlainObject } from "is-what" +import { utils } from "tea" +const { flatmap } = utils //TODO should read from the shell configuration files to get originals properly //TODO don’t wait on each print, instead chain the promises to be more time-efficient @@ -11,7 +12,7 @@ interface Parameters { } export default async function dump({ env, shell }: Parameters) { - const { TEA_REWIND, getEnvAsObject } = useEnv(); + const { TEA_REWIND, obj: oldenv } = useConfig().env const { print } = usePrint() const [set, unset]= (() => { @@ -37,11 +38,9 @@ export default async function dump({ env, shell }: Parameters) { const is_env = env['SRCROOT'] if (is_env) { - const oldenv = getEnvAsObject() - // first rewind the env to the original state - if (oldenv['TEA_REWIND']) { - const rewind = JSON.parse(oldenv['TEA_REWIND']) as { revert: Record, unset: string[] } + if (TEA_REWIND) { + const rewind = JSON.parse(TEA_REWIND) as { revert: Record, unset: string[] } delete oldenv['TEA_REWIND'] for (const key of rewind.unset) { @@ -59,7 +58,7 @@ export default async function dump({ env, shell }: Parameters) { } // now calculate the new rewind - const TEA_REWIND = (() => { + const new_TEA_REWIND = (() => { const revert: Record = {} const unset: string[] = [] for (const key of Object.keys(env)) { @@ -79,7 +78,7 @@ export default async function dump({ env, shell }: Parameters) { for (const [key, value] of Object.entries(env)) { if (value) await print(set(key, value)) } - await print(set('TEA_REWIND', TEA_REWIND)) + await print(set('TEA_REWIND', new_TEA_REWIND)) } else { const unwind = flatmap(TEA_REWIND, JSON.parse) as { revert: Record, unset: string[] } diff --git a/src/app.exec.ts b/src/app.exec.ts index c814883c1..c16f8b3b7 100644 --- a/src/app.exec.ts +++ b/src/app.exec.ts @@ -1,24 +1,21 @@ -import { pkg as pkgutils, TeaError, chuzzle } from "utils" -import { ExitError, Installation } from "types" -import { useEnv, useConfig, useRun } from "hooks" -import { RunError } from "hooks/useRun.ts" -import { gray, red, teal } from "hooks/useLogger.ts" +import { useConfig, useRun, useLogger, RunError, Verbosity, ExitError } from "hooks" +import { Installation, Path, utils, TeaError } from "tea" import { basename } from "deno/path/mod.ts" -import { isNumber } from "is_what" -import Path from "path" +import { isNumber } from "is-what" export default async function(cmd: string[], env: Record) { - const { TEA_FORK_BOMB_PROTECTOR } = useEnv() + const { TEA_FORK_BOMB_PROTECTOR } = useConfig().env + const { red, teal } = useLogger() // ensure we cannot fork bomb the user since this is basically the worst thing tea/cli can do - let nobomb = chuzzle(parseInt(TEA_FORK_BOMB_PROTECTOR ?? '0')) ?? 0 + let nobomb = parseInt(TEA_FORK_BOMB_PROTECTOR ?? '0').chuzzle() ?? 0 env['TEA_FORK_BOMB_PROTECTOR'] = `${++nobomb}` if (nobomb > 20) throw new Error("FORK BOMB KILL SWITCH ACTIVATED") try { await useRun({cmd, env}) } catch (err) { - const { debug } = useConfig() + const debug = useConfig().modifiers.verbosity >= Verbosity.debug const arg0 = cmd?.[0] if (err instanceof TeaError) { @@ -44,8 +41,9 @@ export default async function(cmd: string[], env: Record) { } export async function repl(installations: Installation[], env: Record) { - const { SHELL } = useEnv() - const pkgs_str = () => installations.map(({pkg}) => gray(pkgutils.str(pkg))).join(", ") + const { SHELL } = useConfig().env + const { gray } = useLogger() + const pkgs_str = () => installations.map(({pkg}) => gray(utils.pkg.str(pkg))).join(", ") // going to stderr so that we don’t potentially break (nonsensical) pipe scenarios, eg. // tea -E | env diff --git a/src/app.help.ts b/src/app.help.ts index 3389471fe..40a28748e 100644 --- a/src/app.help.ts +++ b/src/app.help.ts @@ -1,11 +1,12 @@ +import { Verbosity } from "./hooks/useConfig.ts" import { useConfig, usePrint } from "hooks" -import { undent } from "utils" +import undent from "outdent" export default async function help() { - const { verbose } = useConfig() + const { modifiers: { verbosity } } = useConfig() const { print } = usePrint() - if (!verbose) { + if (verbosity < Verbosity.loud) { // 10| 20| 30| 40| 50| 60| 70| | 80| await print(undent` usage: diff --git a/src/app.magic.ts b/src/app.magic.ts index 292dfa12b..69894db35 100644 --- a/src/app.magic.ts +++ b/src/app.magic.ts @@ -1,10 +1,10 @@ import { basename } from "deno/path/mod.ts" -import { undent } from "utils" -import Path from "path" -import { useEnv } from "./hooks/useConfig.ts" +import { useConfig } from "hooks" +import undent from "outdent" +import { Path } from "tea" export default function(self: Path, shell?: string) { - const { SHELL } = useEnv() + const { SHELL } = useConfig().env shell ??= basename(SHELL ?? "unknown") const d = self.parent() diff --git a/src/app.main.ts b/src/app.main.ts index da77fca6c..d9119c851 100644 --- a/src/app.main.ts +++ b/src/app.main.ts @@ -1,25 +1,24 @@ -import { usePrefix, useExec, useVirtualEnv, useVersion, useSync, usePrint, useConfig, useEnv } from "hooks" -import dump from "./app.dump.ts" -import help from "./app.help.ts" -import provides from "./app.provides.ts"; -import magic from "./app.magic.ts" -import exec, { repl } from "./app.exec.ts" -import { pkg as pkgutils, flatmap } from "utils" -import Path from "path" -import { Verbosity } from "./types.ts" -import * as semver from "semver" +import { usePrefix, useExec, useVirtualEnv, useVersion, usePrint, useConfig } from "hooks" import { VirtualEnv } from "./hooks/useVirtualEnv.ts" +import { Verbosity } from "./hooks/useConfig.ts" import { basename } from "deno/path/mod.ts" -import { Args } from "./args.ts"; +import exec, { repl } from "./app.exec.ts" +import { Path, utils, semver, hooks } from "tea" +import provides from "./app.provides.ts" +import magic from "./app.magic.ts" +import dump from "./app.dump.ts" +import help from "./app.help.ts" +import { Args } from "./args.ts" +const { flatmap } = utils +const { useSync } = hooks export async function run(args: Args) { - const { print } = usePrint(); - const { execPath } = useConfig() - const { PATH, SHELL } = useEnv() + const { print } = usePrint() + const { arg0: execPath, env: { PATH, SHELL } } = useConfig() if (args.cd) { const chdir = args.cd - console.verbose({ chdir }) + console.log({ chdir }) Deno.chdir(chdir.string) } @@ -74,7 +73,7 @@ export async function run(args: Args) { break case "dump": { env['PATH'] = full_path().join(':') - env["TEA_PKGS"] = pkgs.map(pkgutils.str).join(":").trim() + env["TEA_PKGS"] = pkgs.map(utils.pkg.str).join(":").trim() env["TEA_PREFIX"] ??= usePrefix().string env["TEA_VERSION"] = useVersion() @@ -103,7 +102,7 @@ function announce(self: Path) { const prefix = usePrefix().string const version = useVersion() - switch (useConfig().verbosity) { + switch (useConfig().modifiers.verbosity) { case Verbosity.debug: if (self.basename() == "deno") { console.debug({ deno: self.string, prefix, import: import.meta, tea: version }) @@ -117,7 +116,7 @@ function announce(self: Path) { } function injection({ args, inject }: Args) { - const { TEA_FILES, TEA_PKGS, SRCROOT, VERSION } = useEnv() + const { TEA_FILES, TEA_PKGS, SRCROOT, VERSION } = useConfig().env const teaPkgs = TEA_PKGS?.trim() //TODO if TEA_PKGS then extract virtual-env from that, don’t reinterpret it @@ -133,7 +132,7 @@ function injection({ args, inject }: Args) { cwd = file.parent() } - if (useConfig().keepGoing) { + if (useConfig().modifiers.keepGoing) { return useVirtualEnv(cwd).swallow(/^not-found/) } else if (teaPkgs) { /// if an env is defined then we still are going to try to read it @@ -158,7 +157,7 @@ function injection({ args, inject }: Args) { //TODO anything that isn’t an absolute path will crash return { env: {}, - pkgs: TEA_PKGS!.split(":").map(pkgutils.parse), + pkgs: TEA_PKGS!.split(":").map(utils.pkg.parse), teafiles: TEA_FILES.split(":").map(x => new Path(x)), srcroot: new Path(SRCROOT), version: flatmap(VERSION, semver.parse) @@ -180,7 +179,7 @@ export function wut(args: Args): 'dump' | 'exec' | 'repl' | 'env' | 'dryrun' { return true })() - if (useConfig().dryrun) { + if (useConfig().modifiers.dryrun) { return 'dryrun' } else if (stack_mode) { return 'dump' diff --git a/src/app.provides.ts b/src/app.provides.ts index 44cf40e97..f52674552 100644 --- a/src/app.provides.ts +++ b/src/app.provides.ts @@ -1,5 +1,5 @@ -import { which } from "hooks/useExec.ts" -import { ExitError } from "./types.ts" +import { ExitError } from "./hooks/useErrorHandler.ts" +import { which } from "./hooks/useExec.ts" export default async function provides(args: string[]) { let status = 0; diff --git a/src/app.ts b/src/app.ts index d2592bb5b..c29db0086 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,21 +1,40 @@ +import useConfig, { Config, ConfigDefault, Verbosity } from "./hooks/useConfig.ts" +import { parseArgs, UsageError } from "./args.ts" import { useErrorHandler } from "hooks" -import help from "./app.help.ts" -import { UsageError } from "utils" import { run } from "./app.main.ts" -import { parseArgs } from "./args.ts"; -import { init } from "./init.ts"; -import useConfig from "./hooks/useConfig.ts"; +import help from "./app.help.ts" try { const [args, flags] = parseArgs(Deno.args, Deno.execPath()) - init(flags) + + const config = ConfigDefault(flags) + + config.logger.prefix = (!Deno.isatty(Deno.stdout.rid) || Deno.env.get("CI")) ? "tea:" : undefined + useConfig(config) + applyVerbosity(config) + await run(args) + } catch (err) { if (err instanceof UsageError) { - if (!useConfig().silent) await help() + console.error(err.message) + await help() Deno.exit(64) } else { const code = await useErrorHandler(err) Deno.exit(code) } } + + +function applyVerbosity(config: Config) { + function noop() {} + if (config!.modifiers.verbosity < Verbosity.debug) console.debug = noop + if (config!.modifiers.verbosity < Verbosity.loud) console.log = noop + if (config!.modifiers.verbosity < Verbosity.normal) { + console.info = noop + console.warn = noop + console.log = noop + console.error = noop + } +} diff --git a/src/args.ts b/src/args.ts index e6155ae3b..38a0f957f 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,7 +1,5 @@ -import Path from "path" -import { chuzzle, pkg, validate_str } from "utils" -import { PackageSpecification } from "types" -import { TeaError } from "utils" +import { utils, PackageSpecification, Path } from "tea" +const { pkg, validate } = utils export type Args = { cd?: Path @@ -55,10 +53,10 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { const it = args[Symbol.iterator]() for (const arg of it) { - const barf = (arg_?: string) => { throw new TeaError('not-found: arg', {arg: arg_ ?? arg}) } + const barf = (arg_?: string) => { throw new UsageError(arg_ ?? arg) } if (arg == '+' || arg == '-') { - throw new TeaError('not-found: arg', {arg}) + throw new UsageError(arg) } if (arg.startsWith('+')) { @@ -79,13 +77,13 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { flags.verbosity = 1 break } - const bi = chuzzle(parseInt(value) + 1) + const bi = (parseInt(value) + 1).chuzzle() if (bi !== undefined) { flags.verbosity = bi break } const bv = parseBool(value) - if (bv === undefined) throw new TeaError('not-found: arg', {arg}) + if (bv === undefined) throw new UsageError(arg) flags.verbosity = bv ? 1 : 0 } break case 'debug': @@ -94,7 +92,7 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { case 'cd': case 'chdir': case 'cwd': // ala bun - rv.cd = Path.cwd().join(validate_str(value ?? it.next().value)) + rv.cd = Path.cwd().join(validate.str(value ?? it.next().value)) break case 'help': nonovalue() @@ -151,7 +149,7 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { flags.verbosity = (flags.verbosity ?? 0) + 1 break case 'C': - rv.cd = Path.cwd().join(validate_str(it.next().value)) + rv.cd = Path.cwd().join(validate.str(it.next().value)) break case 's': flags.verbosity = -1; @@ -209,3 +207,9 @@ function parseBool(input: string) { return false } } + +export class UsageError extends Error { + constructor(arg: string) { + super(`usage error: no such arg: ${arg}`) + } +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 9941d75ae..c0d1025c4 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,46 +1,49 @@ -// order is important to avoid circular dependencies and thus uncaught ReferenceErrors -import usePrefix from "hooks/usePrefix.ts" -import useOffLicense from "hooks/useOffLicense.ts" -import useDownload from "hooks/useDownload.ts" -import useCache from "hooks/useCache.ts" -import useCellar from "hooks/useCellar.ts" -import useExec from "hooks/useExec.ts" -import useFetch from "hooks/useFetch.ts" -import useInventory from "hooks/useInventory.ts" -import useShellEnv from "hooks/useShellEnv.ts" -import usePantry from "hooks/usePantry.ts" -import useVirtualEnv from "hooks/useVirtualEnv.ts" -import usePackageYAML, { usePackageYAMLFrontMatter } from "hooks/usePackageYAML.ts" -import useSync from "hooks/useSync.ts" -import useVersion from "hooks/useVersion.ts" -import useMoustaches from "hooks/useMoustaches.ts" -import useErrorHandler from "hooks/useErrorHandler.ts" -import usePrint from "hooks/usePrint.ts" -import useRun from "hooks/useRun.ts" -import useConfig, { useEnv } from "hooks/useConfig.ts" +import useExec from "./useExec.ts" +import useVirtualEnv from "./useVirtualEnv.ts" +import usePackageYAML, { usePackageYAMLFrontMatter } from "./usePackageYAML.ts" +import useVersion from "./useVersion.ts" +import useErrorHandler, { ExitError } from "./useErrorHandler.ts" +import usePrint from "./usePrint.ts" +import useConfig, { Verbosity } from "./useConfig.ts" +import useLogger from "./useLogger.ts" +import useRun, { RunError, RunOptions } from "./useRun.ts" + +function usePrefix() { + return useConfig().prefix +} -// but we can sort these alphabetically export { - useCache, - useCellar, - useDownload, - useErrorHandler, useExec, - useFetch, - useInventory, - useMoustaches, - useOffLicense, + useVirtualEnv, usePackageYAML, usePackageYAMLFrontMatter, - usePantry, - usePrefix, - useShellEnv, - useSync, useVersion, - useVirtualEnv, + useErrorHandler, usePrint, - useRun, + usePrefix, useConfig, - useEnv, + useLogger, + Verbosity, + RunError, + useRun, + ExitError +} + +export type { RunOptions } + +declare global { + interface Array { + uniq(): Array + } +} + +Array.prototype.uniq = function(): Array { + const set = new Set() + return this.compact(x => { + const s = x.toString() + if (set.has(s)) return + set.add(s) + return x + }) } diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts deleted file mode 100644 index 943c5e6ed..000000000 --- a/src/hooks/useCache.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { usePrefix } from "hooks" -import { Package, Stowage } from "types" -import * as utils from "utils" - -export default function useCache() { - return { path } -} - -type DownloadOptions = { - type: 'bottle' - pkg: Package -} | { - type: 'src', - url: URL - pkg: Package -} - -const path = (stowage: Stowage) => { - const { pkg, type } = stowage - const stem = pkg.project.replaceAll("/", "∕") - - let filename = `${stem}-${pkg.version}` - if (type == 'bottle') { - const { platform, arch } = stowage.host ?? utils.host() - filename += `+${platform}+${arch}.tar.${stowage.compression}` - } else { - filename += stowage.extname - } - - return usePrefix().www.join(filename) -} diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts deleted file mode 100644 index 7cca59189..000000000 --- a/src/hooks/useCellar.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Package, PackageRequirement, Installation } from "types" -import { pkg as pkgutils } from "utils" -import SemVer from "semver" -import Path from "path" -//ALERT!! do not usePantry() or you can softlock in usePantry.git.ts -import { usePrefix, useConfig } from "hooks" - -export default function useCellar() { - return { - has, - ls, - keg, - resolve, - shelf, - } -} - -/// returns the `Installation` if the pkg is installed -const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(/^not-found:/) - -/// eg. ~/.tea/deno.land -const shelf = (project: string) => usePrefix().join(project) - -/// eg. ~/.tea/deno.land/v1.2.3 -const keg = (pkg: Package) => shelf(pkg.project).join(`v${pkg.version}`) - -/// returns a project’s installations (sorted by version) -async function ls(project: string) { - const d = shelf(project) - const { verbose } = useConfig() - - if (!d.isDirectory()) return [] - - const rv: Installation[] = [] - for await (const [path, {name, isDirectory}] of d.ls()) { - try { - if (!isDirectory) continue - if (!name.startsWith("v") || name == 'var') continue - const version = new SemVer(name) - if (await vacant(path)) continue - rv.push({path, pkg: {project, version}}) - } catch { - // not console.warn as we allow other dirs as a design choice - if (verbose) { - console.warn(`warn: invalid version: ${name}`) - } - } - } - - return rv.sort((a, b) => pkgutils.compare(a.pkg, b.pkg)) -} - -/// if package is installed, returns its installation -async function resolve(pkg: Package | PackageRequirement | Path | Installation) { - const installation = await (async () => { - if ("pkg" in pkg) { return pkg } - // ^^ is `Installation` - - const prefix = usePrefix() - if (pkg instanceof Path) { - const path = pkg - const version = new SemVer(path.basename()) - const project = path.parent().relative({ to: prefix }) - return { - path, pkg: { project, version } - } - } else if ("version" in pkg) { - const path = keg(pkg) - return { path, pkg } - } else { - const installations = await ls(pkg.project) - const versions = installations.map(({ pkg: {version}}) => version) - const version = pkg.constraint.max(versions) - console.debug({ installations, versions, version }) - if (version) { - const path = installations.find(({pkg: {version: v}}) => v.eq(version))!.path - return { path, pkg: { project: pkg.project, version } } - } - } - throw new Error(`not-found:${pkgutils.str(pkg)}`) - })() - if (await vacant(installation.path)) { - throw new Error(`not-found:${pkgutils.str(installation.pkg)}`) - } - return installation -} - -/// if we ignore transient files, is there a package here? -async function vacant(path: Path): Promise { - if (!path.isDirectory()) { - return true - } else for await (const _ of path.ls()) { - return false - } - return true -} diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index c32a43083..e689637da 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -1,98 +1,139 @@ -import Path from "path" -import { Verbosity } from "types" +import useVersion from "./useVersion.ts" +import { Flags } from "../args.ts" +import { isNumber } from "is-what" +import { utils, Path } from "tea" +const { flatmap, panic } = utils -export interface EnvAccessor { - getEnvAsObject: () => { [index: string]: string } -} - -export interface Env { - CI?: string - CLICOLOR?: string - CLICOLOR_FORCE?: string - DEBUG?: string - GITHUB_ACTIONS?: string - GITHUB_TOKEN?: string - NO_COLOR?: string - PATH?: string - RUNNER_DEBUG?: string - SHELL?: string - SRCROOT?: string - TEA_DIR?: string - TEA_FILES?: string - TEA_FORK_BOMB_PROTECTOR?: string - TEA_MAGIC?: string - TEA_PANTRY_PATH?: string - TEA_PKGS?: string - TEA_PREFIX?: string - TEA_REWIND?: string - VERBOSE?: string - VERSION?: string -} +import useConfig, { Config as ConfigBase, ConfigDefault as ConfigBaseDefault, _internals } from "tea/hooks/useConfig.ts" -export interface Config { - isCI: boolean +export interface Config extends ConfigBase { + arg0: Path - execPath: Path - loggerGlobalPrefix?: string - teaPrefix: Path - - verbosity: Verbosity - dryrun: boolean - keepGoing: boolean + logger: { + prefix?: string + color: boolean + } - verbose: boolean - debug: boolean - silent: boolean + env: { + TEA_DIR?: Path + TEA_PKGS?: string + TEA_FILES?: string + TEA_MAGIC?: string + TEA_REWIND?: string + TEA_FORK_BOMB_PROTECTOR?: string + VERSION?: string + SRCROOT?: string + SHELL?: string + PATH?: string + + obj: Record + } - env: Env + modifiers: { + dryrun: boolean + verbosity: Verbosity + json: boolean + keepGoing: boolean + } +} - json: boolean +export default function(input?: Config): Config { + if (!_internals.initialized()) { + const rv = useConfig(input ?? panic("useConfig() not initialized")) as Config + return rv + } else { + if (input) console.warn("useConfig() already initialized, new parameters ignored") + return useConfig() as Config + } } -let config: Config | undefined; +export function ConfigDefault(flags?: Flags, arg0 = Deno.execPath(), defaults?: ConfigBase, env = Deno.env.toObject()): Config { + defaults ??= ConfigBaseDefault(env) + + const { + TEA_DIR, + TEA_REWIND, + TEA_FORK_BOMB_PROTECTOR, + SHELL, + TEA_FILES, + TEA_PKGS, + TEA_MAGIC, + SRCROOT, + VERSION, + PATH + } = env -// Apply config should only be called once during application initialization -export function applyConfig(cf: Config) { - config = cf - applyVerbosity(config) + return { + ...defaults, + arg0: new Path(arg0), + UserAgent: `tea.cli/${useVersion()}`, + logger: { + prefix: undefined, + color: loggerColor(env) + }, + modifiers: { + dryrun: flags?.dryrun ?? false, + verbosity: flags?.verbosity ?? getVerbosity(env), + json: flags?.json ?? false, + keepGoing: flags?.keepGoing ?? false, + }, + env: { + TEA_DIR: flatmap(TEA_DIR, x => Path.abs(x) ?? Path.cwd().join(x)), + TEA_REWIND, + TEA_FORK_BOMB_PROTECTOR, + SHELL, + TEA_FILES, + TEA_MAGIC, + TEA_PKGS, + SRCROOT, + VERSION, + PATH, + obj: env + } + } } -function applyVerbosity(config: Config) { - function noop() {} - if (config.verbosity > Verbosity.debug) config.verbosity = Verbosity.debug - if (config.verbosity < Verbosity.debug) console.debug = noop - if (config.verbosity < Verbosity.loud) console.verbose = noop - if (config.verbosity < Verbosity.normal) { - console.info = noop - console.warn = noop - console.log = noop - console.error = noop - } +export enum Verbosity { + quiet = -1, + normal = 0, + loud = 1, + debug = 2, + trace = 3 } -// useConfig provides the global configuration state of the application. -// It must not be called until applyConfig has been called. -export default function useConfig(): Readonly { - if (!config) { - throw Error("contract-violated: config must be applied before it can be used.") - } +function getVerbosity(env: Record): Verbosity { + const { DEBUG, GITHUB_ACTIONS, RUNNER_DEBUG, VERBOSE } = env - return config -} + if (DEBUG == '1') return Verbosity.debug + if (GITHUB_ACTIONS == 'true' && RUNNER_DEBUG == '1') return Verbosity.debug -export function useEnv(): Readonly & EnvAccessor { - const { env } = useConfig() - return { - ...env, - getEnvAsObject: _internals.getEnvAsObject - } + const verbosity = flatmap(VERBOSE, parseInt) + return isNumber(verbosity) ? verbosity : Verbosity.normal } -const nativeGetEnvAsObject = () => Deno.env.toObject() +function loggerColor(env: Record) { + const isTTY = () => Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stdout.rid) + + if ((env.CLICOLOR ?? '1') != '0' && isTTY()){ + //https://bixense.com/clicolors/ + return true + } + if ((env.CLICOLOR_FORCE ?? '0') != '0') { + //https://bixense.com/clicolors/ + return true + } + if ((env.NO_COLOR ?? '0') != '0') { + return false + } + if (env.CLICOLOR == '0' || env.CLICOLOR_FORCE == '0') { + return false + } + if (env.CI) { + // this is what charm’s lipgloss does, we copy their lead + // however surely nobody wants `tea foo > bar` to contain color codes? + // the thing is otherwise we have no color in CI since it is not a TTY + return true + } -// _internals are used for testing -export const _internals = { - getConfig: () => config, - setConfig: (c: Config) => config = c, - getEnvAsObject: nativeGetEnvAsObject, + return false } diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts deleted file mode 100644 index 1f780d3d7..000000000 --- a/src/hooks/useDownload.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { crypto, toHashString } from "deno/crypto/mod.ts" -import useLogger, { Logger, teal, gray, logJSON } from "./useLogger.ts" -import { chuzzle, error, TeaError } from "utils" -import { usePrefix, useFetch } from "hooks" -import { isString } from "is_what" -import Path from "path" -import useConfig, { useEnv } from "./useConfig.ts" - -interface DownloadOptions { - src: URL - headers?: Record - logger?: Logger | string - dst?: Path -} - -interface RV { - path: Path - - // we only give you the sha if we download - // if we found the cache then you have to calculate the sha yourself - sha: string | undefined -} - -async function internal({ src, headers, logger, dst }: DownloadOptions): Promise<[Path, ReadableStream | undefined]> -{ - const { isCI, silent, json } = useConfig(); - const { GITHUB_TOKEN } = useEnv(); - logger = isString(logger) ? useLogger(logger) : logger ?? useLogger() - - const hash = hash_key(src) - const mtime_entry = hash.join("mtime") - const etag_entry = hash.join("etag") - - dst ??= hash.join(src.path().basename()) - if (src.protocol === "file:") throw new Error() - - console.verbose({src: src, dst}) - - if (dst.isReadableFile()) { - headers ??= {} - if (etag_entry.isFile()) { - headers["If-None-Match"] = await etag_entry.read() - } - // sending both if we have them is ChatGPT recommended - // also this fixes getting the mysql.com sources, otherwise it redownloads 400MB every time! - if (mtime_entry.isFile()) { - headers["If-Modified-Since"] = await mtime_entry.read() - } - } else if (!json) { - logger.replace(teal('downloading')) - } - - // so the user can add private repos if they need to etc. - if (/(^|\.)github.com$/.test(src.host)) { - if (GITHUB_TOKEN) { - headers ??= {} - headers["Authorization"] = `bearer ${GITHUB_TOKEN}` - } - } - - const rsp = await useFetch(src, { headers }) - - switch (rsp.status) { - case 200: { - const sz = chuzzle(parseInt(rsp.headers.get("Content-Length")!)) - - let txt = teal('downloading') - if (sz) txt += ` ${gray(pretty_size(sz))}` - if (!json) { - logger.replace(txt) - } else { - logJSON({status: "downloading"}) - } - - const reader = rsp.body ?? error.panic() - - const text = rsp.headers.get("Last-Modified") - if (text) mtime_entry.write({text, force: true}) - const etag = rsp.headers.get("ETag") - if (etag) etag_entry.write({text: etag, force: true}) - - if (isCI || silent) { - return [dst, reader] - } else { - let n = 0 - return [dst, reader.pipeThrough(new TransformStream({ - transform: (buf, controller) => { - n += buf.length - if (json) { - logJSON({status: "downloading", "received": n, "content-size": sz }) - } else if (!sz) { - (logger as Logger).replace(`${txt} ${pretty_size(n)}`) - } else { - let s = txt - if (n < sz) { - let pc = n / sz * 100; - pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5% - s += ` ${pc}%` - } else { - s = teal('extracting') - } - (logger as Logger).replace(s) - } - controller.enqueue(buf) - }}))] - } - } - case 304: - if (json) { - logJSON({status: "downloaded"}) - } else { - logger.replace(`cache: ${teal('hit')}`) - } - return [dst, undefined] - default: - throw new Error(`${rsp.status}: ${src}`) - } -} - -async function download(opts: DownloadOptions): Promise { - try { - const [path, stream] = await internal(opts) - if (!stream) return path // already downloaded - - path.parent().mkpath() - const f = await Deno.open(path.string, {create: true, write: true, truncate: true}) - await stream.pipeTo(f.writable) - return path - } catch (cause) { - throw new TeaError('http', {cause, ...opts}) - } -} - -async function stream(opts: DownloadOptions): Promise | undefined> { - try { - const [, stream] = await internal(opts) - return stream - } catch (cause) { - throw new TeaError('http', {cause, ...opts}) - } -} - -function hash_key(url: URL): Path { - function hash(url: URL) { - const formatted = `${url.pathname}${url.search ? "?" + url.search : ""}` - const contents = new TextEncoder().encode(formatted) - return toHashString(crypto.subtle.digestSync("SHA-256", contents)) - } - - const prefix = usePrefix().www - - return prefix - .join(url.protocol.slice(0, -1)) - .join(url.hostname) - .join(hash(url)) - .mkpath() -} - -export default function useDownload() { - return { - download, - stream, - hash_key - } -} - -function pretty_size(n: number) { - const units = ["B", "KiB", "MiB", "GiB", "TiB"] - let i = 0 - while (n > 1024 && i < units.length - 1) { - n /= 1024 - i++ - } - const precision = n < 10 ? 2 : n < 100 ? 1 : 0 - return `${n.toFixed(precision)} ${units[i]}` -} diff --git a/src/hooks/useErrorHandler.ts b/src/hooks/useErrorHandler.ts index a53dd4b12..9f084f405 100644 --- a/src/hooks/useErrorHandler.ts +++ b/src/hooks/useErrorHandler.ts @@ -1,39 +1,25 @@ -import * as logger from "./useLogger.ts" -import { usePantry, usePrefix, useInventory } from "hooks" -import useConfigBase, { applyConfig } from "hooks/useConfig.ts" -import { chuzzle, TeaError, undent } from "utils" -import Path from "path" -import { ExitError, Verbosity } from "../types.ts" - -const useConfig = () => { - try { - return useConfigBase() - } catch { - // lol we threw before we could apply the config - // these defaults will do for error handling - applyConfig({ - isCI: false, - execPath: Path.root, - teaPrefix: Path.root, - verbosity: Verbosity.normal, - dryrun: false, - keepGoing: false, - verbose: false, - debug: false, - silent: false, - env: {}, - json: false - }) - return useConfigBase() +import { useLogger, Verbosity, useConfig } from "hooks" +import { hooks, Path, TeaError } from "tea" +const { usePantry, useInventory } = hooks +import undent from "outdent" + +// ExitError will cause the application to exit with the specified exit code if it bubbles +// up to the main error handler +export class ExitError extends Error { + code: number + constructor(code: number) { + super(`exiting with code: ${code}`) + this.code = code } } export async function suggestions(err: TeaError) { + const { teal } = useLogger() switch (err.id) { case 'not-found: pantry: package.yml': { const suggestion = await getClosestPackageSuggestion(err.ctx.project).swallow() return suggestion - ? `did you mean \`${logger.teal(suggestion)}\`? otherwise… see you on GitHub?` + ? `did you mean \`${teal(suggestion)}\`? otherwise… see you on GitHub?` : undefined } case 'not-found: pkg.version': @@ -46,17 +32,20 @@ export async function suggestions(err: TeaError) { } export default async function(err: Error) { - const { silent, debug, json } = useConfig() + const { logJSON, red, gray } = useLogger() + const { verbosity, json } = useConfig().modifiers + const silent = verbosity <= Verbosity.quiet + const debug = verbosity >= Verbosity.debug if (err instanceof ExitError) { - if (json) logger.logJSON({ error: true }) + if (json) logJSON({ error: true }) return err.code } else if (err instanceof TeaError) { if (json) { - logger.logJSON({ error: true, message: msg(err) }) + logJSON({ error: true, message: msg(err) }) } else if (!silent) { const suggestion = await suggestions(err).swallow() - console.error(`${logger.red('error')}: ${err.title()} (${logger.gray(err.code())})`) + console.error(`${red('error')}: ${err.title()} (${gray(err.code())})`) if (suggestion) { console.error() console.error(suggestion) @@ -65,10 +54,10 @@ export default async function(err: Error) { console.error(msg(err)) if (debug) console.error(err.ctx) } - const code = chuzzle(parseInt(err.code().match(/\d+$/)?.[0] ?? '1')) ?? 1 + const code = parseInt(err.code().match(/\d+$/)?.[0] ?? '1').chuzzle() ?? 1 return code } else if (json) { - logger.logJSON({ error: true, message: err.message }) + logJSON({ error: true, message: err.message }) } else if (!silent) { const { stack, message } = err ?? {} @@ -76,12 +65,12 @@ export default async function(err: Error) { const url = `https://github.com/teaxyz/cli/issues/new?title=${title}` console.error() - console.error(`${logger.red("panic")}:`, "spilt tea. we’re sorry and we’ll fix it… but you have to report the bug!") + console.error(`${red("panic")}:`, "spilt tea. we’re sorry and we’ll fix it… but you have to report the bug!") console.error() - console.error(" ", logger.gray(url)) + console.error(" ", gray(url)) console.error() console.error("----------------------------------------------------->> attachment begin") - console.error(logger.gray(stack ?? "null")) + console.error(gray(stack ?? "null")) console.debug("------------------------------------------------------------------------") console.debug({ err }) console.error("<<----------------------------------------------------- attachment end") @@ -97,12 +86,13 @@ export default async function(err: Error) { /// this is here because error.ts cannot import higher level modules /// like hooks without creating a cyclic dependency function msg(err: TeaError): string { + const { gray } = useLogger() let msg = err.message const { ctx } = err switch (err.code()) { case 'spilt-tea-009': - if (ctx.filename instanceof Path && !ctx.filename.in(usePrefix())) { + if (ctx.filename instanceof Path && !ctx.filename.string.startsWith(useConfig().prefix.string)) { // this yaml is being worked on by the user msg = `${ctx.filename.prettyLocalString()}: ${ctx.cause?.message ?? 'unknown cause'}` } else { @@ -113,7 +103,7 @@ function msg(err: TeaError): string { https://github.com/teaxyz/pantry/issues/new ----------------------------------------------------->> attachment begin - ${logger.gray(attachment)} + ${gray(attachment)} <<------------------------------------------------------- attachment end ` } @@ -129,7 +119,7 @@ async function getClosestPackageSuggestion(input: string) { for await (const {project} of pantry.ls()) { if (min == 0) break - pantry.getProvides({ project }).then(provides => { + pantry.project(project).provides().then(provides => { if (provides.includes(input)) { choice = project min = 0 diff --git a/src/hooks/useExec.ts b/src/hooks/useExec.ts index 75a68385a..98d28fc38 100644 --- a/src/hooks/useExec.ts +++ b/src/hooks/useExec.ts @@ -1,12 +1,13 @@ -import { usePantry, useShellEnv, useDownload, usePackageYAMLFrontMatter, usePrefix } from "hooks" -import { PackageSpecification, Installation, PackageRequirement } from "types" -import { hydrate, resolve, install as base_install, link } from "prefab" +import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError } from "tea" +import { usePackageYAMLFrontMatter } from "./usePackageYAML.ts" +import { ExitError } from "./useErrorHandler.ts" import { VirtualEnv } from "./useVirtualEnv.ts" -import { flatten } from "./useShellEnv.ts" -import useLogger, { logJSON } from "./useLogger.ts" -import { pkg as pkgutils, TeaError } from "utils" -import * as semver from "semver" -import Path from "path" +import useConfig from "./useConfig.ts" +import useLogger from "./useLogger.ts" +import undent from "outdent" + +const { usePantry, useCellar, useDownload, useShellEnv } = hooks +const { hydrate, resolve, install: base_install, link } = prefab interface Parameters { args: string[] @@ -22,6 +23,7 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) { if (arg0) cmd[0] = arg0?.toString() // if we downloaded it then we need to replace args[0] const clutch = pkgs.length > 0 const env: Record = inject?.env ?? {} + const sh = useShellEnv() if (inject) { const {version, srcroot, teafiles, ...vrtenv} = inject @@ -54,7 +56,7 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) { } if (precmd.length == 0) { - const found = await usePantry().getInterpreter(arg0.extname()) + const found = await usePantry().which({ interprets: arg0.extname() }) if (found) { pkgs.push({ ...found, constraint: new semver.Range('*') }) precmd.unshift(...found.args) @@ -97,14 +99,14 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) { pkgs = dry // reassign as condensed + sorted } - Object.assign(env, flatten(await useShellEnv({ installations }))) + Object.assign(env, sh.flatten(await sh.map({ installations }))) - env["TEA_PREFIX"] ??= usePrefix().string + env["TEA_PREFIX"] ??= useConfig().prefix.string return { env, cmd, installations, pkgs } async function add_companions(pkg: {project: string}) { - pkgs.push(...await usePantry().getCompanions(pkg)) + pkgs.push(...await usePantry().project(pkg).companions()) } } @@ -112,8 +114,9 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) { ///////////////////////////////////////////////////////////////////////////// funcs async function install(pkgs: PackageSpecification[], update: boolean) { - const { json } = useConfig() - const logger = useLogger() + const { modifiers: { json, dryrun }, env } = useConfig() + const logger = useLogger().new() + const { logJSON } = useLogger() if (!json) { logger.replace("resolving package graph") @@ -128,10 +131,30 @@ async function install(pkgs: PackageSpecification[], update: boolean) { logger.clear() if (json) { - logJSON({ status: "resolved", pkgs: pending.map(pkgutils.str) }) + logJSON({ status: "resolved", pkgs: pending.map(utils.pkg.str) }) } - for (const pkg of pending) { + if (!dryrun && env.TEA_MAGIC?.split(':').includes("prompt")) { + if (!Deno.isatty(Deno.stdin.rid)) { + throw new Error("TEA_MAGIC=prompt but stdin is not a tty") + } + + do { + const val = prompt(undent` + ┌ ⚠️ tea requests to install: ${pending.map(utils.pkg.str).join(", ")} + └ \x1B[1mallow?\x1B[0m [y/n]` + )?.toLowerCase() + + if (val === "y") { + break + } + if (val === "n") { + throw new ExitError(1) + } + } while (true) + } + + if (!dryrun) for (const pkg of pending) { const install = await base_install(pkg) await link(install) installed.push(install) @@ -162,9 +185,6 @@ async function read_shebang(path: Path): Promise { return [] } -import useCellar from "./useCellar.ts" -import useConfig, { useEnv } from "./useConfig.ts" - async function fetch_it(arg0: string | undefined) { if (!arg0) return @@ -174,7 +194,7 @@ async function fetch_it(arg0: string | undefined) { return path.chmod(0o700) //FIXME like… I don’t feel we should necessarily do this… } - const { execPath } = useConfig() + const { arg0: execPath } = useConfig() const path = Path.cwd().join(arg0) if (path.exists() && execPath.basename() == "tea") { // ^^ in the situation where we are shadowing other tool names @@ -220,12 +240,12 @@ type WhichResult = PackageRequirement & { } export async function which(arg0: string | undefined) { - if (!arg0?.chuzzle() || arg0.includes("/")) { + if (!arg0?.trim() || arg0.includes("/")) { // no shell we know allows searching for subdirectories off PATH return false } - const { TEA_PKGS, TEA_MAGIC } = useEnv() + const { TEA_PKGS, TEA_MAGIC } = useConfig().env const pantry = usePantry() let found: { project: string, constraint: semver.Range, shebang: string[] } | undefined const promises: Promise[] = [] @@ -233,13 +253,13 @@ export async function which(arg0: string | undefined) { for await (const entry of pantry.ls()) { if (found) break - const p = pantry.getProvides(entry).then(providers => { + const p = pantry.project(entry).provides().then(providers => { for (const provider of providers) { if (found) { return } else if (provider == arg0 || (!abracadabra && provider == `tea-${arg0}`)) { const inenv = TEA_PKGS?.split(":") - .map(pkgutils.parse) + .map(utils.pkg.parse) .find(x => x.project == entry.project) if (inenv) { // we are being executed via the command not found handler inside a dev-env @@ -282,7 +302,7 @@ export async function which(arg0: string | undefined) { }).swallow(/^parser: pantry: package.yml/) promises.push(p) - const pp = pantry.getProvider({ project: entry.project }).then(f => { + const pp = pantry.project(entry).provider().then(f => { if (!f) return const rv = f(arg0) if (rv) found = { diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts deleted file mode 100644 index 447fc41e8..000000000 --- a/src/hooks/useFetch.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useVersion } from "hooks"; - -// useFetch wraps the native Deno fetch api and inserts a User-Agent header -export default function useFetch(input: string | URL | Request, init?: RequestInit | undefined): Promise { - const requestInit = init ?? {} as RequestInit - requestInit.headers = { ...requestInit.headers, "User-Agent": `tea.cli/${useVersion()}` } - return fetch(input, requestInit) -} diff --git a/src/hooks/useInventory.ts b/src/hooks/useInventory.ts deleted file mode 100644 index baff8206c..000000000 --- a/src/hooks/useInventory.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Package, PackageRequirement } from "types" -import { host, error, TeaError } from "utils" -import SemVer from "semver" -import Path from "../vendor/Path.ts" -import { useFetch } from "hooks"; - -export interface Inventory { - [project: string]: { - [platform: string]: { - [arch: string]: string[] - } - } -} - -const select = async (rq: PackageRequirement | Package) => { - const versions = await get(rq) - - console.debug({ project: rq.project, versions }) - - if ("constraint" in rq) { - return rq.constraint.max(versions) - } else if (versions.find(x => x.eq(rq.version))) { - return rq.version - } -} - -const get = async (rq: PackageRequirement | Package) => { - const { platform, arch } = host() - - const url = new URL('https://dist.tea.xyz') - url.pathname = Path.root.join(rq.project, platform, arch, 'versions.txt').string - - const rsp = await useFetch(url) - - if (!rsp.ok) { - const cause = new Error(`${rsp.status}: ${url}`) - throw new TeaError('http', {cause}) - } - - const releases = await rsp.text() - let versions = releases.split("\n").compact(x => new SemVer(x)) - - if (versions.length < 1) throw new Error() - - if (rq.project == 'openssl.org') { - // workaround our previous sins - const v = new SemVer("1.1.118") - versions = versions.filter(x => x.neq(v)) - } - - return versions -} - -export default function useInventory() { - return { - select: error.wrap(select, 'http'), - get - } -} diff --git a/src/hooks/useLogger.ts b/src/hooks/useLogger.ts index a15941b30..15231c9d1 100644 --- a/src/hooks/useLogger.ts +++ b/src/hooks/useLogger.ts @@ -1,7 +1,7 @@ +import useConfig, { Verbosity } from "./useConfig.ts" import { colors, tty } from "cliffy/ansi/mod.ts" -import { flatmap } from "utils"; -import useConfig, { useEnv } from "hooks/useConfig.ts"; -import { Verbosity } from "types"; +import { utils } from "tea" +const { flatmap } = utils // ref https://github.com/chalk/ansi-regex/blob/main/index.js const ansi_escapes_rx = new RegExp([ @@ -26,42 +26,21 @@ function ln(s: string, prefix_length: number) { } } -export default function useLogger(prefix?: string) { - const { verbosity, loggerGlobalPrefix } = useConfig() - return new Logger(verbosity, prefix, loggerGlobalPrefix) +export default function() { + return { + new: useLogger, + teal, red, gray, dark, lite, + logJSON + } } -function colorIfTTY(x: string, colorMethod: (x: string)=>string) { - //TODO this function needs to take a out/err parameter since that's what - // needs to be tested rather than testing for both (this is safe for now) - const isTTY = () => Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stdout.rid) - const { isCI } = useConfig() - const { CLICOLOR, CLICOLOR_FORCE, NO_COLOR } = useEnv() - - const color_on = () => { - if ((CLICOLOR ?? '1') != '0' && isTTY()){ - //https://bixense.com/clicolors/ - return true - } - if ((CLICOLOR_FORCE ?? '0') != '0') { - //https://bixense.com/clicolors/ - return true - } - if ((NO_COLOR ?? '0') != '0') { - return false - } - if (CLICOLOR == '0' || CLICOLOR_FORCE == '0') { - return false - } - if (isCI) { - // this is what charm’s lipgloss does, we copy their lead - // however surely nobody wants `tea foo > bar` to contain color codes? - // the thing is otherwise we have no color in CI since it is not a TTY - return true - } - } +function useLogger(prefix?: string) { + const { modifiers: { verbosity }, logger: { prefix: loggerGlobalPrefix, color } } = useConfig() + return new Logger(verbosity, prefix, loggerGlobalPrefix, color) +} - return color_on() ? colorMethod(x) : x +function colorIfTTY(x: string, colorMethod: (x: string)=>string) { + return useConfig().logger.color ? colorMethod(x) : x } export const teal = (x: string) => colorIfTTY(x, (x) => colors.rgb8(x, 86)) @@ -74,13 +53,15 @@ export class Logger { readonly verbosity: Verbosity readonly prefix: string readonly globalPrefix?: string + readonly sequences_ok: boolean lines = 0 last_line = '' tty = tty({ stdout: Deno.stderr }) prefix_length: number - constructor(verbosity: Verbosity, prefix?: string, globalPrefix?: string) { + constructor(verbosity: Verbosity, prefix: string | undefined, globalPrefix: string | undefined, sequences_ok: boolean) { this.verbosity = verbosity + this.sequences_ok = sequences_ok prefix = prefix?.chuzzle() this.prefix_length = prefix?.length ?? 0 @@ -103,7 +84,7 @@ export class Logger { if (this.lines) { const n = ln(this.last_line, this.prefix_length) - if (this.verbosity < 1) { + if (this.sequences_ok) { this.tty.cursorLeft.cursorUp(n).eraseDown() } this.lines -= n @@ -119,7 +100,7 @@ export class Logger { } clear() { - if (this.lines && this.verbosity >= 0) { + if (this.sequences_ok && this.lines && this.verbosity >= 0) { this.tty.cursorLeft.cursorUp(this.lines).eraseDown(this.lines) this.lines = 0 } diff --git a/src/hooks/useMoustaches.ts b/src/hooks/useMoustaches.ts deleted file mode 100644 index b1d5d9dd0..000000000 --- a/src/hooks/useMoustaches.ts +++ /dev/null @@ -1,45 +0,0 @@ -import SemVer from "semver" -import { host } from "utils" - -export default function useMoustaches() { - return { - apply, - tokenize: { - version: tokenizeVersion, - host: tokenizeHost - } - } -} - -function tokenizeVersion(version: SemVer, prefix = 'version') { - const rv = [ - { from: prefix, to: `${version}` }, - { from: `${prefix}.major`, to: `${version.major}` }, - { from: `${prefix}.minor`, to: `${version.minor}` }, - { from: `${prefix}.patch`, to: `${version.patch}` }, - { from: `${prefix}.marketing`, to: `${version.major}.${version.minor}` }, - { from: `${prefix}.build`, to: version.build.join('+') }, - { from: `${prefix}.raw`, to: version.raw }, - ] - if ('tag' in version) { - rv.push({from: `${prefix}.tag`, to: (version as unknown as {tag: string}).tag}) - } - return rv -} - -//TODO replace `hw` with `host` -function tokenizeHost() { - const { arch, target, platform } = host() - return [ - { from: "hw.arch", to: arch }, - { from: "hw.target", to: target }, - { from: "hw.platform", to: platform }, - { from: "hw.concurrency", to: navigator.hardwareConcurrency.toString() } - ] -} - -function apply(input: string, map: { from: string, to: string }[]) { - return map.reduce((acc, {from, to}) => - acc.replace(new RegExp(`(^\\$)?{{\\s*${from}\\s*}}`, "g"), to), - input) -} diff --git a/src/hooks/useOffLicense.ts b/src/hooks/useOffLicense.ts deleted file mode 100644 index 4eb385098..000000000 --- a/src/hooks/useOffLicense.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Stowage } from "types" -import { host } from "utils" -import Path from "path" - -type Type = 's3' - -export default function useOffLicense(_type: Type) { - return { url, key } -} - -function key(stowage: Stowage) { - let rv = Path.root.join(stowage.pkg.project) - if (stowage.type == 'bottle') { - const { platform, arch } = stowage.host ?? host() - rv = rv.join(`${platform}/${arch}`) - } - let fn = `v${stowage.pkg.version}` - if (stowage.type == 'bottle') { - fn += `.tar.${stowage.compression}` - } else { - fn += stowage.extname - } - return rv.join(fn).string.slice(1) -} - -function url(stowage: Stowage) { - return new URL(`https://dist.tea.xyz/${key(stowage)}`) -} diff --git a/src/hooks/usePackageYAML.ts b/src/hooks/usePackageYAML.ts index 62a7364e3..24ca26644 100644 --- a/src/hooks/usePackageYAML.ts +++ b/src/hooks/usePackageYAML.ts @@ -1,10 +1,8 @@ -import { PackageRequirement } from "types" -import { isPlainObject, isString, isArray, PlainObject } from "is_what" -import { validatePackageRequirement } from "utils/hacks.ts" -import { usePrefix } from "hooks" -import { validate_plain_obj } from "utils" -import Path from "path" -import useMoustaches from "./useMoustaches.ts" +import { isPlainObject, isString, isArray, PlainObject } from "is-what" +import { PackageRequirement, Path, hacks, utils, hooks } from "tea" +const { validatePackageRequirement } = hacks +const { useMoustaches, useConfig } = hooks +const { validate } = utils interface Return1 { getDeps: (wbuild: boolean) => PackageRequirement[] @@ -21,8 +19,8 @@ export default function usePackageYAML(yaml: unknown): Return1 { // deno-lint-ignore no-explicit-any function go(node: any) { if (!node) return [] - return Object.entries(validate_plain_obj(node)) - .compact(([project, constraint]) => validatePackageRequirement({ project, constraint })) + return Object.entries(validate.obj(node)) + .compact(([project, constraint]) => validatePackageRequirement(project, constraint)) } } @@ -67,7 +65,7 @@ export function refineFrontMatter(obj: unknown, srcroot?: Path): FrontMatter { const foo = [ ...moustaches.tokenize.host(), - { from: "tea.prefix", to: usePrefix().string }, + { from: "tea.prefix", to: useConfig().prefix.string }, { from: "home", to: Path.home().string } ] diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts deleted file mode 100644 index 40cba55eb..000000000 --- a/src/hooks/usePantry.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { Package, PackageRequirement, Installation } from "types" -import { host, validate_plain_obj, pkg, TeaError, validate_arr } from "utils" -import { isNumber, isPlainObject, isString, isArray, isPrimitive, PlainObject, isBoolean } from "is_what" -import { validatePackageRequirement } from "utils/hacks.ts" -import { useCellar, usePrefix } from "hooks" -import Path from "path" - -interface Entry { - dir: Path - yml: () => Promise - versions: Path -} - -export interface Interpreter { - project: string // FIXME: should probably be a stronger type - args: string[] -} - -export default function usePantry() { - return { - getDeps, - getCompanions, - getProvides, - getProvider, - getInterpreter, - getRuntimeEnvironment, - available, - ls, - prefix: getPantryPrefix() - } -} - -/// returns ONE LEVEL of deps, to recurse use `hydrate.ts` -const getDeps = async (pkg: Package | PackageRequirement) => { - const yml = await entry(pkg).yml() - return parse_pkgs_node(yml.dependencies) -} - -// deno-lint-ignore no-explicit-any -function parse_pkgs_node(node: any) { - if (!node) return [] - node = validate_plain_obj(node) - platform_reduce(node) - - const rv: PackageRequirement[] = [] - for (const [project, constraint] of Object.entries(node)) { - rv.compact_push(validatePackageRequirement({ project, constraint })) - } - return rv -} - -const getProvides = async (pkg: { project: string }) => { - const yml = await entry(pkg).yml() - let node = yml["provides"] - if (!node) return [] - if (isPlainObject(node)) { - node = node[host().platform] - } - if (!isArray(node)) throw new Error("bad-yaml") - - return node.compact(x => { - if (isPlainObject(x)) { - x = x["executable"] - } - if (isString(x)) { - if (x.startsWith("bin/")) return x.slice(4) - if (x.startsWith("sbin/")) return x.slice(5) - } - }) -} - -import { parse as parseYaml } from "deno/encoding/yaml.ts" - -const getProvider = async ({ project }: { project: string }) => { - for (const prefix of pantry_paths()) { - if (!prefix.exists()) continue - const dir = prefix.join(project) - const filename = dir.join("provider.yml") - if (!filename.exists()) continue - const read = await Deno.readTextFile(filename.string) - const yaml = validate_plain_obj(await parseYaml(read)) - const cmds = validate_arr(yaml.cmds) - return (binname: string) => { - if (!cmds.includes(binname)) return - const args = yaml['args'] - if (isPlainObject(args)) { - if (args[binname]) { - return get_args(args[binname]) - } else { - return get_args(args['...']) - } - } else { - return get_args(args) - } - } - } - - function get_args(input: unknown) { - if (isString(input)) { - return input.split(/\s+/) - } else { - return validate_arr(input) - } - } -} - -const getCompanions = async (pkg: {project: string}) => { - const yml = await entry(pkg).yml() - const node = yml["companions"] - return parse_pkgs_node(node) -} - -const getInterpreter = async (extension: string): Promise => { - extension = extension.slice(1) - if (!extension) return - for await (const pkg of ls()) { - const yml = await entry(pkg).yml() - const node = yml["interprets"] - if (!isPlainObject(node)) continue - try { - const { extensions, args } = yml["interprets"] - if ((isString(extensions) && extensions === extension) || - (isArray(extensions) && extensions.includes(extension))) { - return { project: pkg.project, args: isArray(args) ? args : [args] } - } - } catch { - continue - } - } - return undefined -} - -const getRuntimeEnvironment = async (pkg: Package): Promise> => { - const yml = await entry(pkg).yml() - const obj = validate_plain_obj(yml["runtime"]?.["env"] ?? {}) - return expand_env_obj(obj, pkg, []) -} - -const available = async (pkg: PackageRequirement): Promise => { - let { platforms } = await entry(pkg).yml() - if (!platforms) return true - if (isString(platforms)) platforms = [platforms] - if (!isArray(platforms)) throw new Error("bad-yaml") - return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`) -} - -function entry({ project }: { project: string }): Entry { - for (const prefix of pantry_paths()) { - if (!prefix.exists()) throw new TeaError('not-found: pantry', { path: prefix.parent() }) - const dir = prefix.join(project) - const filename = dir.join("package.yml") - if (!filename.exists()) continue - const yml = async () => { - try { - const yml = await filename.readYAML() - if (!isPlainObject(yml)) throw null - return yml - } catch (cause) { - throw new TeaError('parser: pantry: package.yml', {cause, project, filename}) - } - } - const versions = dir.join("versions.txt") - return { dir, yml, versions } - } - - throw new TeaError('not-found: pantry: package.yml', {project}, ) -} - -/// expands platform specific keys into the object -/// expands inplace because JS is nuts and you have to suck it up -function platform_reduce(env: PlainObject) { - const sys = host() - for (const [key, value] of Object.entries(env)) { - const [os, arch] = (() => { - let match = key.match(/^(darwin|linux)\/(aarch64|x86-64)$/) - if (match) return [match[1], match[2]] - if ((match = key.match(/^(darwin|linux)$/))) return [match[1]] - if ((match = key.match(/^(aarch64|x86-64)$/))) return [,match[1]] - return [] - })() - - if (!os && !arch) continue - delete env[key] - if (os && os != sys.platform) continue - if (arch && arch != sys.arch) continue - - const dict = validate_plain_obj(value) - for (const [key, value] of Object.entries(dict)) { - // if user specifies an array then we assume we are supplementing - // otherwise we are replacing. If this is too magical let us know - if (isArray(value)) { - if (!env[key]) env[key] = [] - else if (!isArray(env[key])) env[key] = [env[key]] - //TODO if all-platforms version comes after the specific then order accordingly - env[key].push(...value) - } else { - env[key] = value - } - } - } -} - -function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installation[]): Record { - const env = {...env_} - - platform_reduce(env) - - const rv: Record = {} - - for (let [key, value] of Object.entries(env)) { - if (isArray(value)) { - value = value.map(transform).join(" ") - } else { - value = transform(value) - } - - rv[key] = value - } - - return rv - - // deno-lint-ignore no-explicit-any - function transform(value: any): string { - if (!isPrimitive(value)) throw new Error(`invalid-env-value: ${JSON.stringify(value)}`) - - if (isBoolean(value)) { - return value ? "1" : "0" - } else if (value === undefined || value === null) { - return "0" - } else if (isString(value)) { - const mm = useMoustaches() - const home = Path.home().string - const obj = [ - { from: 'env.HOME', to: home }, // historic, should be removed at v1 - { from: 'home', to: home } // remove, stick with just ~ - ] - obj.push(...mm.tokenize.all(pkg, deps)) - return mm.apply(value, obj) - } else if (isNumber(value)) { - return value.toString() - } - throw new Error("unexpected-error") - } -} - -//////////////////////////////////////////// useMoustaches() additions -import useMoustachesBase from "./useMoustaches.ts" -import { useEnv } from "./useConfig.ts" - -function useMoustaches() { - const base = useMoustachesBase() - - const deps = (deps: Installation[]) => { - const map: {from: string, to: string}[] = [] - for (const dep of deps ?? []) { - map.push({ from: `deps.${dep.pkg.project}.prefix`, to: dep.path.string }) - map.push(...useMoustaches().tokenize.version(dep.pkg.version, `deps.${dep.pkg.project}.version`)) - } - return map - } - - const tea = () => [{ from: "tea.prefix", to: usePrefix().string }] - - const all = (pkg: Package, deps_: Installation[]) => [ - ...deps(deps_), - ...tokenizePackage(pkg), - ...tea(), - ...base.tokenize.version(pkg.version), - ...base.tokenize.host(), - ] - - return { - apply: base.apply, - tokenize: { - ...base.tokenize, - deps, pkg, tea, all - } - } -} - -function tokenizePackage(pkg: Package) { - return [{ from: "prefix", to: useCellar().keg(pkg).string }] -} - -const getPantryPrefix = () => usePrefix().join('tea.xyz/var/pantry/projects') - -export function pantry_paths(): Path[] { - const rv: Path[] = [] - const { TEA_PANTRY_PATH } = useEnv() - - const prefix = getPantryPrefix() - if (prefix.isDirectory()) rv.push(prefix) - - if (TEA_PANTRY_PATH) for (const path of TEA_PANTRY_PATH.split(":").reverse()) { - rv.unshift(Path.cwd().join(path, "projects")) - } - - if (rv.length == 0) throw new TeaError("not-found: pantry", {prefix}) - - return rv -} - -interface LsEntry { - project: string - path: Path -} - -export async function* ls(): AsyncGenerator { - for (const prefix of pantry_paths()) { - for await (const path of _ls_pantry(prefix)) { - yield { - project: path.parent().relative({ to: prefix }), - path - } - } - } -} - -async function* _ls_pantry(dir: Path): AsyncGenerator { - if (!dir.isDirectory()) throw new TeaError('not-found: pantry', { path: dir }) - - for await (const [path, { name, isDirectory }] of dir.ls()) { - if (isDirectory) { - for await (const x of _ls_pantry(path)) { - yield x - } - } else if (name === "package.yml" || name === "package.yaml") { - yield path - } - } -} diff --git a/src/hooks/usePrefix.ts b/src/hooks/usePrefix.ts deleted file mode 100644 index 93479d316..000000000 --- a/src/hooks/usePrefix.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Path from "path" -import useConfig from "hooks/useConfig.ts" - -export class Prefix extends Path { - www: Path - - constructor(prefix: Path) { - super(prefix) - this.www = prefix.join("tea.xyz/var/www") - } -} - -export default function usePrefix() { - const { teaPrefix } = useConfig() - return new Prefix(teaPrefix) -} diff --git a/src/hooks/useRun.ts b/src/hooks/useRun.ts index 890ed1883..0a724605f 100644 --- a/src/hooks/useRun.ts +++ b/src/hooks/useRun.ts @@ -1,5 +1,5 @@ -import Path from "path" -import { isArray } from "is_what" +import { isArray } from "is-what" +import { Path } from "tea" export interface RunOptions extends Omit { cmd: (string | Path)[] | Path @@ -19,7 +19,7 @@ export class RunError extends Error { export default async function useRun({ spin, ...opts }: RunOptions) { const cmd = isArray(opts.cmd) ? opts.cmd.map(x => `${x}`) : [opts.cmd.string] const cwd = opts.cwd?.toString() - console.verbose({ cwd, ...opts, cmd }) + console.log({ cwd, ...opts, cmd }) const stdio = { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } as Pick if (spin) { @@ -30,7 +30,7 @@ export default async function useRun({ spin, ...opts }: RunOptions) { try { proc = _internals.nativeRun(cmd.shift()!, { ...opts, args: cmd, cwd, ...stdio }).spawn() const exit = await proc.status - console.verbose({ exit }) + console.log({ exit }) if (!exit.success) throw new RunError(exit.code, cmd) } catch (err) { if (spin && proc) { diff --git a/src/hooks/useShellEnv.ts b/src/hooks/useShellEnv.ts deleted file mode 100644 index b166cbc7b..000000000 --- a/src/hooks/useShellEnv.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Installation } from "types" -import { OrderedHashSet } from "rimbu/ordered/set/index.ts" -import { host } from "utils" -import { usePrefix, usePantry } from "hooks" - -export const EnvKeys = [ - 'PATH', - 'MANPATH', - 'PKG_CONFIG_PATH', - 'LIBRARY_PATH', - 'LD_LIBRARY_PATH', - 'CPATH', - 'XDG_DATA_DIRS', - 'CMAKE_PREFIX_PATH', - 'DYLD_FALLBACK_LIBRARY_PATH', - 'SSL_CERT_FILE', - 'LDFLAGS', - 'TEA_PREFIX', - 'ACLOCAL_PATH' -] as const -export type EnvKey = typeof EnvKeys[number] - -interface Options { - installations: Installation[] -} - -/// returns an environment that supports the provided packages -export default async function useShellEnv({installations}: Options): Promise> { - const {getRuntimeEnvironment} = usePantry() - - const vars: Partial>> = {} - const isMac = host().platform == 'darwin' - - const projects = new Set(installations.map(x => x.pkg.project)) - const has_cmake = projects.has('cmake.org') - const archaic = true - - const rv: Record = {} - const seen = new Set() - - for (const installation of installations) { - - if (!seen.insert(installation.pkg.project).inserted) { - console.warn("warning: env is being duped:", installation.pkg.project) - } - - for (const key of EnvKeys) { - for (const suffix of suffixes(key)!) { - vars[key] = compact_add(vars[key], installation.path.join(suffix).compact()?.string) - } - } - - if (archaic) { - vars.LIBRARY_PATH = compact_add(vars.LIBRARY_PATH, installation.path.join("lib").compact()?.string) - vars.CPATH = compact_add(vars.CPATH, installation.path.join("include").compact()?.string) - } - - if (has_cmake) { - vars.CMAKE_PREFIX_PATH = compact_add(vars.CMAKE_PREFIX_PATH, installation.path.string) - } - - if (projects.has('gnu.org/autoconf')) { - vars.ACLOCAL_PATH = compact_add(vars.ACLOCAL_PATH, installation.path.join("share/aclocal").compact()?.string) - } - - if (installation.pkg.project === 'openssl.org') { - const certPath = installation.path.join("ssl/cert.pem").compact()?.string - // this is a single file, so we assume a - // valid entry is correct - if (certPath) vars.SSL_CERT_FILE = OrderedHashSet.of(certPath) - } - - // pantry configured runtime environment - const runtime = await getRuntimeEnvironment(installation.pkg) - for (const key in runtime) { - rv[key] ??= [] - rv[key].push(runtime[key]) - } - } - - // this is how we use precise versions of libraries - // for your virtual environment - //FIXME SIP on macOS prevents DYLD_FALLBACK_LIBRARY_PATH from propagating to grandchild processes - if (vars.LIBRARY_PATH) { - vars.LD_LIBRARY_PATH = vars.LIBRARY_PATH - if (isMac) { - // non FALLBACK variety causes strange issues in edge cases - // where our symbols somehow override symbols from the macOS system - vars.DYLD_FALLBACK_LIBRARY_PATH = vars.LIBRARY_PATH - } - } - - for (const key of EnvKeys) { - //FIXME where is this `undefined` __happening__? - if (vars[key] === undefined || vars[key]!.isEmpty) continue - rv[key] = vars[key]!.toArray() - } - - if (isMac) { - // required to link to our libs - // tea.xyz/gx/cc automatically adds this, but use of any other compilers will not - rv["LDFLAGS"] = [`-Wl,-rpath,${usePrefix()}`] - } - - // don’t break `man` lol - rv["MANPATH"]?.push("/usr/share/man") - - return rv -} - -function suffixes(key: EnvKey) { - switch (key) { - case 'PATH': - return ["bin", "sbin"] - case 'MANPATH': - return ["share/man"] - case 'PKG_CONFIG_PATH': - return ['share/pkgconfig', 'lib/pkgconfig'] - case 'XDG_DATA_DIRS': - return ['share'] - case 'LIBRARY_PATH': - case 'LD_LIBRARY_PATH': - case 'DYLD_FALLBACK_LIBRARY_PATH': - case 'CPATH': - case 'CMAKE_PREFIX_PATH': - case 'SSL_CERT_FILE': - case 'LDFLAGS': - case 'TEA_PREFIX': - case 'ACLOCAL_PATH': - return [] // we handle these specially - default: { - const exhaustiveness_check: never = key - throw new Error(`unhandled id: ${exhaustiveness_check}`) - }} -} - -export function expand(env: Record) { - let rv = '' - for (const [key, value] of Object.entries(env)) { - if (value.length == 0) continue - rv += `export ${key}="${value.join(":")}"\n` - } - return rv -} - -export function flatten(env: Record) { - const rv: Record = {} - for (const [key, value] of Object.entries(env)) { - rv[key] = value.join(":") - } - return rv -} - -function compact_add(set: OrderedHashSet | undefined, item: T | null | undefined): OrderedHashSet { - if (!set) set = OrderedHashSet.empty() - if (item) set = set.add(item) - - return set -} diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts deleted file mode 100644 index 0f9f272fb..000000000 --- a/src/hooks/useSync.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useCellar, useConfig, useDownload, usePantry } from "hooks" -import useRun, { RunOptions } from "./useRun.ts" -import useLogger from "./useLogger.ts" -import * as semver from "semver" -import Path from "path" -import { host } from "../utils/index.ts"; - -export default async function() { - const logger = useLogger() - const pantry_dir = usePantry().prefix.parent() - - logger.replace("syncing pantries…") - - const { rid } = await Deno.open(pantry_dir.mkpath().string) - await Deno.flock(rid, true) - - try { - //TODO if there was already a lock, just wait on it, don’t do the following stuff - - const git_dir = pantry_dir.parent().join("pantries/teaxyz/pantry") - - if (git_dir.join("HEAD").isFile()) { - await git("-C", git_dir, "fetch", "origin", "--force", "main:main") - } else { - await git("clone", "--bare", "--depth=1", "https://github.com/teaxyz/pantry", git_dir) - } - - await git("--git-dir", git_dir, "--work-tree", pantry_dir, "checkout", "--force") - - } catch { - // git failure or no git installed - // ∴ download the latest tarball and uncompress over the top - //FIXME deleted packages will not be removed with this method - //TODO parallelize - const src = new URL(`https://github.com/teaxyz/pantry/archive/refs/heads/main.tar.gz`) - const tgz = await useDownload().download({ src }) - await run({cmd: ["tar", "xzf", tgz, "--strip-components=1"], cwd: pantry_dir }) - } finally { - //TODO if this gets stuck then nothing will work so need a handler for that - await Deno.funlock(rid) - Deno.close(rid) // docs aren't clear if we need to do this or not - } - - logger.replace("pantries sync’d ⎷") -} - -//////////////////////// utils - -/// we support a tea installed or system installed git, nothing else -/// eg. `git` could be a symlink in `PATH` to tea, which would cause a fork bomb -/// on darwin if xcode or xcode/clt is not installed this will fail to our http fallback above -async function git(...args: (string | Path)[]) { - const pkg = await useCellar().has({ project: 'git-scm.org', constraint: new semver.Range('*') }) - const git = (pkg?.path ?? usr())?.join("bin/git") - if (!git) throw new Error("no-git") // caught above to trigger http download instead - await run({cmd: [git, ...args]}) - - function usr() { - // only return /usr/bin if in the PATH so user can explicitly override this - const rv = Deno.env.get("PATH")?.split(":")?.includes("/usr/bin") ? new Path("/usr") : undefined - - /// don’t cause macOS to abort and then prompt the user to install the XcodeCLT - //FIXME test! but this is hard to test without docker images or something! - if (host().platform == 'darwin') { - if (new Path("/Library/Developer/CommandLineTools/usr/bin/git").isExecutableFile()) return rv - if (new Path("/Application/Xcode.app").isDirectory()) return rv - return // don’t use `git` - } - - return rv - } -} - -function run(opts: RunOptions) { - const spin = useConfig().verbosity < 1 - return useRun({ ...opts, spin }) -} diff --git a/src/hooks/useVersion.ts b/src/hooks/useVersion.ts index 1248113b2..8ede4baa7 100644 --- a/src/hooks/useVersion.ts +++ b/src/hooks/useVersion.ts @@ -1,8 +1,9 @@ import { README } from "./useVirtualEnv.ts" +import { Path } from "tea" const version = `${( await README( - new URL(import.meta.url).path() + new Path(new URL(import.meta.url).pathname) .parent().parent().parent() .join("README.md") ).swallow(/not-found/))?.version}+dev` diff --git a/src/hooks/useVirtualEnv.ts b/src/hooks/useVirtualEnv.ts index 07a09a952..5cc777425 100644 --- a/src/hooks/useVirtualEnv.ts +++ b/src/hooks/useVirtualEnv.ts @@ -1,11 +1,10 @@ import { usePackageYAMLFrontMatter, refineFrontMatter, FrontMatter } from "./usePackageYAML.ts" -import { flatmap, pkg, TeaError, validate_plain_obj } from "utils" -import { useEnv, useMoustaches, usePrefix } from "hooks" -import { PackageRequirement } from "types" -import SemVer, * as semver from "semver" -import { isPlainObject } from "is_what" +import { PackageRequirement, Path, SemVer, utils, TeaError, hooks, semver } from "tea" +const { flatmap, pkg, validate } = utils +import { isPlainObject } from "is-what" +import useConfig from "./useConfig.ts" +const { useMoustaches } = hooks import { JSONC } from "jsonc" -import Path from "path" export interface VirtualEnv { pkgs: PackageRequirement[] @@ -18,11 +17,9 @@ export interface VirtualEnv { // we call into useVirtualEnv a bunch of times const cache: Record = {} -export default async function(cwd: Path = Path.cwd()): Promise { - const { TEA_DIR, getEnvAsObject } = useEnv() - - const teaDir = flatmap(TEA_DIR, Path.cwd().join) - if (teaDir) cwd = teaDir +export default async function(cwd?: Path): Promise { + const { TEA_DIR } = useConfig().env + cwd = TEA_DIR ?? cwd ?? Path.cwd() if (cache[cwd.string]) return cache[cwd.string] @@ -53,19 +50,19 @@ export default async function(cwd: Path = Path.cwd()): Promise { err.cause = f throw err } - if (teaDir && dir.eq(teaDir)) break + if (TEA_DIR && dir.eq(TEA_DIR)) break dir = dir.parent() } } const lastd = teafiles.slice(-1)[0]?.parent() - if (teaDir) { - srcroot = teaDir + if (TEA_DIR) { + srcroot = TEA_DIR } else if (!srcroot || lastd?.components().length < srcroot.components().length) { srcroot = lastd } - if (!srcroot) throw new TeaError("not-found: dev-env", {cwd, teaDir}) + if (!srcroot) throw new TeaError("not-found: dev-env", {cwd, TEA_DIR}) for (const [key, value] of Object.entries(env)) { if (key != 'TEA_PREFIX') { @@ -80,7 +77,7 @@ export default async function(cwd: Path = Path.cwd()): Promise { const moustaches = useMoustaches() const foo = [ ...moustaches.tokenize.host(), - { from: "tea.prefix", to: usePrefix().string }, + { from: "tea.prefix", to: useConfig().prefix.string }, { from: "home", to: Path.home().string }, { from: "srcroot", to: srcroot!.string} ] @@ -185,7 +182,7 @@ export default async function(cwd: Path = Path.cwd()): Promise { flatmap(semver.parse(json?.version), v => version = v) } if (_if("action.yml", "action.yaml")) { - const yaml = validate_plain_obj(await f!.readYAML()) + const yaml = validate.obj(await f!.readYAML()) const [,v] = yaml.runs?.using.match(/node(\d+)/) ?? [] pkgs.push({ project: "nodejs.org", @@ -237,21 +234,6 @@ export default async function(cwd: Path = Path.cwd()): Promise { if (_if_d(".hg", ".svn")) { srcroot ??= f } - if ((f = dir.join(".envrc").isFile())) { - //TODO really we should pkg `direnv` install it if we find this file and configure it to do the following - const subst = getEnvAsObject() - subst.SRCROOT = "{{srcroot}}" - subst.TEA_PREFIX = "{{tea.prefix}}" - subst.VERSION = "{{version}}" - for await (const line of f!.readLines()) { - let [,key,value] = line.match(/^export (\S+)=(.*)$/) ?? [] - if (key && value) for (const [key, value_subst] of Object.entries(subst)) { - value = value.replaceAll(`$${key}`, value_subst) - } - env[key] = value - } - srcroot ??= f - } } } diff --git a/src/init.ts b/src/init.ts deleted file mode 100644 index 56090b2e6..000000000 --- a/src/init.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Flags } from "./args.ts" -import { applyConfig, Config, Env } from "hooks/useConfig.ts" -import Path from "path"; -import { isNumber } from "is_what" -import { Verbosity } from "./types.ts"; -import { flatmap } from "utils" - -export function init(flags?: Flags) { - const config = createConfig(flags) - applyConfig(config) -} - -export function createConfig(flags?: Flags): Config { - const env = collectEnv() - - const isCI = !!env.CI - - const execPath = new Path(Deno.execPath()) - const loggerGlobalPrefix = (!Deno.isatty(Deno.stdout.rid) || isCI) ? "tea:" : undefined - const teaPrefix = findTeaPrefix(execPath, env.TEA_PREFIX) - - const verbosity = getVerbosity(flags?.verbosity, env) - const dryrun = !!flags?.dryrun - const keepGoing = !!flags?.keepGoing - - const json = !!flags?.json - - return { - isCI, - execPath, - loggerGlobalPrefix, - teaPrefix, - verbosity, - dryrun, - keepGoing, - verbose: verbosity >= Verbosity.loud, - debug: verbosity >= Verbosity.debug, - silent: verbosity <= Verbosity.quiet, - env, - json, - } -} - -export function collectEnv(): Env { - return { - CI: Deno.env.get("CI"), - CLICOLOR: Deno.env.get("CLICOLOR"), - CLICOLOR_FORCE: Deno.env.get("CLICOLOR_FORCE"), - DEBUG: Deno.env.get("DEBUG"), - GITHUB_ACTIONS: Deno.env.get("GITHUB_ACTIONS"), - GITHUB_TOKEN: Deno.env.get("GITHUB_TOKEN"), - NO_COLOR: Deno.env.get("NO_COLOR"), - PATH: Deno.env.get("PATH"), - RUNNER_DEBUG: Deno.env.get("RUNNER_DEBUG"), - SHELL: Deno.env.get("SHELL"), - SRCROOT: Deno.env.get("SRCROOT"), - TEA_DIR: Deno.env.get("TEA_DIR"), - TEA_FILES: Deno.env.get("TEA_FILES"), - TEA_FORK_BOMB_PROTECTOR: Deno.env.get("TEA_FORK_BOMB_PROTECTOR"), - TEA_MAGIC: Deno.env.get("TEA_MAGIC"), - TEA_PANTRY_PATH: Deno.env.get("TEA_PANTRY_PATH"), - TEA_PKGS: Deno.env.get("TEA_PKGS"), - TEA_PREFIX: Deno.env.get("TEA_PREFIX"), - TEA_REWIND: Deno.env.get("TEA_REWIND"), - VERBOSE: Deno.env.get("VERBOSE"), - VERSION: Deno.env.get("VERSION") - } -} - -export const findTeaPrefix = (execPath: Path, envVar?: string) => { - //NOTE doesn't work for scripts as Deno.run doesn't push through most env :/ - if (envVar) { - return new Path(envVar) - } else { - // we’re either deno.land/vx/bin/deno, tea.xyz/vx/bin/tea or some symlink to the latter - const shelf = execPath - .readlink() // resolves the leaf symlink (if any) - .parent() - .parent() - .parent() - - switch (shelf.basename()) { - case 'tea.xyz': - case 'deno.land': - return shelf.parent() - default: - // we’re being generous for users who just download `tea` by itself - // and execute it without installing it in a sanctioned structure - return Path.home().join(".tea") - } - } -} - -function getVerbosity(v: number | undefined, env: Env): Verbosity { - const { DEBUG, GITHUB_ACTIONS, RUNNER_DEBUG, VERBOSE } = env - - if (isNumber(v)) return v - if (DEBUG == '1') return Verbosity.debug - if (GITHUB_ACTIONS == 'true' && RUNNER_DEBUG == '1') return Verbosity.debug - - const verbosity = flatmap(VERBOSE, parseInt) - return isNumber(verbosity) ? verbosity : Verbosity.normal -} diff --git a/src/prefab/README.md b/src/prefab/README.md deleted file mode 100644 index 48703d16e..000000000 --- a/src/prefab/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Prefabs - -* **Don’t** offer informative messages - * Thus they should be units that represent something you would inform the - user about *before* you call them -* **Do** copious verbose messages -* **Don’t** do stdout, they return data you *may* want to stdout diff --git a/src/prefab/hydrate.ts b/src/prefab/hydrate.ts deleted file mode 100644 index 8af0c295a..000000000 --- a/src/prefab/hydrate.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { PackageRequirement, Package } from "types" -import { isArray } from "is_what" -import * as semver from "semver" -import { usePantry } from "hooks" -import "utils" - - -//TODO linktime cyclic dependencies cannot be allowed -//NOTE however if they aren’t link time it's presumably ok in some scenarios -// eg a tool that lists a directory may depend on a tool that identifies the -// mime types of files which could depend on the listing tool -//FIXME actually we are not refining the constraints currently -//TODO we are not actually restricting subsequent asks, eg. deno^1 but then deno^1.2 - - -interface ReturnValue { - /// full list topologically sorted (ie dry + wet) - pkgs: PackageRequirement[] - - /// your input, but version constraints refined based on the whole graph - /// eg. you hydrate the graph for a and b, but b depends on a tighter range of a than you input - dry: PackageRequirement[] - - /// packages that were not supplied to input or that require bootstrap - wet: PackageRequirement[] - - /// the graph cycles at these packages - /// this is only a problem if you need to build one of these, - // in which case TADA! here's the list! - bootstrap_required: Set -} - -const get = (x: PackageRequirement) => usePantry().getDeps(x) - -/// sorts a list of packages topologically based on their -/// dependencies. Throws if there is a cycle in the input. -/// ignores changes in dependencies based on versions -export default async function hydrate( - input: (PackageRequirement | Package)[] | (PackageRequirement | Package), - get_deps: (pkg: PackageRequirement, dry: boolean) => Promise = get, -): Promise -{ - if (!isArray(input)) input = [input] - - const dry = condense(input.map(spec => { - if ("version" in spec) { - return {project: spec.project, constraint: new semver.Range(`=${spec.version}`)} - } else { - return spec - } - })) - - const graph: Record = {} - const bootstrap = new Set() - const initial_set = new Set(dry.map(x => x.project)) - - const go = async (target: Node) => { - /// we trace up a target pkg’s dependency graph - /// the target pkg is thus the youngest child and we are ascending up its parents - const ascend = async (node: Node, children: Set) => { - - for (const dep of await get_deps(node.pkg, initial_set.has(node.project))) { - - if (children.has(dep.project)) { - if (!bootstrap.has(dep.project)) { - console.warn(`${dep.project} must be bootstrapped to build ${node.project}`) - - //TODO the bootstrap should keep the version constraint since it may be different - bootstrap.add(dep.project) - } - } else { - const found = graph[dep.project] - if (found) { - /// we already traced this graph - - if (found.count() < node.count()) { - found.parent = node - } - - //FIXME strictly we only have to constrain graphs that contain linkage - // ie. you cannot have a binary that links two separate versions of eg. openssl - // or (maybe) services, eg. you might suffer if there are two versions of postgres running (though tea mitigates this) - found.pkg.constraint = semver.intersect(found.pkg.constraint, dep.constraint) - - } else { - const new_node = new Node(dep, node) - graph[dep.project] = new_node - await ascend(new_node, new Set([...children, dep.project])) - } - } - } - } - await ascend(target, new Set([target.project])) - } - - for (const pkg of dry) { - if (pkg.project in graph) { - graph[pkg.project].pkg.constraint = semver.intersect(graph[pkg.project].pkg.constraint, pkg.constraint) - } else { - const new_node = new Node(pkg) - graph[pkg.project] = new_node - await go(new_node) - } - } - - const pkgs = Object.values(graph) - .sort((a, b) => b.count() - a.count()) - .map(({pkg}) => pkg) - - //TODO strictly we need to record precisely the bootstrap version constraint - const bootstrap_required = new Set(pkgs.compact(({project}) => bootstrap.has(project) && project)) - - return { - pkgs, - dry: pkgs.filter(({project}) => initial_set.has(project)), - wet: pkgs.filter(({project}) => !initial_set.has(project) || bootstrap_required.has(project)), - bootstrap_required - } -} - -function condense(pkgs: PackageRequirement[]) { - const out: PackageRequirement[] = [] - for (const pkg of pkgs) { - const found = out.find(x => x.project === pkg.project) - if (found) { - found.constraint = semver.intersect(found.constraint, pkg.constraint) - } else { - out.push(pkg) - } - } - return out -} - - -/////////////////////////////////////////////////////////////////////////// lib -class Node { - parent: Node | undefined - readonly pkg: PackageRequirement - readonly project: string - - constructor(pkg: PackageRequirement, parent?: Node) { - this.parent = parent - this.pkg = pkg - this.project = pkg.project - } - - count(): number { - let n = 0 - let node = this as Node | undefined - // deno-lint-ignore no-cond-assign - while (node = node?.parent) n++ - return n - } -} diff --git a/src/prefab/index.ts b/src/prefab/index.ts deleted file mode 100644 index f6adb339d..000000000 --- a/src/prefab/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import hydrate from "./hydrate.ts" -import install from "./install.ts" -import link from "./link.ts" -import resolve from "./resolve.ts" - -export { - hydrate, - install, - link, - resolve -} diff --git a/src/prefab/install.ts b/src/prefab/install.ts deleted file mode 100644 index 945979a8f..000000000 --- a/src/prefab/install.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { usePrefix, useCache, useCellar, useDownload, useOffLicense, useFetch, useConfig } from "hooks" -import { host, panic, pkg as pkgutils, undent } from "utils" -import useLogger, { Logger, red, teal, gray, logJSON } from "hooks/useLogger.ts" -import { ExitError, Installation, StowageNativeBottle } from "types" -import { crypto, toHashString } from "deno/crypto/mod.ts" -import { Package } from "types" -import Path from "path" - -export default async function install(pkg: Package, logger?: Logger): Promise { - const { project, version } = pkg - logger ??= useLogger(pkgutils.str(pkg)) - - const cellar = useCellar() - const tea_prefix = usePrefix() - const { isCI, dryrun, json, env } = useConfig() - const compression = get_compression(isCI) - const stowage = StowageNativeBottle({ pkg: { project, version }, compression }) - const url = useOffLicense('s3').url(stowage) - const tarball = useCache().path(stowage) - const shelf = tea_prefix.join(pkg.project) - - const pkg_prefix_str = (pkg: Package) => [ - gray(usePrefix().prettyString()), - pkg.project, - `${gray('v')}${pkg.version}` - ].join(gray('/')) - - if (env.TEA_MAGIC?.split(":").includes("prompt")) { - if (!Deno.isatty(Deno.stdin.rid)) { - throw new Error("TEA_MAGIC=prompt but stdin is not a tty") - } - - do { - const val = prompt(undent` - ┌ ⚠️ tea requests to install ${pkg_prefix_str(pkg)} - └ \x1B[1mallow?\x1B[0m [y/n]` - )?.toLowerCase() - - if (val === "y") { - break - } - if (val === "n") { - throw new ExitError(1) - } - } while (true) - } - - const log_install_msg = (install: Installation, title = 'installed') => { - if (json) { - logJSON({status: title, pkg: pkgutils.str(install.pkg)}) - } else { - const str = pkg_prefix_str(install.pkg) - logger!.replace(`${title}: ${str}`, { prefix: false }) - } - } - - if (dryrun) { - const install = { pkg, path: tea_prefix.join(pkg.project, `v${pkg.version}`) } - log_install_msg(install, 'imagined') - return install - } - - if (!json) { - logger.replace(teal("locking")) - } else { - logJSON({status: "locking", pkg: pkgutils.str(pkg) }) - } - const { rid } = await Deno.open(shelf.mkpath().string) - await Deno.flock(rid, true) - - try { - const already_installed = await cellar.has(pkg) - if (already_installed) { - // some other tea instance installed us while we were waiting for the lock - // or potentially we were already installed and the caller is naughty - if (!json) { - logger.replace(teal("installed")) - } else { - logJSON({status: "installed", pkg: pkgutils.str(pkg) }) - } - return already_installed - } - - if (!json) { - logger.replace(teal("querying")) - } else { - logJSON({status: "querying", pkg: pkgutils.str(pkg) }) - } - - let stream = await useDownload().stream({ src: url, logger, dst: tarball }) - const is_downloading = stream !== undefined - stream ??= await Deno.open(tarball.string, {read: true}).then(f => f.readable) ?? panic() - const tar_args = compression == 'xz' ? 'xJ' : 'xz' // laughably confusing - const tee = stream.tee() - const pp: Promise[] = [] - - if (is_downloading) { // cache the download (write stream to disk) - tarball.parent().mkpath() - const f = await Deno.open(tarball.string, {create: true, write: true, truncate: true}) - const teee = tee[1].tee() - pp.push(teee[0].pipeTo(f.writable)) - tee[1] = teee[1] - } - - const tmpdir = Path.mktemp({ - prefix: pkg.project.replaceAll("/", "_") + "_", - dir: usePrefix().join("local/tmp") - //NOTE ^^ inside tea prefix to avoid TMPDIR is on a different volume problems - }) - const untar = new Deno.Command("tar", { - args: [tar_args, "--strip-components", (pkg.project.split("/").length + 1).toString()], - stdin: 'piped', stdout: "inherit", stderr: "inherit", - cwd: tmpdir.string, - }).spawn() - - pp.unshift( - crypto.subtle.digest("SHA-256", tee[0]).then(toHashString), - remote_SHA(new URL(`${url}.sha256sum`)), - untar.status, - tee[1].pipeTo(untar.stdin) - ) - - const [computed_hash_value, checksum, tar_exit_status] = await Promise.all(pp) as [string, string, Deno.CommandStatus] - - if (!tar_exit_status.success) { - throw new Error(`tar exited with status ${tar_exit_status.code}`) - } - - if (computed_hash_value != checksum) { - if (!json) logger.replace(red('error')) - tarball.rm() - console.error("we deleted the invalid tarball. try again?") - throw new Error(`sha: expected: ${checksum}, got: ${computed_hash_value}`) - } - - const path = tmpdir.mv({ to: shelf.join(`v${pkg.version}`) }) - const install = { pkg, path } - - log_install_msg(install) - - return install - } catch (err) { - tarball.rm() //FIXME resumable downloads! - throw err - } finally { - await Deno.funlock(rid) - Deno.close(rid) // docs aren't clear if we need to do this or not - } -} - -async function remote_SHA(url: URL) { - const rsp = await useFetch(url) - if (!rsp.ok) throw rsp - const txt = await rsp.text() - return txt.split(' ')[0] -} - -function get_compression(isCI: boolean) { - if (isCI) return 'gz' // in CI CPU is more constrained than bandwidth - if (host().platform == 'darwin') return 'xz' // most users are richer in CPU than bandwidth - // TODO determine if `tar` can handle xz - return 'gz' -} diff --git a/src/prefab/link.ts b/src/prefab/link.ts deleted file mode 100644 index f3788e053..000000000 --- a/src/prefab/link.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Package, Installation } from "types" -import { useCellar, useConfig } from "hooks" -import SemVer, * as semver from "semver" -import { panic } from "utils" -import Path from "path" - - -export default async function link(pkg: Package | Installation) { - if (useConfig().dryrun) return - - const installation = await useCellar().resolve(pkg) - pkg = installation.pkg - - const versions = (await useCellar() - .ls(installation.pkg.project)) - .map(({pkg: {version}, path}) => [version, path] as [SemVer, Path]) - .sort(([a],[b]) => a.compare(b)) - - if (versions.length <= 0) { - console.error(pkg, installation) - throw new Error(`no versions`) - } - - const shelf = installation.path.parent() - const newest = versions.slice(-1)[0] - const vMm = `${pkg.version.major}.${pkg.version.minor}` - const minorRange = new semver.Range(`^${vMm}`) - const mostMinor = versions.filter(v => minorRange.satisfies(v[0])).at(-1) ?? panic() - - if (mostMinor[0].neq(pkg.version)) return - // ^^ if we’re not the most minor we definitely not the most major - - await makeSymlink(`v${vMm}`) - - const majorRange = new semver.Range(`^${pkg.version.major.toString()}`) - const mostMajor = versions.filter(v => majorRange.satisfies(v[0])).at(-1) ?? panic() - - if (mostMajor[0].neq(pkg.version)) return - // ^^ if we’re not the most major we definitely aren’t the newest - - await makeSymlink(`v${pkg.version.major}`) - - if (pkg.version.eq(newest[0])) { - await makeSymlink('v*') - } - - async function makeSymlink(symname: string) { - const to = shelf.join(symname) - console.verbose({ "symlinking:": to }) - await shelf.symlink({ from: (await installation).path, to, force: true }) - } -} diff --git a/src/prefab/resolve.ts b/src/prefab/resolve.ts deleted file mode 100644 index ddc5c7ff5..000000000 --- a/src/prefab/resolve.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Package, PackageRequirement, Installation } from "types" -import { useCellar, useInventory } from "hooks" -import { TeaError } from "utils" - -/// NOTE resolves to bottles -/// NOTE contract there are no duplicate projects in input - -interface RT { - /// fully resolved list (includes both installed and pending) - pkgs: Package[] - - /// already installed packages - installed: Installation[] - - /// these are the pkgs that aren’t yet installed - pending: Package[] -} - -/// 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 -/// node 19 is the latest we return node 13. if `update` is true we return node -/// 19 and *you will need to install it*. -export default async function resolve(reqs: (Package | PackageRequirement)[], {update}: {update: boolean} = {update: false}): Promise { - const inventory = useInventory() - const cellar = useCellar() - const rv: RT = { pkgs: [], installed: [], pending: [] } - let installation: Installation | undefined - for (const req of reqs) { - if (!update && (installation = await cellar.has(req))) { - // if something is already installed that satisfies the constraint then use it - rv.installed.push(installation) - rv.pkgs.push(installation.pkg) - } else { - const version = await inventory.select(req) - if (!version) { - throw new TeaError("not-found: pkg.version", {pkg: req}) - } - const pkg = { version, project: req.project } - rv.pkgs.push(pkg) - - if ((installation = await cellar.has(pkg))) { - rv.installed.push(installation) - } else { - rv.pending.push(pkg) - } - } - } - return rv -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 1269e9f50..000000000 --- a/src/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -import SemVer, { Range as SemVerRange } from "semver" -import Path from "path" -import { host } from "./utils/index.ts" - -export interface Package { - project: string - version: SemVer -} - -export interface PackageRequirement { - project: string - constraint: SemVerRange -} - -export type PackageSpecification = Package | PackageRequirement - -export interface Installation { - path: Path - pkg: Package -} - -export enum Verbosity { - quiet = -1, - normal = 0, - loud = 1, - debug = 2, - trace = 3 -} - -// when we support more variants of these that require specification -// we will tuple a version in with each eg. 'darwin' | ['windows', 10 | 11 | '*'] -export const SupportedPlatforms = ["darwin" , "linux" , "windows" , "freebsd" , "netbsd" , "aix" , "solaris" , "illumos"] as const -export type SupportedPlatform = typeof SupportedPlatforms[number] - -export const SupportedArchitectures = ["x86-64", "aarch64"] as const -export type SupportedArchitecture = typeof SupportedArchitectures[number] - -/// remotely available package content (bottles or source tarball) -export type Stowage = { - type: 'src' - pkg: Package - extname: string -} | { - type: 'bottle' - pkg: Package - compression: 'xz' | 'gz' - host?: { platform: SupportedPlatform, arch: SupportedArchitecture } -} - -/// once downloaded, `Stowage` becomes `Stowed` -export type Stowed = Stowage & { path: Path } - -export function StowageNativeBottle(opts: { pkg: Package, compression: 'xz' | 'gz' }): Stowage { - return { ...opts, host: host(), type: 'bottle' } -} - -// ExitError will cause the application to exit with the specified exit code if it bubbles -// up to the main error handler -export class ExitError extends Error { - code: number - constructor(code: number) { - super(`exiting with code: ${code}`) - this.code = code - } -} diff --git a/src/utils/error.ts b/src/utils/error.ts deleted file mode 100644 index 709c198fb..000000000 --- a/src/utils/error.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { PlainObject } from "is_what" -import { undent, pkg } from "utils" - -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: ErrorOptions = {cause: ctx.cause} - - super(msg, opts) - this.id = id ?? msg - this.ctx = ctx - } -} - -export class UsageError extends Error -{} - -export { panic } from "./safe-utils.ts" - -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}) - } - } - } -} diff --git a/src/utils/hacks.ts b/src/utils/hacks.ts deleted file mode 100644 index 745f5db8d..000000000 --- a/src/utils/hacks.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { PackageRequirement } from "types" -import { PlainObject } from "is_what" -import { host, validate_str } from "utils" -import { isString, isNumber } from "is_what" -import * as semver from "semver" - -export function validatePackageRequirement(input: PlainObject): PackageRequirement | undefined { - let { constraint, project } = input - - if (host().platform == 'darwin' && (project == "apple.com/xcode/clt" || project == "tea.xyz/gx/make")) { - // Apple will error out and prompt the user to install - //NOTE what we would really like is to error out when this dependency is *used* - // this is not the right place to error that. so FIXME - return // compact this dep away - } - if (host().platform == 'linux' && project == "tea.xyz/gx/make") { - project = "gnu.org/make" - constraint = '*' - } - - validate_str(project) - - //HACKS - if (constraint == 'c99' && project == 'tea.xyz/gx/cc') { - constraint = '^0.1' - } - - if (constraint === undefined) { - constraint = '*' - } else if (isNumber(constraint)) { - //FIXME change all pantry entries to use proper syntax - constraint = `^${constraint}` - } - if (!isString(constraint)) { - throw new Error(`invalid constraint: ${constraint}`) - } else if (/^\d/.test(constraint)) { - //FIXME change all pantry entries to use proper syntax - constraint = `^${constraint}` - } - - constraint = new semver.Range(constraint) - - return { - project, - constraint - } -} diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 267b937ce..000000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,202 +0,0 @@ -//CONTRACT you can’t use anything from hooks - -import { isString, isPlainObject, isArray, isRegExp, PlainObject } from "is_what" - -// deno-lint-ignore no-explicit-any -export function validate_str(input: any): string { - if (typeof input == 'boolean') return input ? 'true' : 'false' - if (typeof input == 'number') return input.toString() - if (typeof input != 'string') throw new Error(`not-string: ${input}`) - return input -} - -// deno-lint-ignore no-explicit-any -export function validate_plain_obj(input: any): PlainObject { - if (!isPlainObject(input)) throw new Error(`not-plain-obj: ${JSON.stringify(input)}`) - return input -} - -// deno-lint-ignore no-explicit-any -export function validate_arr(input: any): Array { - if (!isArray(input)) throw new Error(`not-array: ${JSON.stringify(input)}`) - return input -} - -////////////////////////////////////////////////////////////// base extensions -import outdent from "outdent" -export { outdent as undent } - -declare global { - interface Array { - compact(body?: (t: T) => S | null | undefined | false, opts?: { rescue: boolean }): Array - compact_push(item: T | undefined | null): void - compact_unshift(item: T | undefined | null): void - chuzzle(): Array | undefined - uniq(): Array - } - - interface String { - chuzzle(): string | undefined - } - - interface Console { - // deno-lint-ignore no-explicit-any - verbose(...args: any[]): void - - /// prohibits standard logging unless verbosity is loud or above - silence(body: () => Promise): Promise - } - - interface Set { - insert(t: T): { inserted: boolean } - } -} - -String.prototype.chuzzle = function() { - return this.trim() || undefined -} - -export { chuzzle } from "./safe-utils.ts" - -Set.prototype.insert = function(t: T) { - if (this.has(t)) { - return {inserted: false} - } else { - this.add(t) - return {inserted: true} - } -} - -Array.prototype.uniq = function(): Array { - const set = new Set() - return this.compact(x => { - const s = x.toString() - if (set.has(s)) return - set.add(s) - return x - }) -} - -Array.prototype.compact = function(body?: (t: T) => S | null | undefined | false, opts?: { rescue: boolean }) { - const rv: Array = [] - for (const e of this) { - try { - const f = body ? body(e) : e - if (f) rv.push(f) - } catch (err) { - if (opts === undefined || opts.rescue === false) throw err - } - } - return rv -} - -//TODO would be nice to chuzzle contents to reduce first -Array.prototype.chuzzle = function() { - if (this.length <= 0) { - return undefined - } else { - return this - } -} - -console.verbose = console.error -console.debug = console.error - -Array.prototype.compact_push = function(item: T | null | undefined) { - if (item) this.push(item) -} - -export function flatmap(t: T | undefined | null, body: (t: T) => S | undefined, opts?: {rescue?: boolean}): NonNullable | undefined { - try { - if (t) return body(t) ?? undefined - } catch (err) { - if (!opts?.rescue) throw err - } -} - -export async function async_flatmap(t: Promise, body: (t: T) => Promise | undefined, opts?: {rescue?: boolean}): Promise | undefined> { - try { - const tt = await t - if (tt) return await body(tt) ?? undefined - } catch (err) { - if (!opts?.rescue) throw err - } -} - -declare global { - interface Promise { - swallow(err?: unknown): Promise - } -} - -Promise.prototype.swallow = function(gristle?: unknown) { - 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) { - throw err - } - return undefined - }) -} - -///////////////////////////////////////////////////////////////////////// misc -import TeaError, { UsageError, panic } from "./error.ts" -export { TeaError, UsageError, panic } -export * as error from "./error.ts" - -///////////////////////////////////////////////////////////////////////// pkgs -export * as pkg from "./pkg.ts" - -///////////////////////////////////////////////////////////////////// platform -import { SupportedPlatform, SupportedArchitecture } from "types" - -interface HostReturnValue { - platform: SupportedPlatform - arch: SupportedArchitecture - target: string - build_ids: [SupportedPlatform, SupportedArchitecture] -} - -export function host(): HostReturnValue { - const arch = (() => { - switch (Deno.build.arch) { - case "aarch64": return "aarch64" - case "x86_64": return "x86-64" - // ^^ ∵ https://en.wikipedia.org/wiki/X86-64 and semver.org prohibits underscores - default: - throw new Error(`unsupported-arch: ${Deno.build.arch}`) - } - })() - - const { target } = Deno.build - - const platform = (() => { - switch (Deno.build.os) { - case "darwin": - case "linux": - case "windows": - return Deno.build.os - default: - console.warn("assuming linux mode for:", Deno.build.os) - return 'linux' - } - })() - - return { - platform, - arch, - target, - build_ids: [platform, arch] - } -} diff --git a/src/utils/pkg.ts b/src/utils/pkg.ts deleted file mode 100644 index fe2db6025..000000000 --- a/src/utils/pkg.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Package, PackageRequirement } from "types" -import * as semver from "semver" - -/// allows inputs `nodejs.org@16` when `semver.parse` would reject -export function parse(input: string): PackageRequirement { - const match = input.match(/^(.+?)([\^=~<>@].+)?$/) - if (!match) throw new Error(`invalid pkgspec: ${input}`) - if (!match[2]) match[2] = "*" - - const project = match[1] - - if (match[2] == "@latest") { - return { project, constraint: new semver.Range('*') } - } else { - // @ is not a valid semver operator, but people expect it to work like so: - // @5 => latest 5.x (ie ^5) - // @5.1 => latest 5.1.x - // @5.1.0 => latest 5.1.0 (usually 5.1.0 since most stuff hasn't got more digits) - if (match[2].startsWith("@")) { - const v = match[2].slice(1) - const parts = v.split(".") - const n = parts.length - switch (n) { - case 1: - match[2] = `^${v}` - break - case 2: - match[2] = `~${v}` - break - default: { - const x = parseInt(parts.pop()!) + 1 - match[2] = `>=${v} <${parts.join('.')}.${x}` - }} - } - - const constraint = new semver.Range(match[2]) - return { project, constraint } - } -} - -export function compare(a: Package, b: Package): number { - return a.project === b.project - ? a.version.compare(b.version) - : a.project.localeCompare(b.project) -} - -export function str(pkg: Package | PackageRequirement): string { - if (!("constraint" in pkg)) { - return `${pkg.project}=${pkg.version}` - } else if (pkg.constraint.set === "*") { - return pkg.project - } else { - return `${pkg.project}${pkg.constraint}` - } -} diff --git a/src/utils/safe-utils.ts b/src/utils/safe-utils.ts deleted file mode 100644 index 7b1c9b23b..000000000 --- a/src/utils/safe-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -// utils safe enough “pure” stuff (eg. semver.ts, Path.ts) - -export function chuzzle(input: number) { - return Number.isNaN(input) ? undefined : input -} - -export function panic(message?: string): never { - throw new Error(message) -} diff --git a/src/utils/semver.ts b/src/utils/semver.ts deleted file mode 100644 index f7b0636af..000000000 --- a/src/utils/semver.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { isArray, isString } from "is_what" - -/** - * we have our own implementation because open source is full of weird - * but *almost* valid semver schemes, eg: - * openssl 1.1.1q - * ghc 5.64.3.2 - * it also allows us to implement semver_intersection without hating our lives - */ -export default class SemVer { - readonly components: number[] - - major: number - minor: number - patch: number - - //FIXME parse these - readonly prerelease: string[] = [] - readonly build: string[] = [] - - readonly raw: string - readonly pretty?: string - - constructor(input: string | number[] | Range | SemVer) { - if (typeof input == 'string') { - const vprefix = input.startsWith('v') - const raw = vprefix ? input.slice(1) : input - const parts = raw.split('.') - let pretty_is_raw = false - this.components = parts.flatMap((x, index) => { - const match = x.match(/^(\d+)([a-z])$/) - if (match) { - if (index != parts.length - 1) throw new Error(`invalid version: ${input}`) - const n = parseInt(match[1]) - if (isNaN(n)) throw new Error(`invalid version: ${input}`) - pretty_is_raw = true - return [n, char_to_num(match[2])] - } else if (/^\d+$/.test(x)) { - const n = parseInt(x) // parseInt will parse eg. `5-start` to `5` - if (isNaN(n)) throw new Error(`invalid version: ${input}`) - return [n] - } else { - throw new Error(`invalid version: ${input}`) - } - }) - this.raw = raw - if (pretty_is_raw) this.pretty = raw - } else if (input instanceof Range || input instanceof SemVer) { - const v = input instanceof Range ? input.single() : input - if (!v) throw new Error(`range represents more than a single version: ${input}`) - this.components = v.components - this.raw = v.raw - this.pretty = v.pretty - } else { - this.components = input - this.raw = input.join('.') - } - - this.major = this.components[0] - this.minor = this.components[1] ?? 0 - this.patch = this.components[2] ?? 0 - - function char_to_num(c: string) { - return c.charCodeAt(0) - 'a'.charCodeAt(0) + 1 - } - } - - toString(): string { - return this.pretty ?? - (this.components.length <= 3 - ? `${this.major}.${this.minor}.${this.patch}` - : this.components.join('.')) - } - - eq(that: SemVer): boolean { - return this.compare(that) == 0 - } - - neq(that: SemVer): boolean { - return this.compare(that) != 0 - } - - gt(that: SemVer): boolean { - return this.compare(that) > 0 - } - - lt(that: SemVer): boolean { - return this.compare(that) < 0 - } - - compare(that: SemVer): number { - return _compare(this, that) - } - - [Symbol.for("Deno.customInspect")]() { - return this.toString() - } -} - -/// the same as the constructor but swallows the error returning undefined instead -/// also slightly more tolerant parsing -export function parse(input: string) { - try { - return new SemVer(input) - } catch { - return undefined - } -} - -/// we don’t support as much as node-semver but we refuse to do so because it is badness -export class Range { - // contract [0, 1] where 0 != 1 and 0 < 1 - readonly set: ([SemVer, SemVer] | SemVer)[] | '*' - - constructor(input: string | ([SemVer, SemVer] | SemVer)[]) { - if (input === "*") { - this.set = '*' - } else if (!isString(input)) { - this.set = input - } else { - input = input.trim() - - const err = () => new Error(`invalid semver range: ${input}`) - - this.set = input.split(/(?:,|\s*\|\|\s*)/).map(input => { - let match = input.match(/^>=((\d+\.)*\d+)\s*(<((\d+\.)*\d+))?$/) - if (match) { - const v1 = new SemVer(match[1]) - const v2 = match[3] ? new SemVer(match[4])! : new SemVer([Infinity, Infinity, Infinity]) - return [v1, v2] - } else if ((match = input.match(/^([~=<^])(.+)$/))) { - let v1: SemVer | undefined, v2: SemVer | undefined - switch (match[1]) { - // deno-lint-ignore no-case-declarations - case "^": - v1 = new SemVer(match[2]) - const parts = [] - for (let i = 0; i < v1.components.length; i++) { - if (v1.components[i] === 0 && i < v1.components.length - 1) { - parts.push(0) - } else { - parts.push(v1.components[i] + 1) - break - } - } - v2 = new SemVer(parts) - return [v1, v2] - case "~": { - v1 = new SemVer(match[2]) - if (v1.components.length == 1) { - // yep this is the official policy - v2 = new SemVer([v1.major + 1]) - } else { - v2 = new SemVer([v1.major, v1.minor + 1]) - } - } return [v1, v2] - case "<": - v1 = new SemVer([0]) - v2 = new SemVer(match[2]) - return [v1, v2] - case "=": - return new SemVer(match[2]) - } - } - throw err() - }) - - if (this.set.length == 0) { - throw err() - } - - for (const i of this.set) { - if (isArray(i) && !i[0].lt(i[1])) throw err() - } - } - } - - toString(): string { - if (this.set === '*') { - return '*' - } else { - return this.set.map(v => { - if (!isArray(v)) return `=${v.toString()}` - const [v1, v2] = v - if (v1.major > 0 && v2.major == v1.major + 1 && v2.minor == 0 && v2.patch == 0) { - const v = chomp(v1) - return `^${v}` - } else if (v2.major == v1.major && v2.minor == v1.minor + 1 && v2.patch == 0) { - const v = chomp(v1) - return `~${v}` - } else if (v2.major == Infinity) { - const v = chomp(v1) - return `>=${v}` - } else { - return `>=${chomp(v1)}<${chomp(v2)}` - } - }).join(",") - } - } - - // eq(that: Range): boolean { - // if (this.set.length !== that.set.length) return false - // for (let i = 0; i < this.set.length; i++) { - // const [a,b] = [this.set[i], that.set[i]] - // if (typeof a !== 'string' && typeof b !== 'string') { - // if (a[0].neq(b[0])) return false - // if (a[1].neq(b[1])) return false - // } else if (a != b) { - // return false - // } - // } - // return true - // } - - /// tolerant to stuff in the wild that hasn’t semver specifiers - static parse(input: string): Range | undefined { - try { - return new Range(input) - } catch { - if (!/^(\d+\.)*\d+$/.test(input)) return - - // AFAICT this is what people expect - // verified via https://jubianchi.github.io/semver-check/ - - const parts = input.split('.') - if (parts.length < 3) { - return new Range(`^${input}`) - } else { - return new Range(`~${input}`) - } - } - } - - satisfies(version: SemVer): boolean { - if (this.set === '*') { - return true - } else { - return this.set.some(v => { - if (isArray(v)) { - const [v1, v2] = v - return version.compare(v1) >= 0 && version.compare(v2) < 0 - } else { - return version.eq(v) - } - }) - } - } - - max(versions: SemVer[]): SemVer | undefined { - return versions.filter(x => this.satisfies(x)).sort((a,b) => a.compare(b)).pop() - } - - single(): SemVer | undefined { - if (this.set === '*') return - if (this.set.length > 1) return - return isArray(this.set[0]) ? undefined : this.set[0] - } - - [Symbol.for("Deno.customInspect")]() { - return this.toString() - } -} - -function zip(a: T[], b: U[]) { - const N = Math.max(a.length, b.length) - const rv: [T | undefined, U | undefined][] = [] - for (let i = 0; i < N; ++i) { - rv.push([a[i], b[i]]) - } - return rv -} - -function _compare(a: SemVer, b: SemVer): number { - for (const [c,d] of zip(a.components, b.components)) { - if (c != d) return (c ?? 0) - (d ?? 0) - } - return 0 -} -export { _compare as compare } - - -export function intersect(a: Range, b: Range): Range { - if (b.set === '*') return a - if (a.set === '*') return b - - // calculate the intersection between two semver.Ranges - const set: ([SemVer, SemVer] | SemVer)[] = [] - - for (const aa of a.set) { - for (const bb of b.set) { - if (!isArray(aa) && !isArray(bb)) { - if (aa.eq(bb)) set.push(aa) - } else if (!isArray(aa)) { - const bbb = bb as [SemVer, SemVer] - if (aa.compare(bbb[0]) >= 0 && aa.lt(bbb[1])) set.push(aa) - } else if (!isArray(bb)) { - const aaa = aa as [SemVer, SemVer] - if (bb.compare(aaa[0]) >= 0 && bb.lt(aaa[1])) set.push(bb) - } else { - const a1 = aa[0] - const a2 = aa[1] - const b1 = bb[0] - const b2 = bb[1] - - if (a1.compare(b2) >= 0 || b1.compare(a2) >= 0) { - continue - } - - set.push([a1.compare(b1) > 0 ? a1 : b1, a2.compare(b2) < 0 ? a2 : b2]) - } - } - } - - if (set.length <= 0) throw new Error(`cannot intersect: ${a} && ${b}`) - - return new Range(set) -} - - -//FIXME yes yes this is not sufficient -export const regex = /\d+\.\d+\.\d+/ - -function chomp(v: SemVer) { - return v.toString().replace(/(\.0)+$/g, '') || '0' -} diff --git a/src/vendor/Path.ts b/src/vendor/Path.ts deleted file mode 100644 index 6d7dcde23..000000000 --- a/src/vendor/Path.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { parse as parseYaml } from "deno/encoding/yaml.ts" -import * as sys from "deno/path/mod.ts" -import * as fs from "deno/fs/mod.ts" -import { PlainObject } from "is_what" -import { readLines } from "deno/io/read_lines.ts" -import "utils" //FIXME for console.verbose - -// based on https://github.com/mxcl/Path.swift - -// everything is Sync because TypeScript will unfortunately not -// cascade `await`, meaning our chainable syntax would become: -// -// await (await foo).bar -// -// however we use async versions for “terminators”, eg. `ls()` - -export default class Path { - /// the normalized string representation of the underlying filesystem path - readonly string: string - - /// the filesystem root - static root = new Path("/") - - static cwd(): Path { - return new Path(Deno.cwd()) - } - - static home(): Path { - return new Path((() => { - switch (Deno.build.os) { - case "windows": - return Deno.env.get("USERPROFILE")! - default: - return Deno.env.get("HOME")! - } - })()) - } - - /// normalizes the path - /// throws if not an absolute path - constructor(input: string | Path) { - if (input instanceof Path) { - this.string = input.string - } else if (!input || input[0] != '/') { - throw new Error(`invalid absolute path: ${input}`) - } else { - this.string = sys.normalize(input) - } - } - - /// returns Path | undefined rather than throwing error if Path is not absolute - static abs(input: string | Path) { - try { - return new Path(input) - } catch { - return - } - } - - /** - If the path represents an actual entry that is a symlink, returns the symlink’s - absolute destination. - - - Important: This is not exhaustive, the resulting path may still contain a symlink. - - Important: The path will only be different if the last path component is a symlink, any symlinks in prior components are not resolved. - - Note: If file exists but isn’t a symlink, returns `self`. - - Note: If symlink destination does not exist, is **not** an error. - */ - readlink(): Path { - try { - const output = Deno.readLinkSync(this.string) - return this.parent().join(output) - } catch (err) { - const code = err.code - if (err instanceof TypeError) { - switch (code) { - case 'EINVAL': - return this // is file - case 'ENOENT': - throw err // there is no symlink at this path - } - } - throw err - } - } - /** - Returns the parent directory for this path. - Path is not aware of the nature of the underlying file, but this is - irrlevant since the operation is the same irrespective of this fact. - - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`. - */ - parent(): Path { - return new Path(sys.dirname(this.string)) - } - - /// returns normalized absolute path string - toString(): string { - return this.string - } - - /// joins this path with the provided component and normalizes it - /// if you provide an absolute path that path is returned - /// rationale: usually if you are trying to join an absolute path it is a bug in your code - /// TODO should warn tho - join(...components: string[]): Path { - const joined = components.filter(x => x).join("/") - if (joined[0] == '/') { - return new Path(joined) - } else if (joined) { - return new Path(`${this.string}/${joined}`) - } else { - return this - } - } - - /// Returns true if the path represents an actual filesystem entry that is *not* a directory. - /// NOTE we use `stat`, so if the file is a symlink it is resolved, usually this is what you want - isFile(): Path | undefined { - try { - return Deno.statSync(this.string).isFile ? this : undefined - } catch { - return //FIXME - // if (err instanceof Deno.errors.NotFound == false) { - // throw err - // } - } - } - - isSymlink(): Path | undefined { - try { - return Deno.lstatSync(this.string).isSymlink ? this : undefined - } catch { - return //FIXME - // if (err instanceof Deno.errors.NotFound) { - // return false - // } else { - // throw err - // } - } - } - - isExecutableFile(): Path | undefined { - try { - if (!this.isFile()) return - const info = Deno.statSync(this.string) - if (!info.mode) throw new Error() - const is_exe = (info.mode & 0o111) > 0 - if (is_exe) return this - } catch { - return //FIXME catch specific errors - } - } - - isReadableFile(): Path | undefined { - return this.isFile() /*FIXME*/ ? this : undefined - } - - exists(): Path | undefined { - //FIXME can be more efficient - try { - Deno.statSync(this.string) - return this - } catch { - return //FIXME - // if (err instanceof Deno.errors.NotFound) { - // return false - // } else { - // throw err - // } - } - } - - /// Returns true if the path represents an actual directory. - /// NOTE we use `stat`, so if the file is a symlink it is resolved, usually this is what you want - isDirectory(): Path | undefined { - try { - return Deno.statSync(this.string).isDirectory ? this : undefined - } catch { - return //FIXME catch specific errorrs - } - } - - async *ls(): AsyncIterable<[Path, Deno.DirEntry]> { - for await (const entry of Deno.readDir(this.string)) { - yield [this.join(entry.name), entry] - } - } - - //FIXME probs can be infinite - async *walk(): AsyncIterable<[Path, Deno.DirEntry]> { - const stack: Path[] = [this] - while (stack.length > 0) { - const dir = stack.pop()! - for await (const entry of Deno.readDir(dir.string)) { - const path = dir.join(entry.name) - yield [path, entry] - if (entry.isDirectory) { - stack.push(path) - } - } - } - } - - components(): string[] { - return this.string.split('/') - } - - static mktemp(opts?: { prefix?: string, dir?: Path }): Path { - const {prefix, dir} = opts ?? {} - const rv = Deno.makeTempDirSync({prefix, dir: dir?.mkpath().string}) - return new Path(rv) - } - - split(): [Path, string] { - const d = this.parent() - const b = this.basename() - return [d, b] - } - - /// the file extension with the leading period - extname(): string { - const match = this.string.match(/\.tar\.\w+$/) - if (match) { - return match[0] - } else { - return sys.extname(this.string) - } - } - - basename(): string { - return sys.basename(this.string) - } - - /** - Moves a file. - - Path.root.join("bar").mv({to: Path.home.join("foo")}) - // => Path("/Users/mxcl/foo") - - - Parameter to: Destination filename. - - Parameter into: Destination directory (you get `into/${this.basename()`) - - Parameter overwrite: If true overwrites any entry that already exists at the destination. - - Returns: `to` to allow chaining. - - Note: `force` will still throw if `to` is a directory. - - Note: Throws if `overwrite` is `false` yet `to` is *already* identical to - `self` because even though *our policy* is to noop if the desired - end result preexists, checking for this condition is too expensive a - trade-off. - */ - mv({force, ...opts}: {to: Path, force?: boolean} | {into: Path, force?: boolean}): Path { - if ("to" in opts) { - fs.moveSync(this.string, opts.to.string, { overwrite: force }) - return opts.to - } else { - const dst = opts.into.join(this.basename()) - fs.moveSync(this.string, dst.string, { overwrite: force }) - return dst - } - } - - ///FIXME operates in ”force” mode - cp({into}: {into: Path}): Path { - const dst = into.join(this.basename()) - Deno.copyFileSync(this.string, dst.string) - return dst - } - - rm({recursive} = {recursive: false}) { - if (this.exists()) { - Deno.removeSync(this.string, { recursive }) - } - } - - mkdir(): Path { - if (!this.isDirectory()) { - Deno.mkdirSync(this.string, { recursive: true }) - } - return this - } - - isEmpty(): Path | undefined { - for (const _ of Deno.readDirSync(this.string)) { - return - } - return this - } - - mkpath(): Path { - if (!(this.isSymlink() && this.isDirectory())) { - // if it's a symlink and a directory ensureDirSync fails - fs.ensureDirSync(this.string) - } - return this - } - - mkparent(): Path { - this.parent().mkpath() - return this - } - - eq(that: Path): boolean { - return this.string == that.string - } - - neq(that: Path): boolean { - return this.string != that.string - } - - /// creates a symlink of `from` aliased as a relative path `to`, relative to directory `this` - //TODO deprecate - async symlink({from, to, force}: { from: Path, to: Path, force?: boolean }): Promise { - // NOTE that we use Deno.run as there is no other way in Deno currently to create - // relative symlinks. Also Deno.symlink requires full write permissions for no reason that I understand. - - const src = from.relative({ to: this }) - const dst = to.relative({ to: this }) - - let opts = "-s" - if (force) opts += "fn" - const status = await new Deno.Command("/bin/ln", { - args: [opts, src, dst], - cwd: this.string - }).spawn().status - - if (!status.success) throw new Error(`failed: cd ${this} && ln -sf ${src} ${dst}`) - - return to - } - - /// creates symlink `to` pointing at `this` - ln(_: 's', {to}: { to: Path }): Path { - Deno.symlinkSync(this.string, to.string) - return to - } - - read(): Promise { - return Deno.readTextFile(this.string) - } - - async *readLines(): AsyncIterableIterator { - const fd = Deno.openSync(this.string) - try { - for await (const line of readLines(fd)) - yield line - } - finally { - fd.close() - } - } - - //FIXME like, we don’t want a hard dependency in the published library - //TODO would be nice to validate the output against a type - //TODO shouldn't be part of this module since we want to publish it - async readYAML(): Promise { - try { - const txt = await this.read() - return parseYaml(txt) - } catch (err) { - console.debug("error:", this) //because deno errors are shit - throw err - } - } - - readJSON(): Promise { - return this.read().then(x => JSON.parse(x)) - } - - write({ force, ...content }: ({text: string} | {json: PlainObject, space?: number}) & {force?: boolean}): Path { - if (this.exists()) { - if (!force) throw new Error(`file-exists:${this}`) - this.rm() - } - if ("text" in content) { - Deno.writeTextFileSync(this.string, content.text) - } else { - const text = JSON.stringify(content.json, null, content.space) - Deno.writeTextFileSync(this.string, text) - } - return this - } - - touch(): Path { - //FIXME work more as expected - return this.write({force: true, text: ""}) - } - - in(that: Path) { - //FIXME a bit naive - return this.string.startsWith(that.string) - } - - chmod(mode: number): Path { - Deno.chmodSync(this.string, mode) - return this - } - - compact(): Path | undefined { - if (this.exists()) return this - } - - relative({ to: base }: { to: Path }): string { - const pathComps = ['/'].concat(this.string.split("/").filter(x=>x)) - const baseComps = ['/'].concat(base.string.split("/").filter(x=>x)) - - if (this.string.startsWith(base.string)) { - return pathComps.slice(baseComps.length).join("/") - } else { - const newPathComps = [...pathComps] - const newBaseComps = [...baseComps] - - while (newPathComps[0] == newBaseComps[0]) { - newPathComps.shift() - newBaseComps.shift() - } - - const relComps = Array.from({ length: newBaseComps.length } , () => "..") - relComps.push(...newPathComps) - return relComps.join("/") - } - } - - realpath(): Path { - return new Path(Deno.realPathSync(this.string)) - } - - prettyString(): string { - return this.string.replace(new RegExp(`^${Path.home()}`), '~') - } - - // if we’re inside the CWD we print that - prettyLocalString(): string { - const cwd = Path.cwd() - return this.in(cwd) ? `./${this.relative({ to: cwd })}` : this.prettyString() - } - - [Symbol.for("Deno.customInspect")]() { - return this.prettyString() - } -} - -declare global { - interface URL { - path(): Path - } -} - -URL.prototype.path = function() { return new Path(this.pathname) } diff --git a/src/vendor/PathUtils.ts b/src/vendor/PathUtils.ts deleted file mode 100644 index 7adf859a2..000000000 --- a/src/vendor/PathUtils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Path from "path" - -export default { - envPath, - addPath, - rmPath, - findBinary, -} - -function envPath(pathVar: string | undefined = undefined): Path[] { - const pathVar_ = pathVar || Deno.env.get("PATH")! - return pathVar_.split(":").map(x => new Path(x)) -} - -function addPath(path: Path | string, pathVar: string | undefined = undefined): Path[] { - const pathVar_ = envPath(pathVar) - const path_ = new Path(path) - if (!pathVar_.includes(path_)) { - pathVar_.push(path_) - Deno.env.set("PATH", pathVar_.join(":")) - } - return pathVar_ -} - -function rmPath(path: Path | string, pathVar: string | undefined = undefined): Path[] { - const path_ = new Path(path) - const pathVar_ = envPath(pathVar).filter(x => x.string != path_.string) - Deno.env.set("PATH", pathVar_.join(":")) - return pathVar_ -} - -function findBinary(name: string, pathVar: string | Path[] | undefined = undefined): Path | undefined { - let pathVar_: Path[] - if (pathVar instanceof Array) { - pathVar_ = pathVar - } else { - pathVar_ = envPath(pathVar) - } - return pathVar_.find(x => x.join(name).isExecutableFile())?.join(name) -} diff --git a/src/vendor/README.md b/src/vendor/README.md deleted file mode 100644 index 322b3fd84..000000000 --- a/src/vendor/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Path.ts - -Note not actually vendored yet as we haven’t yet published this package. - -Goals: - -1. Chainable -2. Robust -3. noop if the result of a requested operation has already been done. -4. Delightful -5. Conforms to principle of least surprise -6. Consistent - -Based on [mxcl/Path.swift](https://github.com/mxcl/Path.swift) - - -# PathUtils.ts - -Companion library to `Path.ts`. Contains common path-oriented -operations that don't specifically belong on a `Path` object. diff --git a/tests/functional/args.test.ts b/tests/functional/args.test.ts index 385359bec..1b2b5e525 100644 --- a/tests/functional/args.test.ts +++ b/tests/functional/args.test.ts @@ -1,8 +1,10 @@ +import { ConfigDefault } from "../../src/hooks/useConfig.ts" +import { _internals } from "../../src/hooks/usePrint.ts" import { assertEquals } from "deno/testing/asserts.ts" -import { wut } from "../../src/app.main.ts" import { parseArgs } from "../../src/args.ts" -import { EnvKeys } from "../../src/hooks/useShellEnv.ts" -import { collectEnv, init } from "../../src/init.ts" +import { wut } from "../../src/app.main.ts" +import { hooks } from "tea" +const { useConfig } = hooks Deno.test("parse args", async test => { await test.step("verbosity - int", () => { @@ -78,7 +80,7 @@ Deno.test("parse args", async test => { Deno.test("wut args", async test => { const runTest = (a: string[]) => { const [args, flags] = parseArgs(a, "/tea") - init(flags) + useConfig(ConfigDefault(flags)) return wut(args) } @@ -103,30 +105,30 @@ Deno.test("wut args", async test => { }) }) -Deno.test("reads env", () => { - const keys = ["CI", "CLICOLOR", "CLICOLOR_FORCE", "DEBUG", "GITHUB_ACTIONS", "GITHUB_TOKEN", - "NO_COLOR", "PATH", "RUNNER_DEBUG", "SHELL", "SRCROOT", "TEA_DIR", "TEA_FILES", - "TEA_FORK_BOMB_PROTECTOR", "TEA_PANTRY_PATH", "TEA_PKGS", "TEA_PREFIX", "TEA_REWIND", - "VERBOSE", "VERSION"] - - const oldEnv = keys.reduce((env, key) => { - return { ...env, [key]: Deno.env.get(key) } - }, {} as Record) - - try { - keys.forEach(k => Deno.env.set(k, `${k}-TEST`)) - const env = collectEnv() as Record - for (const k of keys) { - assertEquals(env[k], `${k}-TEST`) - } - } finally { - // restore old env because it can affect the VSCode test runner and other tests - for (const [k, v] of Object.entries(oldEnv)) { - if (v) { - Deno.env.set(k, v) - } else { - Deno.env.delete(k) - } - } - } -}) +// Deno.test("reads env", () => { +// const keys = ["CI", "CLICOLOR", "CLICOLOR_FORCE", "DEBUG", "GITHUB_ACTIONS", "GITHUB_TOKEN", +// "NO_COLOR", "PATH", "RUNNER_DEBUG", "SHELL", "SRCROOT", "TEA_DIR", "TEA_FILES", +// "TEA_FORK_BOMB_PROTECTOR", "TEA_PANTRY_PATH", "TEA_PKGS", "TEA_PREFIX", "TEA_REWIND", +// "VERBOSE", "VERSION"] + +// const oldEnv = keys.reduce((env, key) => { +// return { ...env, [key]: Deno.env.get(key) } +// }, {} as Record) + +// try { +// keys.forEach(k => Deno.env.set(k, `${k}-TEST`)) +// const env = collectEnv() as Record +// for (const k of keys) { +// assertEquals(env[k], `${k}-TEST`) +// } +// } finally { +// // restore old env because it can affect the VSCode test runner and other tests +// for (const [k, v] of Object.entries(oldEnv)) { +// if (v) { +// Deno.env.set(k, v) +// } else { +// Deno.env.delete(k) +// } +// } +// } +// }) diff --git a/tests/functional/devenv.test.ts b/tests/functional/devenv.test.ts index 7331032f3..c22563eda 100644 --- a/tests/functional/devenv.test.ts +++ b/tests/functional/devenv.test.ts @@ -1,8 +1,9 @@ -import { createTestHarness } from "./testUtils.ts" import { assert, assertEquals } from "deno/testing/asserts.ts" -import { flatmap } from "utils" +import { createTestHarness } from "./testUtils.ts" +import { Path, utils } from "tea" +const { flatmap } = utils -const fixturesDir = new URL(import.meta.url).path().parent().parent().join('fixtures') +const fixturesDir = new Path(new URL(import.meta.url).pathname).parent().parent().join('fixtures') Deno.test("dev env interactions with HOME", { sanitizeResources: false, sanitizeOps: false }, async test => { const tests = [ @@ -49,19 +50,19 @@ Deno.test("dev env interactions with HOME", { sanitizeResources: false, sanitize Deno.test("should enter dev env", { sanitizeResources: false, sanitizeOps: false }, async test => { // each of the files in this list must have a zlib.net^1.2 dependency and a FOO=BAR env - const envFiles = ["tea.yaml", "deno.json", "deno.jsonc", "package.json", "cargo.toml", - "Gemfile", "pyproject.toml", "go.mod", "requirements.txt"] + const envFiles = ["tea.yaml"]//, "deno.json", "deno.jsonc", "package.json", "cargo.toml", + // "Gemfile", "pyproject.toml", "go.mod", "requirements.txt"] for (const shell of ["/bin/bash", "/bin/fish", "/bin/elvish"]) { for (const envFile of envFiles) { - await test.step(`${shell}-${envFile}`, async () => { - const {run, teaDir } = await createTestHarness() + await test.step(`${shell} & ${envFile}`, async () => { + const { run, teaDir } = await createTestHarness() fixturesDir.join(envFile).cp({into: teaDir}) const TEA_REWIND = JSON.stringify({revert: {VAL: "REVERTED"}, unset: ["BAZ"]}) - const config = { env: { SHELL: shell, TEA_REWIND } } + const config = { env: { SHELL: shell, TEA_REWIND, obj: {} } } const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], config) const envVar = (key: string) => getEnvVar(shell, stdout, key) @@ -69,7 +70,7 @@ Deno.test("should enter dev env", { sanitizeResources: false, sanitizeOps: false assert(getTeaPackages(shell, stdout).includes("zlib.net^1.2"), "should include zlib dep") - assertEquals(envVar("FOO"), "BAR", "should set virtual env var") + assertEquals(envVar("FOO"), "BAR", "should set virtual env var FOO") assertEquals(envVar("VAL"), "REVERTED", "should revert previous env") assert(isUnset("BAZ"), "should unset previous env") assertEquals(envVar("SRCROOT"), teaDir.string, "should set virtual env SRCROOT") @@ -90,7 +91,7 @@ Deno.test("should leave dev env", { sanitizeResources: false, sanitizeOps: false const TEA_REWIND = JSON.stringify({revert: {VAL: "REVERTED"}, unset: ["BAZ"]}) - const config = { env: { SHELL: shell, TEA_REWIND } } + const config = { env: { SHELL: shell, TEA_REWIND, obj: {} } } const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], config) const envVar = (key: string) => getEnvVar(shell, stdout, key) @@ -117,7 +118,7 @@ Deno.test("should provide packages in dev env", { sanitizeResources: false, sani const {run, teaDir } = await createTestHarness() fixturesDir.join(file).cp({into: teaDir}) - const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL } }) + const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, obj: {} } }) assert(getTeaPackages(SHELL, stdout).includes(pkg), "should include nodejs dep") }) @@ -136,7 +137,7 @@ Deno.test("should provide python in dev env", { sanitizeResources: false, saniti const { run, teaDir } = await createTestHarness() fixturesDir.join(file).cp({ into: teaDir }) - const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL } }) + const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, obj: {} } }) const output = getTeaPackages(SHELL, stdout) assert(output.includes(pkg), "should include python dep") @@ -152,7 +153,7 @@ Deno.test("tolerant .node-version parsing", { sanitizeResources: false, sanitize const {run, teaDir } = await createTestHarness() teaDir.join(".node-version").write({ text: `\n\n\n${spec}\n` }) - const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL } }) + const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, obj: {} } }) const pkg = `nodejs.org${interpretation}` assert(getTeaPackages(SHELL, stdout).includes(pkg), "should include nodejs dep") @@ -172,7 +173,7 @@ Deno.test("should provide ruby in dev env", { sanitizeResources: false, sanitize const { run, teaDir } = await createTestHarness() fixturesDir.join(file).cp({ into: teaDir }) - const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL } }) + const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, obj: {} } }) const output = getTeaPackages(SHELL, stdout) assert(output.includes(pkg), "should include ruby dep") diff --git a/tests/functional/exec.test.ts b/tests/functional/exec.test.ts index 920e06258..7f2397768 100644 --- a/tests/functional/exec.test.ts +++ b/tests/functional/exec.test.ts @@ -1,10 +1,10 @@ import { assertEquals, assertRejects } from "deno/testing/asserts.ts" import { spy, stub, returnsNext } from "deno/testing/mock.ts" import { createTestHarness, newMockProcess } from "./testUtils.ts" -import { TeaError } from "utils" +import { TeaError } from "tea" Deno.test("exec", { sanitizeResources: false, sanitizeOps: false }, async () => { - const {run, useRunInternals } = await createTestHarness() + const { run, useRunInternals } = await createTestHarness() const useRunSpy = spy(useRunInternals, "nativeRun") try { @@ -79,6 +79,6 @@ Deno.test("exec run errors", { sanitizeResources: false, sanitizeOps: false }, a Deno.test("exec forkbomb protector", { sanitizeResources: false, sanitizeOps: false }, async () => { const { run } = await createTestHarness() await assertRejects( - () => run(["sh", "-c", "echo $TEA_PREFIX"], { env: {TEA_FORK_BOMB_PROTECTOR: "21" }}), + () => run(["sh", "-c", "echo $TEA_PREFIX"], { env: {TEA_FORK_BOMB_PROTECTOR: "21", obj: {} }}), "FORK BOMB KILL SWITCH ACTIVATED") }) diff --git a/tests/functional/magic.test.ts b/tests/functional/magic.test.ts index 0a3d8e37e..011d9d119 100644 --- a/tests/functional/magic.test.ts +++ b/tests/functional/magic.test.ts @@ -4,7 +4,7 @@ import { createTestHarness } from "./testUtils.ts" Deno.test("magic", { sanitizeResources: false, sanitizeOps: false }, async test => { await test.step("/bin/bash", async () => { const { run, teaDir } = await createTestHarness({sync: false}) - const { stdout } =await run(["--magic"], { env: { SHELL: "/bin/bash" }}) + const { stdout } =await run(["--magic"], { env: { SHELL: "/bin/bash", obj: {} }}) const expected = `source /dev/stdin <<<"$("${teaDir.parent()}"/tea +tea.xyz/magic -Esk --chaste env)"` assert(stdout[0].includes(expected)) @@ -12,21 +12,21 @@ Deno.test("magic", { sanitizeResources: false, sanitizeOps: false }, async test await test.step("/bin/zsh", async () => { const { run, teaDir } = await createTestHarness({sync: false}) - const { stdout } = await run(["--magic"], { env: { SHELL: "/bin/zsh" }}) + const { stdout } = await run(["--magic"], { env: { SHELL: "/bin/zsh", obj: {} }}) const expected = `source <("${teaDir.parent()}"/tea +tea.xyz/magic -Esk --chaste env)` assert(stdout[0].includes(expected)) }) await test.step("/bin/fish", async () => { const { run, teaDir } = await createTestHarness({sync: false}) - const { stdout } = await run(["--magic"], { env: { SHELL: "/bin/fish" }}) + const { stdout } = await run(["--magic"], { env: { SHELL: "/bin/fish", obj: {} }}) const expected = `"${teaDir.parent()}"/tea --env --keep-going --silent --dry-run=w/trace | source` assert(stdout[0].includes(expected)) }) await test.step("/bin/elvish", async () => { const { run, teaDir } = await createTestHarness({sync: false}) - const { stdout } = await run(["--magic"], { env: { SHELL: "/bin/elvish" }}) + const { stdout } = await run(["--magic"], { env: { SHELL: "/bin/elvish", obj: {} }}) const expected = `eval ("${teaDir.parent()}"/tea +tea.xyz/magic -Esk --chaste env | slurp)` assert(stdout[0].includes(expected)) }) diff --git a/tests/functional/provides.test.ts b/tests/functional/provides.test.ts index 044d34517..43648790e 100644 --- a/tests/functional/provides.test.ts +++ b/tests/functional/provides.test.ts @@ -1,5 +1,5 @@ +import { ExitError } from "../../src/hooks/useErrorHandler.ts" import { assertRejects } from "deno/testing/asserts.ts" -import { ExitError } from "types" import { createTestHarness } from "./testUtils.ts" Deno.test("provides", { sanitizeResources: false, sanitizeOps: false }, async test => { @@ -16,7 +16,7 @@ Deno.test("provides", { sanitizeResources: false, sanitizeOps: false }, async te await test.step("provides version in dev env", async () => { const { run } = await createTestHarness() await assertRejects(() => { - return run(["--provides", "node"], { env: { TEA_PKGS: "nodejs.org=18.14.2"} }) + return run(["--provides", "node"], { env: { TEA_PKGS: "nodejs.org=18.14.2", obj: {} } }) }, ExitError, "exiting with code: 0") }) diff --git a/tests/functional/repl.test.ts b/tests/functional/repl.test.ts index 5d31a022d..b5f4b8eac 100644 --- a/tests/functional/repl.test.ts +++ b/tests/functional/repl.test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertRejects } from "deno/testing/asserts.ts" -import { stub, returnsNext } from "deno/testing/mock.ts" -import { ExitError } from "types" import { createTestHarness, newMockProcess } from "./testUtils.ts" +import { stub, returnsNext } from "deno/testing/mock.ts" +import { ExitError } from "../../src/hooks/useErrorHandler.ts" Deno.test("should enter repl - sh", { sanitizeResources: false, sanitizeOps: false }, async test => { const tests = [ @@ -41,7 +41,7 @@ Deno.test("should enter repl - sh", { sanitizeResources: false, sanitizeOps: fal const useRunStub = stub(useRunInternals, "nativeRun", returnsNext([newMockProcess()])) try { - await run(["sh"], { env: { SHELL: shell } }) + await run(["sh"], { env: { SHELL: shell, obj: {} } }) } finally { useRunStub.restore() } diff --git a/tests/functional/script.test.ts b/tests/functional/script.test.ts index e9b6ca34c..ce3bf86e0 100644 --- a/tests/functional/script.test.ts +++ b/tests/functional/script.test.ts @@ -1,8 +1,9 @@ +import { assert, assertEquals } from "deno/testing/asserts.ts" import { createTestHarness } from "./testUtils.ts" import { spy } from "deno/testing/mock.ts" -import { assert, assertEquals } from "deno/testing/asserts.ts" +import { Path } from "tea" -const fixturesDir = new URL(import.meta.url).path().parent().parent().join('fixtures') +const fixturesDir = new Path(new URL(import.meta.url).pathname).parent().parent().join('fixtures') Deno.test("run a python script", { sanitizeResources: false, sanitizeOps: false }, async () => { const { run, teaDir, useRunInternals } = await createTestHarness() diff --git a/tests/functional/suggestions.test.ts b/tests/functional/suggestions.test.ts index 9aca557fc..5f8138da1 100644 --- a/tests/functional/suggestions.test.ts +++ b/tests/functional/suggestions.test.ts @@ -1,8 +1,7 @@ +import { suggestions } from "../../src/hooks/useErrorHandler.ts" import { assert, assertEquals } from "deno/testing/asserts.ts" import { createTestHarness } from "./testUtils.ts" -import { suggestions } from "hooks/useErrorHandler.ts" -import { TeaError } from "utils" -import SemVer from "semver" +import { TeaError, SemVer } from "tea" Deno.test("suggestions", { sanitizeResources: false, sanitizeOps: false }, async test => { // suggestions need a sync to occur first diff --git a/tests/functional/sync.test.ts b/tests/functional/sync.test.ts index e3df0ecc3..8fec3dee1 100644 --- a/tests/functional/sync.test.ts +++ b/tests/functional/sync.test.ts @@ -1,7 +1,6 @@ import { assert } from "deno/testing/asserts.ts" import { createTestHarness } from "./testUtils.ts" -import SemVer from "../../src/utils/semver.ts" -import Path from "path" +import { SemVer } from "tea" Deno.test("update package", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness() @@ -24,7 +23,7 @@ Deno.test("sync and update without git on path", { sanitizeResources: false, san const {run, TEA_PREFIX } = await createTestHarness({sync: false}) // empty path so tea can't find git - await run(["-S", "+zlib.net"], { env: { PATH: "" }}) + await run(["-S", "+zlib.net"], { env: { PATH: "", obj: {} }}) const expected = TEA_PREFIX.join("zlib.net") assert(expected.exists(), "zlib.net should exist") @@ -32,14 +31,14 @@ Deno.test("sync and update without git on path", { sanitizeResources: false, san // allows us to verify that the subsequent update works TEA_PREFIX.join("tea.xyz/var/pantry/projects/zlib.net").rm({ recursive: true }) - await run(["-S", "+zlib.net"], { env: { PATH: "" }}) + await run(["-S", "+zlib.net"], { env: { PATH: "", obj: {} }}) }) Deno.test("sync without git then update with git", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness({sync: false}) // empty path so tea can't find git - await run(["-S", "+zlib.net"], { env: { PATH: "" }}) + await run(["-S", "+zlib.net"], { env: { PATH: "", obj: {} }}) const expected = TEA_PREFIX.join("zlib.net") assert(expected.exists(), "zlib.net should exist") @@ -61,5 +60,5 @@ Deno.test("sync with git then update without", { sanitizeResources: false, sanit // allows us to verify that the subsequent update works TEA_PREFIX.join("tea.xyz/var/pantry/projects/zlib.net").rm({ recursive: true }) - await run(["-S", "+zlib.net"], { env: { PATH: "" }}) + await run(["-S", "+zlib.net"], { env: { PATH: "", obj: {} }}) }) diff --git a/tests/functional/testUtils.ts b/tests/functional/testUtils.ts index 3c56a90e9..9667cc652 100644 --- a/tests/functional/testUtils.ts +++ b/tests/functional/testUtils.ts @@ -1,12 +1,11 @@ -import Path from "path" -import { run } from "../../src/app.main.ts" -import { parseArgs } from "../../src/args.ts" -import { Config } from "../../src/hooks/useConfig.ts" -import { init } from "../../src/init.ts"; -import { _internals as usePrintInternals } from "hooks/usePrint.ts" -import { _internals as useConfigInternals } from "hooks/useConfig.ts" -import { _internals as useRunInternals } from "hooks/useRun.ts" +import { Config, ConfigDefault } from "../../src/hooks/useConfig.ts" +import { _internals as usePrintInternals } from "../../src/hooks/usePrint.ts" +import { _internals as useRunInternals } from "../../src/hooks/useRun.ts" import { spy } from "deno/testing/mock.ts" +import { parseArgs } from "../../src/args.ts" +import { run } from "../../src/app.main.ts" +import { Path, hooks } from "tea" +const { useConfig } = hooks export interface TestConfig { // run tea sync during test setup. Default: true @@ -20,14 +19,17 @@ export const createTestHarness = async (config?: TestConfig) => { const dir = config?.dir ?? "tea" const tmpDir = new Path(await Deno.makeTempDir({ prefix: "tea-" })).realpath() - const teaDir = tmpDir.join(dir).mkdir() + const teaDir = tmpDir.join(dir).mkdir('p') const TEA_PREFIX = tmpDir.join('opt').mkdir() if (sync) { const [syncArgs, flags] = parseArgs(["--sync", "--silent"], teaDir.string) - init(flags) - updateConfig({ teaPrefix: new Path(TEA_PREFIX.string), env: { NO_COLOR: "1" } }) + const config = ConfigDefault(flags, teaDir.string, undefined, { NO_COLOR: '1' }) + useConfig({ + ...config, + prefix: TEA_PREFIX + }) await run(syncArgs) } @@ -37,16 +39,20 @@ export const createTestHarness = async (config?: TestConfig) => { const usePrintSpy = spy(usePrintInternals, "nativePrint") - useConfigInternals.getEnvAsObject = () => { - return (useConfigInternals.getConfig()?.env ?? {}) as {[index: string]: string} - } - try { const [appArgs, flags] = parseArgs(args, teaDir.string) - init(flags) - updateConfig({ execPath: teaDir, teaPrefix: new Path(TEA_PREFIX.string), ...configOverrides }) + + const config = ConfigDefault(flags, teaDir.string, undefined, { NO_COLOR: '1', PATH: "/usr/bin:/bin" }) + config.arg0 = teaDir + + useConfig({ + ...config, + prefix: TEA_PREFIX, + ...configOverrides, + }) await run(appArgs) + } finally { usePrintSpy.restore() Deno.chdir(cwd) @@ -66,20 +72,6 @@ export const createTestHarness = async (config?: TestConfig) => { } } -// updates the application config by only overriding the provided keys -function updateConfig(updated: Partial) { - const config = useConfigInternals.getConfig() - if (!config) { - throw new Error("test attempted to updated config that has not been applied") - } - useConfigInternals.setConfig({...config, ...updated, env: {...config.env, ...updated.env}}) -} - -// we need Deno.ChildProcress.status to be mutable -type Mutable = { - -readonly [Key in keyof Type]: Type[Key]; -} - // the Deno.Process object cannot be created externally with `new` so we'll just return a // ProcessLike object export function newMockProcess(status?: () => Promise): Deno.Command { diff --git a/tests/integration.suite.ts b/tests/integration.suite.ts index 571daad12..10564b4f8 100644 --- a/tests/integration.suite.ts +++ b/tests/integration.suite.ts @@ -1,6 +1,6 @@ -import { describe } from "deno/testing/bdd.ts" import { assert, assertEquals } from "deno/testing/asserts.ts" -import Path from "path" +import { describe } from "deno/testing/bdd.ts" +import { Path } from "tea" interface This { tea: Path @@ -30,7 +30,7 @@ const suite = describe({ name: "integration tests", async beforeEach(this: This) { const tmp = new Path(await Deno.makeTempDir({ prefix: "tea-" })) - const cwd = new URL(import.meta.url).path().parent().parent().string + const cwd = new Path(new URL(import.meta.url).pathname).parent().parent().string const TEA_PREFIX = existing_tea_prefix ?? tmp.join('opt').mkdir() const bin = tmp.join('bin').mkpath() diff --git a/tests/integration/magic.test.ts b/tests/integration/magic.test.ts index 85414e0ee..b99167dd9 100644 --- a/tests/integration/magic.test.ts +++ b/tests/integration/magic.test.ts @@ -1,8 +1,8 @@ +import { strip_ansi_escapes } from "../../src/hooks/useLogger.ts" import { assertEquals } from "deno/testing/asserts.ts" -import { strip_ansi_escapes } from "hooks/useLogger.ts" import suite from "../integration.suite.ts" import { it } from "deno/testing/bdd.ts" -import { undent } from "utils" +import undent from "outdent" it(suite, "tea --magic in a script. zsh", async function() { const script = this.sandbox.join("magic-zsh").write({ text: undent` diff --git a/tests/integration/package.yml.test.ts b/tests/integration/package.yml.test.ts index 40033daee..26b5e1a63 100644 --- a/tests/integration/package.yml.test.ts +++ b/tests/integration/package.yml.test.ts @@ -1,8 +1,8 @@ +import { assertEquals } from "deno/testing/asserts.ts" import suite from "../integration.suite.ts" import { it } from "deno/testing/bdd.ts" -import { assertEquals } from "deno/testing/asserts.ts" -import { undent } from "utils" -import Path from "path" +import undent from "outdent" +import { Path } from "tea" it(suite, "runtime.env tildes", async function() { const run = async (FOO: string) => { diff --git a/tests/integration/tea.ln.test.ts b/tests/integration/tea.ln.test.ts index d53b153dc..aa35be193 100644 --- a/tests/integration/tea.ln.test.ts +++ b/tests/integration/tea.ln.test.ts @@ -1,7 +1,7 @@ import { assert, assertEquals, assertMatch } from "deno/testing/asserts.ts" import suite from "../integration.suite.ts" import { it } from "deno/testing/bdd.ts" -import Path from "path" +import { Path } from "tea" async function run(cmd: string[], PATH: Path, TEA_PREFIX: Path) { const proc = Deno.run({ @@ -32,7 +32,7 @@ if (Deno.build.os != 'linux') { it(suite, `symlink: node${v}`, async function() { await this.run({args: ["--sync", "--silent"]}) - const node = this.tea.ln("s", {to: this.sandbox.join(`node${v}`)}) + const node = this.sandbox.join(`node${v}`).ln("s", {target: this.tea}) const out = await run([`node${v}`, "--eval", "console.log('hello')"], node.parent(), this.TEA_PREFIX) assertEquals(out, "hello\n") }) @@ -45,9 +45,9 @@ if (Deno.build.os != 'linux') { it(suite, `two level symlink`, async function() { await this.run({args: ["--sync", "--silent"]}) - const node = this.tea - .ln('s', {to: this.sandbox.join('node^18')}) - .ln('s', {to: this.sandbox.join('node')}) + const node18 = this.sandbox.join('node^18') + .ln("s", { target: this.tea }) + const node = this.sandbox.join('node').ln('s', { target: node18 }) const out = await run(['node', '--version'], node.parent(), this.TEA_PREFIX) assertMatch(out, /v18\.\d+\.\d+/, out) @@ -55,7 +55,7 @@ if (Deno.build.os != 'linux') { } it(suite, "symlinks to `tea` called `tea` act like tea", async function() { - const tea = this.tea.ln("s", {to: this.sandbox.join('tea')}) + const tea = this.sandbox.join('tea').ln("s", {target: this.tea }) const out = await run(['tea', '--version'], tea.parent(), Path.root) assertMatch(out, /tea \d+\.\d+\.\d+/) }) diff --git a/tests/integration/tea.scripts.test.ts b/tests/integration/tea.scripts.test.ts index 2dc5f8f23..5bba66090 100644 --- a/tests/integration/tea.scripts.test.ts +++ b/tests/integration/tea.scripts.test.ts @@ -1,7 +1,7 @@ import { assertEquals } from "deno/testing/asserts.ts" -import { undent } from "../../src/utils/index.ts" import suite from "../integration.suite.ts" import { it } from "deno/testing/bdd.ts" +import undent from "outdent" //TODO pick things macOS doesn’t have, eg. php //TODO don’t use deno *we use deno lol* so it will be available perhaps accidentally diff --git a/tests/unit/cache.test.ts b/tests/unit/cache.test.ts deleted file mode 100644 index b15b56285..000000000 --- a/tests/unit/cache.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { assert } from "deno/testing/asserts.ts" -import { useDownload } from "hooks" -import Path from "path" - -console.silence = async function(body: () => Promise) { - const originals = [console.log, console.info] - try { - console.log = () => {} - console.info = () => {} - return await body() - } finally { - console.log = originals[0] - console.info = originals[1] - } -} - -Deno.test("etag-mtime-check",async () => { - const tmpdir = new Path(await Deno.makeTempDir({ prefix: "tea" })) - try { - const src = new URL("https://dist.tea.xyz/ijg.org/versions.txt") - - await console.silence(async () => { - await useDownload().download({src}) - - const mtimePath = await useDownload().hash_key(src).join("mtime") - const etagPath = await useDownload().hash_key(src).join("etag") - - const mtime = await mtimePath.read() - const etag = await etagPath.read() - - const rsp = await fetch(src, {}) - const mtimeA = rsp.headers.get("Last-Modified") - const etagA = rsp.headers.get("etag") - - assert(mtimeA === mtime) - assert(etagA === etag) - await rsp.body?.cancel() - }) - } catch { - tmpdir.rm({ recursive: true }) - } -}) diff --git a/tests/unit/error.test.ts b/tests/unit/error.test.ts deleted file mode 100644 index 3d4831eec..000000000 --- a/tests/unit/error.test.ts +++ /dev/null @@ -1,51 +0,0 @@ - -import { assert, assertEquals } from "https://deno.land/std@0.176.0/testing/asserts.ts" -import { TeaError, error } from "utils" -import SemVer from "semver" - -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") - }) -}) diff --git a/tests/unit/fetch.test.ts b/tests/unit/fetch.test.ts deleted file mode 100644 index 4344cb0e7..000000000 --- a/tests/unit/fetch.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { assertSpyCall, stub, assertSpyCallArgs } from "https://deno.land/std@0.183.0/testing/mock.ts"; -import { useVersion, useFetch } from "hooks"; - - -Deno.test("fetch user-agent header check", async () => { - const url = "https://tea.xyz/tea-cli/"; - const version = useVersion() - - const fetchStub = stub( - globalThis, - "fetch", - () => Promise.resolve(new Response("")), - ); - - try { - await useFetch(url, {}); - } finally { - fetchStub.restore(); - } - - const expectedUserAgent = `tea.cli/${version}`; - - assertSpyCallArgs(fetchStub, 0, [url, { - headers: {"User-Agent": expectedUserAgent} - }]); - -}); \ No newline at end of file diff --git a/tests/unit/hydrate.test.ts b/tests/unit/hydrate.test.ts deleted file mode 100644 index f94a35870..000000000 --- a/tests/unit/hydrate.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { assertEquals } from "deno/testing/asserts.ts" -import suite from "../integration.suite.ts" -import { PackageRequirement } from "types" -import * as semver from "utils/semver.ts" -import { it } from "deno/testing/bdd.ts" -import { hydrate } from "prefab" - -it(suite, "hydrates.1", async function() { - const pkgs = [ - { project: 'nodejs.org', constraint: new semver.Range('*') }, - { project: 'nodejs.org', constraint: new semver.Range('>=18.14') } - ] - - const rv1 = semver.intersect(pkgs[0].constraint, pkgs[1].constraint) - assertEquals(rv1.toString(), '>=18.14') - - const rv = await hydrate(pkgs, (_a: PackageRequirement, _b: boolean) => Promise.resolve([])) - - let nodes = 0 - for (const pkg of rv.pkgs) { - if (pkg.project === 'nodejs.org') { - nodes++ - assertEquals(pkg.constraint.toString(), '>=18.14') - } - } - - assertEquals(nodes, 1) -}) - -it(suite, "hydrates.2", async function() { - const pkgs = [ - { project: 'pipenv.pypa.io', constraint: new semver.Range('*') }, - { project: 'python.org', constraint: new semver.Range('~3.9') } - ] - - const rv = await hydrate(pkgs, (pkg: PackageRequirement, _dry: boolean) => { - if (pkg.project === 'pipenv.pypa.io') { - return Promise.resolve([ - { project: 'python.org', constraint: new semver.Range('>=3.7') } - ]) - } else { - return Promise.resolve([]) - } - }) - - let nodes = 0 - for (const pkg of rv.pkgs) { - if (pkg.project === 'python.org') { - assertEquals(pkg.constraint.toString(), '~3.9') - nodes++ - } - } - - assertEquals(nodes, 1) -}) - -it(suite, "hydrates.3", async function() { - const pkgs = [ - { project: 'pipenv.pypa.io', constraint: new semver.Range('*') }, - { project: 'python.org', constraint: new semver.Range('~3.9') } - ] - - const rv = await hydrate(pkgs, (pkg: PackageRequirement, _dry: boolean) => { - if (pkg.project === 'pipenv.pypa.io') { - return Promise.resolve([ - { project: 'python.org', constraint: new semver.Range('~3.9.1') } - ]) - } else { - return Promise.resolve([]) - } - }) - - let nodes = 0 - for (const pkg of rv.pkgs) { - if (pkg.project === 'python.org') { - assertEquals(pkg.constraint.toString(), '~3.9.1') - nodes++ - } - } - - assertEquals(nodes, 1) -}) diff --git a/tests/unit/path-utils.test.ts b/tests/unit/path-utils.test.ts deleted file mode 100644 index ebda8a038..000000000 --- a/tests/unit/path-utils.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Path from "path" -import PathUtils from "path-utils" -import { assertArrayIncludes, assertEquals } from "deno/testing/asserts.ts" - -Deno.test("test PathUtils", async test => { - await test.step("modify $PATH", () => { - const envPath = PathUtils.envPath() - - assertArrayIncludes(envPath, [Path.root.join("usr/bin")]) - - const a = Path.home().join("tmp/bin") - const b = PathUtils.addPath(a) - assertArrayIncludes(b, [a]) - - const c = PathUtils.envPath() - assertArrayIncludes(c, [a]) - - const d = PathUtils.rmPath(a) - assertEquals(d, envPath) - - const e = PathUtils.envPath() - assertEquals(e, envPath) - }) - - await test.step("search $PATH", () => { - const usrBin = Path.root.join("usr/bin") - const bin = Path.root.join("bin") - const sbin = Path.root.join("sbin") - const envPath = PathUtils.envPath() - - assertArrayIncludes(envPath, [usrBin]) - - const a = PathUtils.findBinary("env", envPath) - assertEquals(a, usrBin.join("env")) - - const b = PathUtils.findBinary("env") - assertEquals(b, usrBin.join("env")) - - const c = PathUtils.findBinary("bloogargle", envPath) - assertEquals(c, undefined) - - const d = PathUtils.findBinary("ls", [bin]) - assertEquals(d, bin.join("ls")) - - const e = PathUtils.findBinary("ls", [sbin]) - assertEquals(e, undefined) - }) -}) diff --git a/tests/unit/path.test.ts b/tests/unit/path.test.ts deleted file mode 100644 index f55f4be87..000000000 --- a/tests/unit/path.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import Path from "path"; -import { assert, assertEquals, assertFalse, assertThrows } from "deno/testing/asserts.ts" - -Deno.test("test Path", async test => { - await test.step("creating files", () => { - assertEquals(new Path("/a/b/c").components(), ["", "a", "b", "c"]) - assertEquals(new Path("/a/b/c").split(), [new Path("/a/b"), "c"]) - - const tmp = Path.mktemp({prefix: "tea-"}) - assert(tmp.isEmpty()) - - const child = tmp.join("a/b/c") - assertFalse(child.parent().isDirectory()) - child.mkparent() - assert(child.parent().isDirectory()) - - assertThrows(() => child.readlink()) // not found - assertFalse(child.isReadableFile()) - child.touch() - assert(child.isReadableFile()) - - assert(child.in(tmp)) - assertFalse(tmp.isEmpty()) - assertEquals(child.readlink(), child) // not a link - }) - - await test.step("write and read", async () => { - const tmp = Path.mktemp({prefix: "tea-"}) - - const data = tmp.join("test.dat") - data.write({text: "hello\nworld"}) - - const lines = await asyncIterToArray(data.readLines()) - assertEquals(lines, ["hello", "world"]) - - // will throw with no force flag - assertThrows(() => data.write({ json: { hello: "world" } })) - - data.write({ json: { hello: "world" }, force: true }) - assertEquals(await data.readJSON(), { hello: "world" }) - }) - - await test.step("test walk", async () => { - const tmp = Path.mktemp({prefix: "tea-"}) - - const a = tmp.join("a").mkdir() - a.join("a1").touch() - a.join("a2").touch() - - const b = tmp.join("b").mkdir() - b.join("b1").touch() - b.join("b2").touch() - - const c = tmp.join("c").mkdir() - c.join("c1").touch() - c.join("c2").touch() - - const walked = (await asyncIterToArray(tmp.walk())) - .map(([path, entry]) => { - return {name: path.basename(), isDir: entry.isDirectory} - }) - .sort((a, b) => a.name.localeCompare(b.name)) - - assertEquals(walked, [ - { name: "a", isDir: true}, - { name: "a1", isDir: false}, - { name: "a2", isDir: false}, - { name: "b", isDir: true}, - { name: "b1", isDir: false}, - { name: "b2", isDir: false}, - { name: "c", isDir: true}, - { name: "c1", isDir: false}, - { name: "c2", isDir: false}, - ]) - }) -}) - -async function asyncIterToArray (iter: AsyncIterable){ - const result = []; - for await(const i of iter) { - result.push(i); - } - return result; -} diff --git a/tests/unit/pkgutils.test.ts b/tests/unit/pkgutils.test.ts deleted file mode 100644 index e70ed15bf..000000000 --- a/tests/unit/pkgutils.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { assert, assertEquals, assertFalse, assertThrows } from "deno/testing/asserts.ts" -import SemVer, { Range } from "utils/semver.ts" -import * as pkg from "utils/pkg.ts" - -Deno.test("pkg.str", async test => { - let out: string - - await test.step("precise", () => { - out = pkg.str({ - project: "test", - version: new SemVer("1.2.3") - }) - assertEquals(out, "test=1.2.3") - }) - - for (const range of ["^1", "^1.2", "^1.2.3"]) { - await test.step(range, () => { - out = pkg.str({ - project: "test", - constraint: new Range(range) - }) - assertEquals(out, `test${range}`) - }) - } - - for (const [range, expected] of [[">=1 <2", "^1"], [">=1.2 <2", "^1.2"], [">=1.2.3 <2", "^1.2.3"]]) { - await test.step(`${range} == ${expected}`, () => { - out = pkg.str({ - project: "test", - constraint: new Range(range) - }) - assertEquals(out, `test${expected}`) - }) - } - - await test.step("range of one version", () => { - const constraint = new Range("=1.2.3") - - out = pkg.str({ - project: "test", - constraint - }) - assert(constraint.single()) - assertEquals(out, `test=1.2.3`) - }) -}) - -Deno.test("pkg.parse", async test => { - await test.step("@latest", () => { - const { constraint } = pkg.parse("test@latest") - assert(constraint.satisfies(new SemVer([5,0,0]))) - assert(constraint.satisfies(new SemVer([5,1,0]))) - assert(constraint.satisfies(new SemVer([6,0,0]))) - }) - - await test.step("@5", () => { - const { constraint } = pkg.parse("test@5") - assert(constraint.satisfies(new SemVer([5,0,0]))) - assert(constraint.satisfies(new SemVer([5,1,0]))) - assertFalse(constraint.satisfies(new SemVer([6,0,0]))) - }) - - await test.step("@5.0", () => { - const { constraint } = pkg.parse("test@5.0") - assert(constraint.satisfies(new SemVer([5,0,0]))) - assert(constraint.satisfies(new SemVer([5,0,1]))) - assertFalse(constraint.satisfies(new SemVer([5,1,0]))) - }) - - await test.step("@5.0.0", () => { - const { constraint } = pkg.parse("test@5.0.0") - assert(constraint.satisfies(new SemVer([5,0,0]))) - assert(constraint.satisfies(new SemVer([5,0,0,1]))) - assertFalse(constraint.satisfies(new SemVer([5,0,1]))) - }) - - await test.step("bad input", () => { - assertThrows(() => pkg.parse("asdf^@~"), "invalid pkgspec: asdf^@~") - }) -}) - -Deno.test("pkg.compare", async test => { - await test.step("compare versions", () => { - const a = { project: "test", version: new SemVer("1.2.3") } - const b = { project: "test", version: new SemVer("2.1.3") } - assert(pkg.compare(a, b) < 0) - }) - - await test.step("compare pkg names", () => { - const a = { project: "a", version: new SemVer("1.2.3") } - const b = { project: "b", version: new SemVer("1.2.3") } - assert(pkg.compare(a, b) < 0) - }) -}) diff --git a/tests/unit/semver.test.ts b/tests/unit/semver.test.ts deleted file mode 100644 index 5f43acfac..000000000 --- a/tests/unit/semver.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { assert, assertEquals, assertFalse, assertThrows } from "deno/testing/asserts.ts" -import SemVer, * as semver from "utils/semver.ts" - - -Deno.test("semver", async test => { - await test.step("sort", () => { - const input = [new SemVer([1,2,3]), new SemVer("2.3.4"), new SemVer("1.2.4"), semver.parse("1.2.3.1")!] - const sorted1 = input.sort(semver.compare) - const sorted2 = input.sort() - - assertEquals(sorted1.join(","), "1.2.3,1.2.3.1,1.2.4,2.3.4") - assertEquals(sorted2.join(","), "1.2.3,1.2.3.1,1.2.4,2.3.4") - }) - - await test.step("parse", () => { - assertEquals(semver.parse("1.2.3.4.5")?.toString(), "1.2.3.4.5") - assertEquals(semver.parse("1.2.3.4")?.toString(), "1.2.3.4") - assertEquals(semver.parse("1.2.3")?.toString(), "1.2.3") - assertEquals(semver.parse("1.2")?.toString(), "1.2.0") - assertEquals(semver.parse("1")?.toString(), "1.0.0") - }) - - await test.step("satisfies", () => { - assertEquals(new semver.Range("=3.1.0").max([new SemVer("3.1.0")]), new SemVer("3.1.0")) - }) - - await test.step("constructor", () => { - assertEquals(new SemVer("1.2.3.4.5.6").toString(), "1.2.3.4.5.6") - assertEquals(new SemVer("1.2.3.4.5").toString(), "1.2.3.4.5") - assertEquals(new SemVer("1.2.3.4").toString(), "1.2.3.4") - assertEquals(new SemVer("1.2.3").toString(), "1.2.3") - assertEquals(new SemVer("v1.2.3").toString(), "1.2.3") - assertEquals(new SemVer("1.2").toString(), "1.2.0") - assertEquals(new SemVer("v1.2").toString(), "1.2.0") - assertEquals(new SemVer("1").toString(), "1.0.0") - assertEquals(new SemVer("v1").toString(), "1.0.0") - - assertEquals(new SemVer("9e").toString(), "9e") - assertEquals(new SemVer("9e").components, [9,5]) - assertEquals(new SemVer("3.3a").toString(), "3.3a") - assertEquals(new SemVer("3.3a").components, [3,3,1]) - assertEquals(new SemVer("1.1.1q").toString(), "1.1.1q") - assertEquals(new SemVer("1.1.1q").components, [1,1,1,17]) - }) - - await test.step("ranges", () => { - const a = new semver.Range(">=1.2.3<2.3.4 || >=3") - assertEquals(a.toString(), ">=1.2.3<2.3.4,>=3") - - assert(a.satisfies(new SemVer("1.2.3"))) - assert(a.satisfies(new SemVer("1.4.1"))) - assert(a.satisfies(new SemVer("3.0.0"))) - assert(a.satisfies(new SemVer("90.0.0"))) - assertFalse(a.satisfies(new SemVer("2.3.4"))) - assertFalse(a.satisfies(new SemVer("2.5.0"))) - - const b = new semver.Range("^0.15") - // Due to the nature of the `^` operator, this - // is the same as `~0.15`, and our code represents - // it as such. - assertEquals(b.toString(), "~0.15") - - const c = new semver.Range("~0.15") - assertEquals(c.toString(), "~0.15") - - assert(c.satisfies(new SemVer("0.15.0"))) - assert(c.satisfies(new SemVer("0.15.1"))) - assertFalse(c.satisfies(new SemVer("0.14.0"))) - assertFalse(c.satisfies(new SemVer("0.16.0"))) - - const d = new semver.Range("~0.15.1") - assertEquals(d.toString(), "~0.15.1") - assert(d.satisfies(new SemVer("0.15.1"))) - assert(d.satisfies(new SemVer("0.15.2"))) - assertFalse(d.satisfies(new SemVer("0.15.0"))) - assertFalse(d.satisfies(new SemVer("0.16.0"))) - assertFalse(d.satisfies(new SemVer("0.14.0"))) - - // `~` is weird - const e = new semver.Range("~1") - assertEquals(e.toString(), "^1") - assert(e.satisfies(new SemVer("v1.0"))) - assert(e.satisfies(new SemVer("v1.1"))) - assertFalse(e.satisfies(new SemVer("v2"))) - - const f = new semver.Range("^14||^16||^18") - assert(f.satisfies(new SemVer("14.0.0"))) - assertFalse(f.satisfies(new SemVer("15.0.0"))) - assert(f.satisfies(new SemVer("16.0.0"))) - assertFalse(f.satisfies(new SemVer("17.0.0"))) - assert(f.satisfies(new SemVer("18.0.0"))) - - const g = new semver.Range("<15") - assert(g.satisfies(new SemVer("14.0.0"))) - assert(g.satisfies(new SemVer("0.0.1"))) - assertFalse(g.satisfies(new SemVer("15.0.0"))) - - const i = new semver.Range("^1.2.3.4") - assert(i.satisfies(new SemVer("1.2.3.4"))) - assert(i.satisfies(new SemVer("1.2.3.5"))) - assert(i.satisfies(new SemVer("1.2.4.2"))) - assert(i.satisfies(new SemVer("1.3.4.2"))) - assertFalse(i.satisfies(new SemVer("2.0.0"))) - - const j = new semver.Range("^0.1.2.3") - assert(j.satisfies(new SemVer("0.1.2.3"))) - assert(j.satisfies(new SemVer("0.1.3"))) - assertFalse(j.satisfies(new SemVer("0.2.0"))) - - const k = new semver.Range("^0.0.1.2") - assertFalse(k.satisfies(new SemVer("0.0.1.1"))) - assert(k.satisfies(new SemVer("0.0.1.2"))) - assert(k.satisfies(new SemVer("0.0.1.9"))) - assertFalse(k.satisfies(new SemVer("0.0.2.0"))) - - const l = new semver.Range("^0.0.0.1") - assertFalse(l.satisfies(new SemVer("0.0.0.0"))) - assert(l.satisfies(new SemVer("0.0.0.1"))) - assertFalse(l.satisfies(new SemVer("0.0.0.2"))) - - // This one is weird, but it should mean "<1" - const m = new semver.Range("^0") - assert(m.satisfies(new SemVer("0.0.0"))) - assert(m.satisfies(new SemVer("0.0.1"))) - assert(m.satisfies(new SemVer("0.1.0"))) - assert(m.satisfies(new SemVer("0.9.1"))) - assertFalse(m.satisfies(new SemVer("1.0.0"))) - - assertThrows(() => new semver.Range("1")) - assertThrows(() => new semver.Range("1.2")) - assertThrows(() => new semver.Range("1.2.3")) - assertThrows(() => new semver.Range("1.2.3.4")) - }) - - await test.step("intersection", async test => { - await test.step("^3.7…=3.11", () => { - const a = new semver.Range("^3.7") - const b = new semver.Range("=3.11") - - assertEquals(b.toString(), "=3.11.0") - - const c = semver.intersect(a, b) - assertEquals(c.toString(), "=3.11.0") - }) - - await test.step("^3.7…^3.9", () => { - const a = new semver.Range("^3.7") - const b = new semver.Range("^3.9") - - assertEquals(b.toString(), "^3.9") - - const c = semver.intersect(a, b) - assertEquals(c.toString(), "^3.9") - }) - - await test.step("^3.7…*", () => { - const a = new semver.Range("^3.7") - const b = new semver.Range("*") - - assertEquals(b.toString(), "*") - - const c = semver.intersect(a, b) - assertEquals(c.toString(), "^3.7") - }) - - await test.step("~3.7…~3.8", () => { - const a = new semver.Range("~3.7") - const b = new semver.Range("~3.8") - - assertThrows(() => semver.intersect(a, b)) - }) - - await test.step("^3.7…=3.8", () => { - const a = new semver.Range("^3.7") - const b = new semver.Range("=3.8") - const c = semver.intersect(a, b) - assertEquals(c.toString(), "=3.8.0") - }) - - await test.step("^11,^12…^11.3", () => { - const a = new semver.Range("^11,^12") - const b = new semver.Range("^11.3") - const c = semver.intersect(a, b) - assertEquals(c.toString(), "^11.3") - }) - - await test.step(">=11<12", () => { - const a = new semver.Range(">=11<12") - const b = new semver.Range(">=11.0.0 <13.0.0.0") - //assertEquals(a.toString(), "^11.3") - assert(a.satisfies(new SemVer("11.0.0"))) - assert(a.satisfies(new SemVer("11.9.0"))) - assert(b.satisfies(new SemVer("11.0.0"))) - assert(b.satisfies(new SemVer("11.9.0"))) - assert(b.satisfies(new SemVer("12.9.0"))) - }) - - await test.step(">=0.47<1", () => { - const a = new semver.Range(">=0.47<1") - assertEquals(a.toString(), ">=0.47<1") - assert(a.satisfies(new SemVer("0.47.0"))) - assert(a.satisfies(new SemVer("0.47.9"))) - assert(a.satisfies(new SemVer("0.48.0"))) - assert(a.satisfies(new SemVer("0.80.0"))) - assertFalse(a.satisfies(new SemVer("1.0.0"))) - }) - - //FIXME this *should* work - // await test.step("^11,^12…^11.3,^12.2", () => { - // const a = new semver.Range("^11,^12") - // const b = new semver.Range("^11.3") - // const c = semver.intersect(a, b) - // assertEquals(c.toString(), "^11.3,^12.2") - // }) - }) -}) diff --git a/tests/unit/useErrorHandler.test.ts b/tests/unit/useErrorHandler.test.ts index c62db4c01..cdefc162f 100644 --- a/tests/unit/useErrorHandler.test.ts +++ b/tests/unit/useErrorHandler.test.ts @@ -1,11 +1,13 @@ -import { assertEquals } from "https://deno.land/std@0.176.0/testing/asserts.ts" -import { useErrorHandler } from "hooks"; -import { Config, _internals } from "hooks/useConfig.ts" -import { ExitError } from "types" -import { TeaError } from "utils"; +import { ConfigDefault } from "../../src/hooks/useConfig.ts" +import { useErrorHandler, Verbosity, ExitError } from "hooks" +import { assertEquals } from "deno/testing/asserts.ts" +import { TeaError, hooks } from "tea" +const { useConfig } = hooks -Deno.test("useErrorHandler", async test => { - _internals.setConfig({silent: false, debug: true} as Config) +Deno.test("useErrorHandler", async test => { + const config = ConfigDefault() + config.modifiers.verbosity = Verbosity.debug + useConfig(config) await test.step("exit error", async () => { const rc = await useErrorHandler(new ExitError(123)) @@ -24,8 +26,10 @@ Deno.test("useErrorHandler", async test => { }) }) -Deno.test("useErrorHandler silent", async test => { - _internals.setConfig({silent: true, debug: false} as Config) +Deno.test("useErrorHandler silent", async test => { + const config = ConfigDefault() + config.modifiers.verbosity = Verbosity.quiet + useConfig(config) await test.step("normal error", async () => { const rc = await useErrorHandler(new Error("unit test error")) diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts deleted file mode 100644 index 04cadfcf3..000000000 --- a/tests/unit/utils.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { assertEquals, assertRejects, assertThrows } from "deno/testing/asserts.ts" -import { async_flatmap, flatmap, panic, validate_arr, validate_str } from "utils" - -Deno.test("validate string", () => { - assertEquals(validate_str(true), "true") - assertEquals(validate_str(false), "false") - assertEquals(validate_str(1), "1") - - assertThrows(() => validate_str({}), "not-string: {}") -}) - -Deno.test("validate array", () => { - assertEquals(validate_arr(["1", "2"]), ["1", "2"]) - assertThrows(() => validate_arr("jkl"), "not-array: jkl") -}) - -Deno.test("flatmap", () => { - assertEquals(flatmap(1, (n) => n + 1), 2) - assertEquals(flatmap(undefined, (n: number) => n + 1), undefined) - - const throws = (_n: number) => { - throw Error("test error") - } - - assertEquals(flatmap(1, throws, {rescue: true}), undefined) - assertThrows(() => flatmap(1, throws), "test error") -}) - -Deno.test("async flatmap", async () => { - const producer = (value?: T, err?: Error): Promise => { - if (err) { - return Promise.reject(err) - } - return Promise.resolve(value) - } - - const add = (n: number) => Promise.resolve(n + 1) - - assertEquals(await async_flatmap(producer(1), add), 2) - assertEquals(await async_flatmap(producer(undefined), add), undefined) - assertEquals(await async_flatmap(producer(1), (_n) => undefined), undefined) - - assertEquals(await async_flatmap(producer(1, Error()), add, {rescue: true}), undefined) - await assertRejects(() => async_flatmap(producer(1, Error()), add, undefined)) -}) - -Deno.test("chuzzle", () => { - assertEquals("".chuzzle(), undefined) - assertEquals("test".chuzzle(), "test") - assertEquals([].chuzzle(), undefined) - assertEquals([1, 2, 3].chuzzle(), [1, 2, 3]) -}) - -Deno.test("panic", () => { - assertThrows(() => panic("test msg"), "test msg") -}) From 840963c58e8b3166ef15acbb8b133ae09b761f21 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 08:38:57 -0400 Subject: [PATCH 02/23] wip --- .github/workflows/ci.sync.yml | 1 - deno.jsonc | 2 +- src/app.main.ts | 2 +- src/hooks/usePackageYAML.ts | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.sync.yml b/.github/workflows/ci.sync.yml index 1dd282375..790ec940c 100644 --- a/.github/workflows/ci.sync.yml +++ b/.github/workflows/ci.sync.yml @@ -5,7 +5,6 @@ on: paths: - import-map.json - src/hooks/app.sync.ts - - src/hooks/useSync.ts - src/prefab/install.ts - .github/workflows/ci.sync.yml diff --git a/deno.jsonc b/deno.jsonc index ea0506b8a..067729a3f 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -32,7 +32,7 @@ } }, "imports": { - "tea": "https://raw.github.com/teaxyz/lib/v0/mod.ts", + "tea": "../lib/mod.ts", "tea/": "https://raw.github.com/teaxyz/lib/v0/src/", "hooks": "./src/hooks/index.ts", "deno/": "https://deno.land/std@0.182.0/", diff --git a/src/app.main.ts b/src/app.main.ts index d9119c851..a0f1d10e7 100644 --- a/src/app.main.ts +++ b/src/app.main.ts @@ -1,9 +1,9 @@ import { usePrefix, useExec, useVirtualEnv, useVersion, usePrint, useConfig } from "hooks" import { VirtualEnv } from "./hooks/useVirtualEnv.ts" import { Verbosity } from "./hooks/useConfig.ts" +import { Path, utils, semver, hooks } from "tea" import { basename } from "deno/path/mod.ts" import exec, { repl } from "./app.exec.ts" -import { Path, utils, semver, hooks } from "tea" import provides from "./app.provides.ts" import magic from "./app.magic.ts" import dump from "./app.dump.ts" diff --git a/src/hooks/usePackageYAML.ts b/src/hooks/usePackageYAML.ts index 24ca26644..b1b12bccb 100644 --- a/src/hooks/usePackageYAML.ts +++ b/src/hooks/usePackageYAML.ts @@ -20,7 +20,8 @@ export default function usePackageYAML(yaml: unknown): Return1 { function go(node: any) { if (!node) return [] return Object.entries(validate.obj(node)) - .compact(([project, constraint]) => validatePackageRequirement(project, constraint)) + .compact(([project, constraint]) => + validatePackageRequirement(project, constraint)) } } From eacdf7a53ac258a93219c1ffb443916a1c0119eb Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 08:40:41 -0400 Subject: [PATCH 03/23] wip --- deno.jsonc | 2 +- src/hooks/usePackageYAML.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 067729a3f..9fa8cd50a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -35,7 +35,7 @@ "tea": "../lib/mod.ts", "tea/": "https://raw.github.com/teaxyz/lib/v0/src/", "hooks": "./src/hooks/index.ts", - "deno/": "https://deno.land/std@0.182.0/", + "deno/": "https://deno.land/std@0.187.0/", "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts", diff --git a/src/hooks/usePackageYAML.ts b/src/hooks/usePackageYAML.ts index b1b12bccb..a6d865a99 100644 --- a/src/hooks/usePackageYAML.ts +++ b/src/hooks/usePackageYAML.ts @@ -85,7 +85,7 @@ export async function usePackageYAMLFrontMatter(script: Path, srcroot?: Path): P } -import { parse as parseYaml } from "deno/encoding/yaml.ts" +import { parse as parseYaml } from "https://deno.land/std@0.182.0/encoding/yaml.ts" import { readLines } from "deno/io/read_lines.ts" async function readYAMLFrontMatter(path: Path): Promise { From 8b562993aab93842a6f1b3de1656ed2780ecf729 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 09:05:46 -0400 Subject: [PATCH 04/23] wip --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 9fa8cd50a..1961e34ca 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -33,7 +33,7 @@ }, "imports": { "tea": "../lib/mod.ts", - "tea/": "https://raw.github.com/teaxyz/lib/v0/src/", + "tea/": "../lib/src/", "hooks": "./src/hooks/index.ts", "deno/": "https://deno.land/std@0.187.0/", "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", From 06ea7e2a477a7a0a03028562bf3d06433357039c Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 09:43:25 -0400 Subject: [PATCH 05/23] wip --- deno.jsonc | 6 +++--- src/hooks/useConfig.ts | 4 ++-- tests/functional/testUtils.ts | 11 +++-------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 1961e34ca..ea0506b8a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -32,10 +32,10 @@ } }, "imports": { - "tea": "../lib/mod.ts", - "tea/": "../lib/src/", + "tea": "https://raw.github.com/teaxyz/lib/v0/mod.ts", + "tea/": "https://raw.github.com/teaxyz/lib/v0/src/", "hooks": "./src/hooks/index.ts", - "deno/": "https://deno.land/std@0.187.0/", + "deno/": "https://deno.land/std@0.182.0/", "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts", diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index e689637da..0bc4740bc 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -47,8 +47,8 @@ export default function(input?: Config): Config { } } -export function ConfigDefault(flags?: Flags, arg0 = Deno.execPath(), defaults?: ConfigBase, env = Deno.env.toObject()): Config { - defaults ??= ConfigBaseDefault(env) +export function ConfigDefault(flags?: Flags, arg0 = Deno.execPath(), env = Deno.env.toObject()): Config { + const defaults = ConfigBaseDefault(env) const { TEA_DIR, diff --git a/tests/functional/testUtils.ts b/tests/functional/testUtils.ts index 9667cc652..e1ef3963d 100644 --- a/tests/functional/testUtils.ts +++ b/tests/functional/testUtils.ts @@ -25,11 +25,8 @@ export const createTestHarness = async (config?: TestConfig) => { if (sync) { const [syncArgs, flags] = parseArgs(["--sync", "--silent"], teaDir.string) - const config = ConfigDefault(flags, teaDir.string, undefined, { NO_COLOR: '1' }) - useConfig({ - ...config, - prefix: TEA_PREFIX - }) + const config = ConfigDefault(flags, teaDir.string, { NO_COLOR: '1', TEA_PREFIX: TEA_PREFIX.string }) + useConfig(config) await run(syncArgs) } @@ -42,12 +39,10 @@ export const createTestHarness = async (config?: TestConfig) => { try { const [appArgs, flags] = parseArgs(args, teaDir.string) - const config = ConfigDefault(flags, teaDir.string, undefined, { NO_COLOR: '1', PATH: "/usr/bin:/bin" }) - config.arg0 = teaDir + const config = ConfigDefault(flags, teaDir.string, { NO_COLOR: '1', PATH: "/usr/bin:/bin", TEA_PREFIX: TEA_PREFIX.string }) useConfig({ ...config, - prefix: TEA_PREFIX, ...configOverrides, }) From 1ab7016c90eef37599fe8cae0634efedd330e874 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 14:22:35 -0400 Subject: [PATCH 06/23] wip --- src/app.help.ts | 5 +-- src/app.main.ts | 4 +- src/app.ts | 3 +- src/args.ts | 9 ++-- src/hooks/useConfig.ts | 28 ++++++++++-- src/hooks/useExec.ts | 96 +++++++++++++++++++++++++++++++++++++++--- src/hooks/useLogger.ts | 2 +- 7 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/app.help.ts b/src/app.help.ts index 40a28748e..c1e2c7470 100644 --- a/src/app.help.ts +++ b/src/app.help.ts @@ -1,9 +1,8 @@ import { Verbosity } from "./hooks/useConfig.ts" -import { useConfig, usePrint } from "hooks" +import { usePrint } from "hooks" import undent from "outdent" -export default async function help() { - const { modifiers: { verbosity } } = useConfig() +export default async function help(verbosity = Verbosity.normal) { const { print } = usePrint() if (verbosity < Verbosity.loud) { diff --git a/src/app.main.ts b/src/app.main.ts index a0f1d10e7..e7be9e1e6 100644 --- a/src/app.main.ts +++ b/src/app.main.ts @@ -14,7 +14,7 @@ const { useSync } = hooks export async function run(args: Args) { const { print } = usePrint() - const { arg0: execPath, env: { PATH, SHELL } } = useConfig() + const { arg0: execPath, env: { PATH, SHELL }, modifiers: { verbosity } } = useConfig() if (args.cd) { const chdir = args.cd @@ -82,7 +82,7 @@ export async function run(args: Args) { } break }} break case "help": - await help() + await help(verbosity) break case "version": await print(`tea ${useVersion()}`) diff --git a/src/app.ts b/src/app.ts index c29db0086..71e418e84 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,10 +31,9 @@ function applyVerbosity(config: Config) { function noop() {} if (config!.modifiers.verbosity < Verbosity.debug) console.debug = noop if (config!.modifiers.verbosity < Verbosity.loud) console.log = noop - if (config!.modifiers.verbosity < Verbosity.normal) { + if (config!.modifiers.verbosity < Verbosity.quiet) { console.info = noop console.warn = noop - console.log = noop console.error = noop } } diff --git a/src/args.ts b/src/args.ts index 38a0f957f..00c5e1904 100644 --- a/src/args.ts +++ b/src/args.ts @@ -117,10 +117,13 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { flags.keepGoing = parseBool(value ?? "yes") ?? barf() break case 'quiet': - case 'silent': nonovalue() flags.verbosity = -1 break + case 'silent': + nonovalue() + flags.verbosity = -2 + break case 'dump': console.warn("tea --dump is deprecated, instead only provide pkg specifiers") break @@ -152,7 +155,7 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { rv.cd = Path.cwd().join(validate.str(it.next().value)) break case 's': - flags.verbosity = -1; + flags.verbosity = -2; break case 'X': console.warn("tea -X is now implicit and thus specifying `-X` now both unrequired and deprecated") @@ -210,6 +213,6 @@ function parseBool(input: string) { export class UsageError extends Error { constructor(arg: string) { - super(`usage error: no such arg: ${arg}`) + super(`error: no such arg: ${arg}`) } } diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 0bc4740bc..7b576b4ae 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -69,7 +69,7 @@ export function ConfigDefault(flags?: Flags, arg0 = Deno.execPath(), env = Deno. UserAgent: `tea.cli/${useVersion()}`, logger: { prefix: undefined, - color: loggerColor(env) + color: getColor(env) }, modifiers: { dryrun: flags?.dryrun ?? false, @@ -94,6 +94,7 @@ export function ConfigDefault(flags?: Flags, arg0 = Deno.execPath(), env = Deno. } export enum Verbosity { + silent = -2, quiet = -1, normal = 0, loud = 1, @@ -102,16 +103,22 @@ export enum Verbosity { } function getVerbosity(env: Record): Verbosity { - const { DEBUG, GITHUB_ACTIONS, RUNNER_DEBUG, VERBOSE } = env + const { DEBUG, GITHUB_ACTIONS, RUNNER_DEBUG, VERBOSE, CI } = env if (DEBUG == '1') return Verbosity.debug if (GITHUB_ACTIONS == 'true' && RUNNER_DEBUG == '1') return Verbosity.debug const verbosity = flatmap(VERBOSE, parseInt) - return isNumber(verbosity) ? verbosity : Verbosity.normal + if (isNumber(verbosity)) { + return verbosity + } else if (boolize(CI)) { + return Verbosity.quiet + } else { + return Verbosity.normal + } } -function loggerColor(env: Record) { +function getColor(env: Record) { const isTTY = () => Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stdout.rid) if ((env.CLICOLOR ?? '1') != '0' && isTTY()){ @@ -137,3 +144,16 @@ function loggerColor(env: Record) { return false } + +function boolize(input: string | undefined): boolean | undefined { + switch (input?.trim()?.toLowerCase()) { + case '0': + case 'false': + case 'no': + return false + case '1': + case 'true': + case 'yes': + return true + } +} \ No newline at end of file diff --git a/src/hooks/useExec.ts b/src/hooks/useExec.ts index 98d28fc38..eb4e0f53b 100644 --- a/src/hooks/useExec.ts +++ b/src/hooks/useExec.ts @@ -1,4 +1,4 @@ -import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError } from "tea" +import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError, Package } from "tea" import { usePackageYAMLFrontMatter } from "./usePackageYAML.ts" import { ExitError } from "./useErrorHandler.ts" import { VirtualEnv } from "./useVirtualEnv.ts" @@ -114,9 +114,9 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) { ///////////////////////////////////////////////////////////////////////////// funcs async function install(pkgs: PackageSpecification[], update: boolean) { - const { modifiers: { json, dryrun }, env } = useConfig() + const { modifiers: { json, dryrun, verbosity }, env } = useConfig() const logger = useLogger().new() - const { logJSON } = useLogger() + const { logJSON, gray, teal } = useLogger() if (!json) { logger.replace("resolving package graph") @@ -124,6 +124,12 @@ async function install(pkgs: PackageSpecification[], update: boolean) { logJSON({ status: "resolving" }) } + const pkg_prefix_str = (pkg: Package) => [ + gray(usePrefix().prettyString()), + pkg.project, + `${gray('v')}${pkg.version}` + ].join(gray('/')) + console.debug({hydrating: pkgs}) const { pkgs: wet, dry } = await hydrate(pkgs) @@ -154,9 +160,75 @@ async function install(pkgs: PackageSpecification[], update: boolean) { } while (true) } - if (!dryrun) for (const pkg of pending) { - const install = await base_install(pkg) - await link(install) + for (const pkg of pending) { + const log_install_msg = (pkg: Package, title = 'installed') => { + if (json) { + logJSON({status: title, pkg: utils.pkg.str(pkg)}) + } else { + const str = pkg_prefix_str(pkg) + logger!.replace(`${title}: ${str}`, { prefix: false }) + } + } + + let install: Installation + + if (dryrun) { + install = { pkg, path: usePrefix().join(pkg.project, `v${pkg.version}`) } + log_install_msg(pkg, 'imagined') + } else { + let bytes = 0 + const timestamp = Date.now() + install = await base_install(pkg, { + locking: () => { + if (!json) { + logger.replace(teal("locking")) + } else { + logJSON({status: "locking", pkg: utils.pkg.str(pkg) }) + } + }, + /// raw http info + downloading: ({pkg, src, dst, rcvd, total}) => { + if (json) { + logJSON({status: "downloading", "received": rcvd, "content-size": total, pkg, src, dst }) + } else if (verbosity >= 0) { + bytes = rcvd ?? 0 + } else if (total) { + logger.replace(`installing: ${pkg_prefix_str(pkg)} (${pretty_size(total)})`) + } else { + logger.replace(`installing: ${pkg_prefix_str(pkg)}`) + } + }, + installing: ({pkg, progress}) => { + if (json) { + logJSON({status: "installing", pkg, progress }) + } else if (verbosity >= 0) { + let s = teal("installing") + let pc = (progress ?? 0) * 100; + pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5% + + s += ` ${pc}%`.padEnd(4, ' ') + + const duration = Date.now() - timestamp + if (duration > 0) { + const speed = bytes / duration * 1000 + let dl = pretty_size(speed) + dl += "/s" + s += ` ${gray(dl)}` + } + + logger.replace(s) + } + }, + unlocking: (pkg: Package) => { + if (json) logJSON({status: "unlocking", pkg: utils.pkg.str(pkg) }) + }, + installed: (installation: Installation) => { + log_install_msg(installation.pkg) + } + }) + await link(install) + } + installed.push(install) } @@ -164,6 +236,7 @@ async function install(pkgs: PackageSpecification[], update: boolean) { } import { readLines } from "deno/io/read_lines.ts" +import { usePrefix } from "./index.ts"; async function read_shebang(path: Path): Promise { const f = await Deno.open(path.string, { read: true }) @@ -328,3 +401,14 @@ export async function which(arg0: string | undefined) { const subst = function(start: number, end: number, input: string, what: string) { return input.substring(0, start) + what + input.substring(end) } + +function pretty_size(n: number) { + const units = ["B", "KiB", "MiB", "GiB", "TiB"] + let i = 0 + while (n > 1024 && i < units.length - 1) { + n /= 1024 + i++ + } + const precision = n < 10 ? 2 : n < 100 ? 1 : 0 + return `${n.toFixed(precision)} ${units[i]}` +} diff --git a/src/hooks/useLogger.ts b/src/hooks/useLogger.ts index 15231c9d1..b0e3c54f3 100644 --- a/src/hooks/useLogger.ts +++ b/src/hooks/useLogger.ts @@ -76,7 +76,7 @@ export class Logger { //TODO don’t erase whole lines, just erase the part that is different replace(line: string, {prefix: wprefix}: {prefix: boolean} = {prefix: true}) { - if (this.verbosity < 0) return + if (this.verbosity < -1) return if (line == this.last_line) { return //noop From 4b80c223e3dec82197ea6ed376dc121c3d190fbc Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 14:33:28 -0400 Subject: [PATCH 07/23] wip --- deno.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index ea0506b8a..a158f5bf3 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -32,8 +32,8 @@ } }, "imports": { - "tea": "https://raw.github.com/teaxyz/lib/v0/mod.ts", - "tea/": "https://raw.github.com/teaxyz/lib/v0/src/", + "tea": "https://raw.github.com/teaxyz/lib/v0.1.2/mod.ts", + "tea/": "https://raw.github.com/teaxyz/lib/v0.1.2/src/", "hooks": "./src/hooks/index.ts", "deno/": "https://deno.land/std@0.182.0/", "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", From 4d47879789a4d7a071808d3fa22047592a6fb851 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 14:47:12 -0400 Subject: [PATCH 08/23] wip --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f73cfc4e0..824894ff3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,8 @@ jobs: - macos-latest steps: - uses: actions/checkout@v3 - - uses: ./.github/actions/cache # avoids sporadic 500s from deno’s CDN - uses: denoland/setup-deno@v1 # using ourself to install deno could compromise the tests + - run: deno cache $(find . -name \*.ts) - run: deno task test --coverage=cov_profile - run: deno coverage cov_profile --lcov --exclude=tests/ --output=cov_profile.lcov - uses: coverallsapp/github-action@v1 From 82fa8b2ea5223b6cd19f6556b5337e1cd38ccda9 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 14:59:44 -0400 Subject: [PATCH 09/23] wip --- src/args.ts | 2 +- src/hooks/useConfig.ts | 16 ++-------------- tests/functional/testUtils.ts | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/args.ts b/src/args.ts index 00c5e1904..a13f96365 100644 --- a/src/args.ts +++ b/src/args.ts @@ -194,7 +194,7 @@ export function parseArgs(args: string[], arg0: string): [Args, Flags, Error?] { return [rv, flags] } -function parseBool(input: string) { +export function parseBool(input: string) { switch (input) { case '1': case 'true': diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 7b576b4ae..31e68e620 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -1,4 +1,5 @@ import useVersion from "./useVersion.ts" +import { parseBool } from "../args.ts" import { Flags } from "../args.ts" import { isNumber } from "is-what" import { utils, Path } from "tea" @@ -111,7 +112,7 @@ function getVerbosity(env: Record): Verbosity { const verbosity = flatmap(VERBOSE, parseInt) if (isNumber(verbosity)) { return verbosity - } else if (boolize(CI)) { + } else if (parseBool(CI)) { return Verbosity.quiet } else { return Verbosity.normal @@ -144,16 +145,3 @@ function getColor(env: Record) { return false } - -function boolize(input: string | undefined): boolean | undefined { - switch (input?.trim()?.toLowerCase()) { - case '0': - case 'false': - case 'no': - return false - case '1': - case 'true': - case 'yes': - return true - } -} \ No newline at end of file diff --git a/tests/functional/testUtils.ts b/tests/functional/testUtils.ts index e1ef3963d..bb767746b 100644 --- a/tests/functional/testUtils.ts +++ b/tests/functional/testUtils.ts @@ -39,7 +39,7 @@ export const createTestHarness = async (config?: TestConfig) => { try { const [appArgs, flags] = parseArgs(args, teaDir.string) - const config = ConfigDefault(flags, teaDir.string, { NO_COLOR: '1', PATH: "/usr/bin:/bin", TEA_PREFIX: TEA_PREFIX.string }) + const config = ConfigDefault(flags, teaDir.string, { NO_COLOR: '1', PATH: "/usr/bin:/bin", TEA_PREFIX: TEA_PREFIX.string, VERBOSE: '-1' }) useConfig({ ...config, From c312dd92fb568ce349111cabf5c4eb06a0efc01b Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 15:30:55 -0400 Subject: [PATCH 10/23] wip --- src/app.main.ts | 4 +++- tests/functional/devenv.test.ts | 4 ++-- tests/functional/run.test.ts | 24 +++++++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/app.main.ts b/src/app.main.ts index e7be9e1e6..bb38eb0ca 100644 --- a/src/app.main.ts +++ b/src/app.main.ts @@ -14,7 +14,7 @@ const { useSync } = hooks export async function run(args: Args) { const { print } = usePrint() - const { arg0: execPath, env: { PATH, SHELL }, modifiers: { verbosity } } = useConfig() + const { arg0: execPath, env: { PATH, SHELL }, modifiers: { verbosity, json } } = useConfig() if (args.cd) { const chdir = args.cd @@ -64,6 +64,8 @@ export async function run(args: Args) { } else { console.error("tea: empty pkg env") } + } else if (json) { + await print(JSON.stringify({env})) } else for (const key in env) { const inferred = env[key].split(":") const inherited = Deno.env.get(key)?.split(":") ?? [] diff --git a/tests/functional/devenv.test.ts b/tests/functional/devenv.test.ts index c22563eda..cce7f51a0 100644 --- a/tests/functional/devenv.test.ts +++ b/tests/functional/devenv.test.ts @@ -50,8 +50,8 @@ Deno.test("dev env interactions with HOME", { sanitizeResources: false, sanitize Deno.test("should enter dev env", { sanitizeResources: false, sanitizeOps: false }, async test => { // each of the files in this list must have a zlib.net^1.2 dependency and a FOO=BAR env - const envFiles = ["tea.yaml"]//, "deno.json", "deno.jsonc", "package.json", "cargo.toml", - // "Gemfile", "pyproject.toml", "go.mod", "requirements.txt"] + const envFiles = ["tea.yaml", "deno.json", "deno.jsonc", "package.json", "cargo.toml", + "Gemfile", "pyproject.toml", "go.mod", "requirements.txt"] for (const shell of ["/bin/bash", "/bin/fish", "/bin/elvish"]) { for (const envFile of envFiles) { diff --git a/tests/functional/run.test.ts b/tests/functional/run.test.ts index 0575596d2..256bda284 100644 --- a/tests/functional/run.test.ts +++ b/tests/functional/run.test.ts @@ -1,10 +1,10 @@ -import { assert, assertEquals } from "deno/testing/asserts.ts" +import { assert, assertEquals, assertRejects } from "deno/testing/asserts.ts" import { createTestHarness } from "./testUtils.ts" Deno.test("env", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness() - const { stdout } = await run(["+kubernetes.io/kubectl"]) + const { stdout } = await run(["+kubernetes.io/kubectl"]) assert(stdout.length > 0, "lines should have printed") @@ -15,7 +15,7 @@ Deno.test("env", { sanitizeResources: false, sanitizeOps: false }, async () => { Deno.test("dry-run", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness() - await run(["--dry-run","+kubernetes.io/kubectl"]) + await run(["--dry-run", "+kubernetes.io/kubectl"]) // TODO: try to capture "imagined text" @@ -26,7 +26,7 @@ Deno.test("dry-run", { sanitizeResources: false, sanitizeOps: false }, async () Deno.test("prefix", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness() - const { stdout } = await run(["--prefix"]) + const { stdout } = await run(["--prefix"]) assert(stdout.length > 0, "lines should have printed") assertEquals(stdout[0], TEA_PREFIX.string) @@ -35,7 +35,7 @@ Deno.test("prefix", { sanitizeResources: false, sanitizeOps: false }, async () = Deno.test("version", { sanitizeResources: false, sanitizeOps: false }, async () => { const { run } = await createTestHarness() - const { stdout } = await run(["--version"]) + const { stdout } = await run(["--version"]) assert(stdout.length > 0, "lines should have printed") assert(stdout[0].startsWith("tea")) @@ -43,7 +43,7 @@ Deno.test("version", { sanitizeResources: false, sanitizeOps: false }, async () Deno.test("help", { sanitizeResources: false, sanitizeOps: false }, async () => { const { run } = await createTestHarness() - const { stdout } = await run(["--help"]) + const { stdout } = await run(["--help"]) assert(stdout.length > 0, "lines should have printed") assert(!stdout[0].includes("alt. modes:")) @@ -52,9 +52,19 @@ Deno.test("help", { sanitizeResources: false, sanitizeOps: false }, async () => Deno.test("help verbose", { sanitizeResources: false, sanitizeOps: false }, async () => { const { run } = await createTestHarness() - const { stdout } = await run(["--verbose", "--help"]) + const { stdout } = await run(["--verbose", "--help"]) assert(stdout.length > 0, "lines should have printed") assert(stdout[0].includes("alt. modes:")) assert(stdout[0].includes("ideology:")) }) + +Deno.test("tea +zlib.net --json", async () => { + const { run } = await createTestHarness() + await run(["--json", "+zlib.net", "true"]) +}) + +Deno.test("`tea +foo.com --json` errors neatly", async function() { + const { run } = await createTestHarness() + assertRejects(() => run(["--json", "+foo.com", "true"])) +}) From d6e6053619581bed902fb10ba6943b7ce8d47dc4 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 15:52:42 -0400 Subject: [PATCH 11/23] wip --- src/app.exec.ts | 2 +- src/hooks/useConfig.ts | 1 + src/hooks/useRun.ts | 31 +++++++------------------------ tests/functional/devenv.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/app.exec.ts b/src/app.exec.ts index c16f8b3b7..15f09ed04 100644 --- a/src/app.exec.ts +++ b/src/app.exec.ts @@ -82,7 +82,7 @@ export async function repl(installations: Installation[], env: Record): Verbosity { if (isNumber(verbosity)) { return verbosity } else if (parseBool(CI)) { + // prevents dumping 100s of lines of download progress return Verbosity.quiet } else { return Verbosity.normal diff --git a/src/hooks/useRun.ts b/src/hooks/useRun.ts index 0a724605f..23def75bb 100644 --- a/src/hooks/useRun.ts +++ b/src/hooks/useRun.ts @@ -1,11 +1,8 @@ -import { isArray } from "is-what" import { Path } from "tea" export interface RunOptions extends Omit { - cmd: (string | Path)[] | Path - cwd?: (string | Path) + cmd: (string | Path)[] clearEnv?: boolean //NOTE might not be cross platform! - spin?: boolean // hide output unless an error occurs } export class RunError extends Error { @@ -16,37 +13,23 @@ export class RunError extends Error { } } -export default async function useRun({ spin, ...opts }: RunOptions) { - const cmd = isArray(opts.cmd) ? opts.cmd.map(x => `${x}`) : [opts.cmd.string] - const cwd = opts.cwd?.toString() - console.log({ cwd, ...opts, cmd }) - +export default async function useRun(opts: RunOptions) { + const cmd = opts.cmd.map(x => `${x}`) const stdio = { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' } as Pick - if (spin) { - stdio.stderr = stdio.stdout = 'piped' - } - let proc: Deno.ChildProcess | undefined + console.log({ ...opts, cmd }) + try { - proc = _internals.nativeRun(cmd.shift()!, { ...opts, args: cmd, cwd, ...stdio }).spawn() + const proc = _internals.nativeRun(cmd.shift()!, { ...opts, args: cmd, ...stdio }).spawn() const exit = await proc.status console.log({ exit }) if (!exit.success) throw new RunError(exit.code, cmd) } catch (err) { - if (spin && proc) { - //FIXME this doesn’t result in the output being correctly interlaced - // ie. stderr and stdout may (probably) have been output interleaved rather than sequentially - const decode = (() => { const e = new TextDecoder(); return e.decode.bind(e) })() - console.error(decode((await proc.output()).stdout)) - console.error(decode((await proc.output()).stderr)) - } - - err.cmd = cmd // help us out since deno-devs don’t want to + err.cmd = cmd throw err } } - const nativeRun = (cmd: string, opts: Deno.CommandOptions) => new Deno.Command(cmd, opts) // _internals are used for testing diff --git a/tests/functional/devenv.test.ts b/tests/functional/devenv.test.ts index cce7f51a0..7e9223b7a 100644 --- a/tests/functional/devenv.test.ts +++ b/tests/functional/devenv.test.ts @@ -181,6 +181,29 @@ Deno.test("should provide ruby in dev env", { sanitizeResources: false, sanitize } }) +Deno.test("TEA_DIR", { sanitizeResources: false, sanitizeOps: false }, async test => { + const SHELL = "/bin/zsh" + + const tests = [ + { file: ".ruby-version", pkg: "ruby-lang.org>=3.2.1<3.2.2" } + ] + + for (const { file, pkg } of tests) { + await test.step(file, async () => { + const { run, teaDir } = await createTestHarness() + const foo = teaDir.join("foo").mkdir() + + fixturesDir.join(file).cp({ into: foo }) + const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, TEA_DIR: foo, obj: {} } }) + + const output = getTeaPackages(SHELL, stdout) + assert(output.includes(pkg), "should include ruby dep") + }) + } +}) + +////////////////////// utils ////////////////////// + function getEnvVar(shell: string, lines: string[], key: string): string | null { const pattern = () => { switch (shell) { From 5ceab5ebcccc190296c4d5f4bf58aa18293c4fce Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 16:08:01 -0400 Subject: [PATCH 12/23] wip --- deno.jsonc | 1 - tests/functional/run.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index a158f5bf3..50d94d7da 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -8,7 +8,6 @@ "coverage" : "scripts/run_coverage.sh", "typecheck": "deno check --unstable ./src/app.ts", // runs this source checkout for testing - // NOTE this doesn't currently work due (our bug) "run": "deno run --unstable --allow-all src/app.ts", // compiles to ./tea "compile": "deno compile --allow-read --allow-write --allow-net --allow-run --allow-env --unstable --output $INIT_CWD/tea src/app.ts", diff --git a/tests/functional/run.test.ts b/tests/functional/run.test.ts index 256bda284..ed3b075f9 100644 --- a/tests/functional/run.test.ts +++ b/tests/functional/run.test.ts @@ -61,10 +61,10 @@ Deno.test("help verbose", { sanitizeResources: false, sanitizeOps: false }, asyn Deno.test("tea +zlib.net --json", async () => { const { run } = await createTestHarness() - await run(["--json", "+zlib.net", "true"]) + await run(["--json", "+zlib.net"]) }) Deno.test("`tea +foo.com --json` errors neatly", async function() { const { run } = await createTestHarness() - assertRejects(() => run(["--json", "+foo.com", "true"])) + assertRejects(() => run(["--json", "+foo.com"])) }) From 7b3b5c9d1a43ea27086b14b3936c3e8bd33b288d Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 16:29:36 -0400 Subject: [PATCH 13/23] wip --- tests/functional/run.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/functional/run.test.ts b/tests/functional/run.test.ts index ed3b075f9..55f9a30b4 100644 --- a/tests/functional/run.test.ts +++ b/tests/functional/run.test.ts @@ -15,7 +15,7 @@ Deno.test("env", { sanitizeResources: false, sanitizeOps: false }, async () => { Deno.test("dry-run", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness() - await run(["--dry-run", "+kubernetes.io/kubectl"]) + await run(["--dry-run", "+kubernetes.io/kubectl", "foo", "bar"]) // TODO: try to capture "imagined text" @@ -68,3 +68,13 @@ Deno.test("`tea +foo.com --json` errors neatly", async function() { const { run } = await createTestHarness() assertRejects(() => run(["--json", "+foo.com"])) }) + +Deno.test("tea +zlib.net --verbose", async () => { + const { run } = await createTestHarness() + await run(["--verbose", "+zlib.net"]) +}) + +Deno.test("tea +zlib.net --cd /tmp", async () => { + const { run } = await createTestHarness() + await run(["--cd", "/tmp", "+zlib.net"]) +}) From 2f76d25f20598deaac8c57c691e750c21bee3d0c Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 May 2023 18:12:08 -0400 Subject: [PATCH 14/23] wip --- tests/functional/run.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/functional/run.test.ts b/tests/functional/run.test.ts index 55f9a30b4..8c630d306 100644 --- a/tests/functional/run.test.ts +++ b/tests/functional/run.test.ts @@ -78,3 +78,13 @@ Deno.test("tea +zlib.net --cd /tmp", async () => { const { run } = await createTestHarness() await run(["--cd", "/tmp", "+zlib.net"]) }) + +Deno.test("tea --env --keep-going", async () => { + const { run } = await createTestHarness() + await run(["--env", "--keep-going"]) +}) + +Deno.test("usage error", async () => { + const { run } = await createTestHarness() + assertRejects(() => run(["--invalid-option"])) +}) From 1b98ec8e9f05d5fa55145195ffe11bab50c76ea5 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 08:07:46 -0400 Subject: [PATCH 15/23] wip --- src/app.help.ts | 1 + src/app.main.ts | 17 ++++- src/hooks/useExec.install.ts | 139 ++++++++++++++++++++++++++++++++++ src/hooks/useExec.ts | 140 +---------------------------------- 4 files changed, 157 insertions(+), 140 deletions(-) create mode 100644 src/hooks/useExec.install.ts diff --git a/src/app.help.ts b/src/app.help.ts index c1e2c7470..e60c660c7 100644 --- a/src/app.help.ts +++ b/src/app.help.ts @@ -42,6 +42,7 @@ export default async function help(verbosity = Verbosity.normal) { --dry-run,-n don’t do anything, just print --keep-going,-k keep going as much as possible after errors --verbose,-v print version and then increase verbosity † + --quiet,-q status messages are more concise --silent,-s no chat, no errors: only output the requested data --cd,-C,--chdir change directory first --chaste abstain from networking, installing packages, etc. diff --git a/src/app.main.ts b/src/app.main.ts index bb38eb0ca..902c1c75d 100644 --- a/src/app.main.ts +++ b/src/app.main.ts @@ -1,4 +1,4 @@ -import { usePrefix, useExec, useVirtualEnv, useVersion, usePrint, useConfig } from "hooks" +import { usePrefix, useExec, useVirtualEnv, useVersion, usePrint, useConfig, useLogger } from "hooks" import { VirtualEnv } from "./hooks/useVirtualEnv.ts" import { Verbosity } from "./hooks/useConfig.ts" import { Path, utils, semver, hooks } from "tea" @@ -23,7 +23,20 @@ export async function run(args: Args) { } if (args.sync) { - await useSync() + const logger = (({ new: make, logJSON }) => { + if (!json) { + const logger = make() + return { + syncing: () => logger.replace("syncing pantries…"), + syncd: () => logger.replace("]pantries sync’d ⎷") + } + } else return { + syncing: () => logJSON({status: "syncing"}), + syncd: () => logJSON({status: "syncd"}) + } + })(useLogger()) + + await useSync(logger) } switch (args.mode) { diff --git a/src/hooks/useExec.install.ts b/src/hooks/useExec.install.ts new file mode 100644 index 000000000..85b47e8c8 --- /dev/null +++ b/src/hooks/useExec.install.ts @@ -0,0 +1,139 @@ +import { PackageSpecification, Package, utils, Installation, prefab } from "tea" +const { hydrate, link, resolve, install } = prefab +import { ExitError } from "./useErrorHandler.ts" +import useConfig from "./useConfig.ts" +import useLogger from "./useLogger.ts" +import undent from "outdent" + +export default async function(pkgs: PackageSpecification[], update: boolean) { + const { modifiers: { json, dryrun, verbosity }, env, prefix } = useConfig() + const logger = useLogger().new() + const { logJSON, gray, teal } = useLogger() + + if (!json) { + logger.replace("resolving package graph") + } else { + logJSON({ status: "resolving" }) + } + + const pkg_prefix_str = (pkg: Package) => [ + gray(prefix.prettyString()), + pkg.project, + `${gray('v')}${pkg.version}` + ].join(gray('/')) + + console.debug({hydrating: pkgs}) + + const { pkgs: wet, dry } = await hydrate(pkgs) + const {installed, pending} = await resolve(wet, { update }) + logger.clear() + + if (json) { + logJSON({ status: "resolved", pkgs: pending.map(utils.pkg.str) }) + } + + if (!dryrun && env.TEA_MAGIC?.split(':').includes("prompt")) { + if (!Deno.isatty(Deno.stdin.rid)) { + throw new Error("TEA_MAGIC=prompt but stdin is not a tty") + } + + do { + const val = prompt(undent` + ┌ ⚠️ tea requests to install: ${pending.map(utils.pkg.str).join(", ")} + └ \x1B[1mallow?\x1B[0m [y/n]` + )?.toLowerCase() + + if (val === "y") { + break + } + if (val === "n") { + throw new ExitError(1) + } + } while (true) + } + + for (const pkg of pending) { + const log_install_msg = (pkg: Package, title = 'installed') => { + if (json) { + logJSON({status: title, pkg: utils.pkg.str(pkg)}) + } else { + const str = pkg_prefix_str(pkg) + logger!.replace(`${title}: ${str}`, { prefix: false }) + } + } + + let installation: Installation + + if (dryrun) { + installation = { pkg, path: prefix.join(pkg.project, `v${pkg.version}`) } + log_install_msg(pkg, 'imagined') + } else { + let bytes = 0 + const timestamp = Date.now() + installation = await install(pkg, { + locking: () => { + if (!json) { + logger.replace(teal("locking")) + } else { + logJSON({status: "locking", pkg: utils.pkg.str(pkg) }) + } + }, + /// raw http info + downloading: ({pkg, src, dst, rcvd, total}) => { + if (json) { + logJSON({status: "downloading", "received": rcvd, "content-size": total, pkg, src, dst }) + } else if (verbosity >= 0) { + bytes = rcvd ?? 0 + } else if (total) { + logger.replace(`installing: ${pkg_prefix_str(pkg)} (${pretty_size(total)})`) + } else { + logger.replace(`installing: ${pkg_prefix_str(pkg)}`) + } + }, + installing: ({pkg, progress}) => { + if (json) { + logJSON({status: "installing", pkg, progress }) + } else if (verbosity >= 0) { + let s = teal("installing") + let pc = (progress ?? 0) * 100; + pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5% + + s += ` ${pc}%`.padEnd(4, ' ') + + const duration = Date.now() - timestamp + if (duration > 0) { + const speed = bytes / duration * 1000 + let dl = pretty_size(speed) + dl += "/s" + s += ` ${gray(dl)}` + } + + logger.replace(s) + } + }, + unlocking: (pkg: Package) => { + if (json) logJSON({status: "unlocking", pkg: utils.pkg.str(pkg) }) + }, + installed: (installation: Installation) => { + log_install_msg(installation.pkg) + } + }) + await link(installation) + } + + installed.push(installation) + } + + return { installed, dry } +} + +function pretty_size(n: number) { + const units = ["B", "KiB", "MiB", "GiB", "TiB"] + let i = 0 + while (n > 1024 && i < units.length - 1) { + n /= 1024 + i++ + } + const precision = n < 10 ? 2 : n < 100 ? 1 : 0 + return `${n.toFixed(precision)} ${units[i]}` +} diff --git a/src/hooks/useExec.ts b/src/hooks/useExec.ts index eb4e0f53b..942237697 100644 --- a/src/hooks/useExec.ts +++ b/src/hooks/useExec.ts @@ -1,13 +1,11 @@ import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError, Package } from "tea" import { usePackageYAMLFrontMatter } from "./usePackageYAML.ts" -import { ExitError } from "./useErrorHandler.ts" import { VirtualEnv } from "./useVirtualEnv.ts" +import install from "./useExec.install.ts" import useConfig from "./useConfig.ts" -import useLogger from "./useLogger.ts" -import undent from "outdent" const { usePantry, useCellar, useDownload, useShellEnv } = hooks -const { hydrate, resolve, install: base_install, link } = prefab +const { hydrate } = prefab interface Parameters { args: string[] @@ -113,130 +111,7 @@ export default async function({ pkgs, inject, sync, ...opts }: Parameters) { ///////////////////////////////////////////////////////////////////////////// funcs -async function install(pkgs: PackageSpecification[], update: boolean) { - const { modifiers: { json, dryrun, verbosity }, env } = useConfig() - const logger = useLogger().new() - const { logJSON, gray, teal } = useLogger() - - if (!json) { - logger.replace("resolving package graph") - } else { - logJSON({ status: "resolving" }) - } - - const pkg_prefix_str = (pkg: Package) => [ - gray(usePrefix().prettyString()), - pkg.project, - `${gray('v')}${pkg.version}` - ].join(gray('/')) - - console.debug({hydrating: pkgs}) - - const { pkgs: wet, dry } = await hydrate(pkgs) - const {installed, pending} = await resolve(wet, { update }) - logger.clear() - - if (json) { - logJSON({ status: "resolved", pkgs: pending.map(utils.pkg.str) }) - } - - if (!dryrun && env.TEA_MAGIC?.split(':').includes("prompt")) { - if (!Deno.isatty(Deno.stdin.rid)) { - throw new Error("TEA_MAGIC=prompt but stdin is not a tty") - } - - do { - const val = prompt(undent` - ┌ ⚠️ tea requests to install: ${pending.map(utils.pkg.str).join(", ")} - └ \x1B[1mallow?\x1B[0m [y/n]` - )?.toLowerCase() - - if (val === "y") { - break - } - if (val === "n") { - throw new ExitError(1) - } - } while (true) - } - - for (const pkg of pending) { - const log_install_msg = (pkg: Package, title = 'installed') => { - if (json) { - logJSON({status: title, pkg: utils.pkg.str(pkg)}) - } else { - const str = pkg_prefix_str(pkg) - logger!.replace(`${title}: ${str}`, { prefix: false }) - } - } - - let install: Installation - - if (dryrun) { - install = { pkg, path: usePrefix().join(pkg.project, `v${pkg.version}`) } - log_install_msg(pkg, 'imagined') - } else { - let bytes = 0 - const timestamp = Date.now() - install = await base_install(pkg, { - locking: () => { - if (!json) { - logger.replace(teal("locking")) - } else { - logJSON({status: "locking", pkg: utils.pkg.str(pkg) }) - } - }, - /// raw http info - downloading: ({pkg, src, dst, rcvd, total}) => { - if (json) { - logJSON({status: "downloading", "received": rcvd, "content-size": total, pkg, src, dst }) - } else if (verbosity >= 0) { - bytes = rcvd ?? 0 - } else if (total) { - logger.replace(`installing: ${pkg_prefix_str(pkg)} (${pretty_size(total)})`) - } else { - logger.replace(`installing: ${pkg_prefix_str(pkg)}`) - } - }, - installing: ({pkg, progress}) => { - if (json) { - logJSON({status: "installing", pkg, progress }) - } else if (verbosity >= 0) { - let s = teal("installing") - let pc = (progress ?? 0) * 100; - pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5% - - s += ` ${pc}%`.padEnd(4, ' ') - - const duration = Date.now() - timestamp - if (duration > 0) { - const speed = bytes / duration * 1000 - let dl = pretty_size(speed) - dl += "/s" - s += ` ${gray(dl)}` - } - - logger.replace(s) - } - }, - unlocking: (pkg: Package) => { - if (json) logJSON({status: "unlocking", pkg: utils.pkg.str(pkg) }) - }, - installed: (installation: Installation) => { - log_install_msg(installation.pkg) - } - }) - await link(install) - } - - installed.push(install) - } - - return { installed, dry } -} - import { readLines } from "deno/io/read_lines.ts" -import { usePrefix } from "./index.ts"; async function read_shebang(path: Path): Promise { const f = await Deno.open(path.string, { read: true }) @@ -401,14 +276,3 @@ export async function which(arg0: string | undefined) { const subst = function(start: number, end: number, input: string, what: string) { return input.substring(0, start) + what + input.substring(end) } - -function pretty_size(n: number) { - const units = ["B", "KiB", "MiB", "GiB", "TiB"] - let i = 0 - while (n > 1024 && i < units.length - 1) { - n /= 1024 - i++ - } - const precision = n < 10 ? 2 : n < 100 ? 1 : 0 - return `${n.toFixed(precision)} ${units[i]}` -} From e86131aff04dee3219952459e23d18f35b4fab65 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 10:54:27 -0400 Subject: [PATCH 16/23] wip --- deno.jsonc | 6 +-- src/hooks/useExec.ts | 2 +- tests/functional/suggestions.test.ts | 3 +- tests/functional/testUtils.ts | 31 +++++++---- tests/integration.suite.ts | 74 +++++++++++++++------------ tests/integration/package.yml.test.ts | 1 - 6 files changed, 70 insertions(+), 47 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 50d94d7da..be0b9c29a 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -31,10 +31,10 @@ } }, "imports": { - "tea": "https://raw.github.com/teaxyz/lib/v0.1.2/mod.ts", - "tea/": "https://raw.github.com/teaxyz/lib/v0.1.2/src/", + "tea": "https://raw.github.com/teaxyz/lib/v0.1.3/mod.ts", + "tea/": "https://raw.github.com/teaxyz/lib/v0.1.3/src/", "hooks": "./src/hooks/index.ts", - "deno/": "https://deno.land/std@0.182.0/", + "deno/": "https://deno.land/std@0.187.0/", "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts", diff --git a/src/hooks/useExec.ts b/src/hooks/useExec.ts index 942237697..5bab3d130 100644 --- a/src/hooks/useExec.ts +++ b/src/hooks/useExec.ts @@ -1,4 +1,4 @@ -import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError, Package } from "tea" +import { prefab, utils, hooks, PackageSpecification, Installation, PackageRequirement, Path, semver, TeaError } from "tea" import { usePackageYAMLFrontMatter } from "./usePackageYAML.ts" import { VirtualEnv } from "./useVirtualEnv.ts" import install from "./useExec.install.ts" diff --git a/tests/functional/suggestions.test.ts b/tests/functional/suggestions.test.ts index 5f8138da1..b520c5cf7 100644 --- a/tests/functional/suggestions.test.ts +++ b/tests/functional/suggestions.test.ts @@ -5,7 +5,8 @@ import { TeaError, SemVer } from "tea" Deno.test("suggestions", { sanitizeResources: false, sanitizeOps: false }, async test => { // suggestions need a sync to occur first - await createTestHarness({sync: true}) + const { run } = await createTestHarness({sync: true}) + run(["-Sh"]) // or test fails due to lack of config being set await test.step("suggest package name", async () => { const err = new TeaError("not-found: pantry: package.yml", { project: "node" }) diff --git a/tests/functional/testUtils.ts b/tests/functional/testUtils.ts index bb767746b..fea614057 100644 --- a/tests/functional/testUtils.ts +++ b/tests/functional/testUtils.ts @@ -1,11 +1,12 @@ -import { Config, ConfigDefault } from "../../src/hooks/useConfig.ts" +import useConfig, { Config, ConfigDefault } from "../../src/hooks/useConfig.ts" import { _internals as usePrintInternals } from "../../src/hooks/usePrint.ts" import { _internals as useRunInternals } from "../../src/hooks/useRun.ts" -import { spy } from "deno/testing/mock.ts" +import { _internals as useConfigInternals } from "tea/hooks/useConfig.ts" import { parseArgs } from "../../src/args.ts" import { run } from "../../src/app.main.ts" -import { Path, hooks } from "tea" -const { useConfig } = hooks +import { spy } from "deno/testing/mock.ts" +import { Path, utils } from "tea" +const { panic } = utils export interface TestConfig { // run tea sync during test setup. Default: true @@ -22,12 +23,11 @@ export const createTestHarness = async (config?: TestConfig) => { const teaDir = tmpDir.join(dir).mkdir('p') const TEA_PREFIX = tmpDir.join('opt').mkdir() + let TEA_PANTRY_PATH: string | undefined + let TEA_CACHE_DIR = Path.home().join(".tea/tea.xyz/var/www").isDirectory()?.string if (sync) { - const [syncArgs, flags] = parseArgs(["--sync", "--silent"], teaDir.string) - const config = ConfigDefault(flags, teaDir.string, { NO_COLOR: '1', TEA_PREFIX: TEA_PREFIX.string }) - useConfig(config) - await run(syncArgs) + TEA_PANTRY_PATH = Path.home().join(".tea/tea.xyz/var/pantry").isDirectory()?.string ?? panic("setup tea before running these tests, k?") } const runTea = async (args: string[], configOverrides: Partial = {}) => { @@ -39,8 +39,18 @@ export const createTestHarness = async (config?: TestConfig) => { try { const [appArgs, flags] = parseArgs(args, teaDir.string) - const config = ConfigDefault(flags, teaDir.string, { NO_COLOR: '1', PATH: "/usr/bin:/bin", TEA_PREFIX: TEA_PREFIX.string, VERBOSE: '-1' }) + const env: Record = { + NO_COLOR: '1', + PATH: "/usr/bin:/bin", + VERBOSE: '-1', + TEA_PREFIX: TEA_PREFIX.string, + } + if (TEA_CACHE_DIR) env['TEA_CACHE_DIR'] = TEA_CACHE_DIR + if (TEA_PANTRY_PATH) env['TEA_PANTRY_PATH'] = TEA_PANTRY_PATH + const config = ConfigDefault(flags, teaDir.string, env) + + useConfigInternals.reset() useConfig({ ...config, ...configOverrides, @@ -48,6 +58,9 @@ export const createTestHarness = async (config?: TestConfig) => { await run(appArgs) + // ensure subsequent tests aren't polluted + useConfigInternals.reset() + } finally { usePrintSpy.restore() Deno.chdir(cwd) diff --git a/tests/integration.suite.ts b/tests/integration.suite.ts index 10564b4f8..e81d486af 100644 --- a/tests/integration.suite.ts +++ b/tests/integration.suite.ts @@ -6,6 +6,8 @@ interface This { tea: Path sandbox: Path TEA_PREFIX: Path + TEA_PANTRY_PATH: Path + TEA_CACHE_DIR: Path run: (opts: RunOptions) => Promise & Enhancements } @@ -16,7 +18,6 @@ type RunOptions = ({ }) & { env?: Record throws?: boolean - sync?: boolean } interface Enhancements { @@ -27,53 +28,68 @@ interface Enhancements { const existing_tea_prefix = Deno.env.get("CI") ? undefined : Path.home().join(".tea").isDirectory() const suite = describe({ + name: "integration tests", - async beforeEach(this: This) { + + async beforeAll(this: This) { const tmp = new Path(await Deno.makeTempDir({ prefix: "tea-" })) - const cwd = new Path(new URL(import.meta.url).pathname).parent().parent().string - const TEA_PREFIX = existing_tea_prefix ?? tmp.join('opt').mkdir() - const bin = tmp.join('bin').mkpath() + const cwd = new Path(new URL(import.meta.url).pathname).parent().parent() + //TODO use deno task compile, however seems to be a bug where we cannot control the output location const proc = Deno.run({ cmd: [ "deno", "compile", "--quiet", - "--allow-read", // restricting reads would be nice but Deno.symlink requires read permission to ALL - "--allow-write", // restricting writes would be nice but Deno.symlink requires write permission to ALL - "--allow-net", - "--allow-run", - "--allow-env", + "-A", "--unstable", - "--output", bin.join("tea").string, + "--output", tmp.join("tea").string, "src/app.ts" - ], cwd + ], cwd: cwd.string }) assert((await proc.status()).success) proc.close() - this.tea = bin.join("tea") + this.tea = tmp.join("tea") assert(this.tea.isExecutableFile()) + this.TEA_PANTRY_PATH = (existing_tea_prefix ?? await (async () => { + const proc = Deno.run({ + cmd: [this.tea.string, "--sync", "--silent"], + cwd: tmp.string, + env: { TEA_PREFIX: tmp.string }, + clearEnv: true + }) + assert((await proc.status()).success) + return tmp + })()).join("tea.xyz/var/pantry") + + this.TEA_CACHE_DIR = (existing_tea_prefix ?? tmp).join("tea.xyz/var/www") + }, + + async beforeEach(this: This) { + const tmp = new Path(await Deno.makeTempDir({ prefix: "tea-" })) + const TEA_PREFIX = existing_tea_prefix ?? tmp.join('opt').mkdir() + this.TEA_PREFIX = TEA_PREFIX assert(this.TEA_PREFIX.isDirectory()) this.sandbox = tmp.join("box").mkdir() - const teafile = bin.join('tea') const { sandbox } = this - this.run = ({env, throws, sync, ...opts}: RunOptions) => { - sync ??= true + this.run = ({env, throws, ...opts}: RunOptions) => { env ??= {} for (const key of ['HOME', 'CI', 'RUNNER_DEBUG', 'GITHUB_ACTIONS']) { const value = Deno.env.get(key) if (value) env[key] = value } - env['PATH'] = `${bin}:/usr/bin:/bin` // these systems are full of junk so we prune PATH + env['PATH'] = `${this.tea.parent()}:/usr/bin:/bin` // these systems are full of junk so we prune PATH env['TEA_PREFIX'] ??= TEA_PREFIX.string - env['CLICOLOR_FORCE'] = '1' + env['CLICOLOR_FORCE'] = '0' + env['TEA_PANTRY_PATH'] ??= this.TEA_PANTRY_PATH.string + env['TEA_CACHE_DIR'] ??= this.TEA_CACHE_DIR.string let stdout: "piped" | undefined let stderr: "piped" | undefined @@ -85,19 +101,8 @@ const suite = describe({ ? [...opts.args] : [...opts.cmd] - - //TODO we typically don’t want silent, we just want ERRORS-ONLY - if ("args" in opts) { - if (!existing_tea_prefix && sync) { - cmd.unshift("--sync") - } - cmd.unshift(teafile.string) - } else if (cmd[0] != 'tea') { - // we need to do an initial --sync - const arg = sync ? "-Ss" : "-s" - const proc = Deno.run({ cmd: [teafile.string, arg], cwd: sandbox.string, env, clearEnv: true }) - assertEquals((await proc.status()).code, 0) - proc.close() + if (cmd[0] != "tea") { + cmd.unshift(this.tea.string) } const proc = Deno.run({ cmd, cwd: sandbox.string, stdout, stderr, env, clearEnv: true}) @@ -150,9 +155,14 @@ const suite = describe({ return p } }, + afterEach() { - // this.TEA_PREFIX.parent().rm({ recursive: true }) + this.sandbox.parent().rm({ recursive: true }) }, + + afterAll() { + this.tea.parent().rm({ recursive: true }) + } }) export default suite diff --git a/tests/integration/package.yml.test.ts b/tests/integration/package.yml.test.ts index 26b5e1a63..4642e76d8 100644 --- a/tests/integration/package.yml.test.ts +++ b/tests/integration/package.yml.test.ts @@ -26,7 +26,6 @@ it(suite, "runtime.env tildes", async function() { TEA_PANTRY_PATH: this.sandbox.string, TEA_PREFIX: this.sandbox.string }, - sync: false }).stdout() assertEquals(out.trim(), FOO.replaceAll("{{home}}", Path.home().string)) From cfb9f7891653183bf553439bb921f4da00d574fc Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 11:47:58 -0400 Subject: [PATCH 17/23] wip --- tests/functional/testUtils.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/functional/testUtils.ts b/tests/functional/testUtils.ts index fea614057..9cf201f89 100644 --- a/tests/functional/testUtils.ts +++ b/tests/functional/testUtils.ts @@ -5,7 +5,8 @@ import { _internals as useConfigInternals } from "tea/hooks/useConfig.ts" import { parseArgs } from "../../src/args.ts" import { run } from "../../src/app.main.ts" import { spy } from "deno/testing/mock.ts" -import { Path, utils } from "tea" +import { Path, utils, hooks } from "tea" +const { useSync } = hooks const { panic } = utils export interface TestConfig { @@ -27,7 +28,7 @@ export const createTestHarness = async (config?: TestConfig) => { let TEA_CACHE_DIR = Path.home().join(".tea/tea.xyz/var/www").isDirectory()?.string if (sync) { - TEA_PANTRY_PATH = Path.home().join(".tea/tea.xyz/var/pantry").isDirectory()?.string ?? panic("setup tea before running these tests, k?") + TEA_PANTRY_PATH = (Path.home().join(".tea/tea.xyz/var/pantry").isDirectory() ?? await mkpantry()).string } const runTea = async (args: string[], configOverrides: Partial = {}) => { @@ -99,3 +100,14 @@ export function newMockProcess(status?: () => Promise): Deno }) } } + +let __pantry: Path | undefined +async function mkpantry() { + if (__pantry) return __pantry + const tmp = new Path(await Deno.makeTempDir({ prefix: "tea.functional-tests." })) + useConfigInternals.reset() + useConfig(ConfigDefault(undefined, tmp.join('tea').string, { TEA_PREFIX: tmp.string })) + await useSync() + useConfigInternals.reset() + return __pantry = tmp.join("tea.xyz/var/pantry") +} From 3e984b4069de7d1f3a231008716bdbd9d4176ee4 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 11:59:02 -0400 Subject: [PATCH 18/23] wip --- deno.jsonc | 10 +++++----- tests/integration.suite.ts | 1 + tests/integration/package.yml.test.ts | 2 -- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index be0b9c29a..280bd0f6e 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -31,13 +31,13 @@ } }, "imports": { + "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", + "jsonc": "https://deno.land/x/jsonc_parser@v0.0.1/mod.ts", "tea": "https://raw.github.com/teaxyz/lib/v0.1.3/mod.ts", "tea/": "https://raw.github.com/teaxyz/lib/v0.1.3/src/", - "hooks": "./src/hooks/index.ts", - "deno/": "https://deno.land/std@0.187.0/", - "is-what": "https://deno.land/x/is_what@v4.1.8/src/index.ts", - "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts", - "jsonc": "https://deno.land/x/jsonc_parser@v0.0.1/mod.ts" + "cliffy/": "https://deno.land/x/cliffy@v0.25.7/", + "deno/": "https://deno.land/std@0.187.0/", + "hooks": "./src/hooks/index.ts" } } diff --git a/tests/integration.suite.ts b/tests/integration.suite.ts index e81d486af..2c3eb1d5a 100644 --- a/tests/integration.suite.ts +++ b/tests/integration.suite.ts @@ -62,6 +62,7 @@ const suite = describe({ clearEnv: true }) assert((await proc.status()).success) + proc.close() return tmp })()).join("tea.xyz/var/pantry") diff --git a/tests/integration/package.yml.test.ts b/tests/integration/package.yml.test.ts index 4642e76d8..6545d2104 100644 --- a/tests/integration/package.yml.test.ts +++ b/tests/integration/package.yml.test.ts @@ -18,8 +18,6 @@ it(suite, "runtime.env tildes", async function() { echo "$FOO" `, force: true}).chmod(0o755) - console.log(this.sandbox, this.sandbox.isDirectory()) - const out = await this.run({ args: ["foo"], env: { From fd7b9e24e9385eb4914865358e9d45f1d4e3d314 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 12:29:24 -0400 Subject: [PATCH 19/23] wip --- src/hooks/useVirtualEnv.ts | 4 ++-- tests/functional/devenv.test.ts | 5 +++-- tests/functional/sync.test.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hooks/useVirtualEnv.ts b/src/hooks/useVirtualEnv.ts index 5cc777425..5c2747d8b 100644 --- a/src/hooks/useVirtualEnv.ts +++ b/src/hooks/useVirtualEnv.ts @@ -17,9 +17,9 @@ export interface VirtualEnv { // we call into useVirtualEnv a bunch of times const cache: Record = {} -export default async function(cwd?: Path): Promise { +export default async function(cwd: Path): Promise { const { TEA_DIR } = useConfig().env - cwd = TEA_DIR ?? cwd ?? Path.cwd() + cwd = TEA_DIR ?? cwd if (cache[cwd.string]) return cache[cwd.string] diff --git a/tests/functional/devenv.test.ts b/tests/functional/devenv.test.ts index 7e9223b7a..fdb1088e5 100644 --- a/tests/functional/devenv.test.ts +++ b/tests/functional/devenv.test.ts @@ -181,7 +181,7 @@ Deno.test("should provide ruby in dev env", { sanitizeResources: false, sanitize } }) -Deno.test("TEA_DIR", { sanitizeResources: false, sanitizeOps: false }, async test => { +Deno.test("TEA_DIR & TEA_PKGS", { sanitizeResources: false, sanitizeOps: false }, async test => { const SHELL = "/bin/zsh" const tests = [ @@ -192,9 +192,10 @@ Deno.test("TEA_DIR", { sanitizeResources: false, sanitizeOps: false }, async tes await test.step(file, async () => { const { run, teaDir } = await createTestHarness() const foo = teaDir.join("foo").mkdir() + const TEA_PKGS = "deno.land^1" fixturesDir.join(file).cp({ into: foo }) - const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, TEA_DIR: foo, obj: {} } }) + const { stdout } = await run(["+tea.xyz/magic", "-Esk", "--chaste", "env"], { env: { SHELL, TEA_DIR: foo, TEA_PKGS, obj: {} } }) const output = getTeaPackages(SHELL, stdout) assert(output.includes(pkg), "should include ruby dep") diff --git a/tests/functional/sync.test.ts b/tests/functional/sync.test.ts index 8fec3dee1..b1a9d7f61 100644 --- a/tests/functional/sync.test.ts +++ b/tests/functional/sync.test.ts @@ -52,7 +52,7 @@ Deno.test("sync without git then update with git", { sanitizeResources: false, s Deno.test("sync with git then update without", { sanitizeResources: false, sanitizeOps: false }, async () => { const {run, TEA_PREFIX } = await createTestHarness({sync: false}) - await run(["-S", "+zlib.net"]) + await run(["-S", "--json", "+zlib.net"]) const expected = TEA_PREFIX.join("zlib.net") assert(expected.exists(), "zlib.net should exist") From 2f59bfa5e661a8f2f62e4d11cdcbed7150a537af Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 13:54:32 -0400 Subject: [PATCH 20/23] wip --- src/hooks/useExec.install.ts | 169 ++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 72 deletions(-) diff --git a/src/hooks/useExec.install.ts b/src/hooks/useExec.install.ts index 85b47e8c8..975ca388c 100644 --- a/src/hooks/useExec.install.ts +++ b/src/hooks/useExec.install.ts @@ -1,14 +1,14 @@ import { PackageSpecification, Package, utils, Installation, prefab } from "tea" const { hydrate, link, resolve, install } = prefab +import useLogger, { Logger } from "./useLogger.ts" import { ExitError } from "./useErrorHandler.ts" import useConfig from "./useConfig.ts" -import useLogger from "./useLogger.ts" import undent from "outdent" export default async function(pkgs: PackageSpecification[], update: boolean) { - const { modifiers: { json, dryrun, verbosity }, env, prefix } = useConfig() + const { modifiers: { json, dryrun }, env, prefix } = useConfig() const logger = useLogger().new() - const { logJSON, gray, teal } = useLogger() + const { logJSON, gray } = useLogger() if (!json) { logger.replace("resolving package graph") @@ -16,20 +16,14 @@ export default async function(pkgs: PackageSpecification[], update: boolean) { logJSON({ status: "resolving" }) } - const pkg_prefix_str = (pkg: Package) => [ - gray(prefix.prettyString()), - pkg.project, - `${gray('v')}${pkg.version}` - ].join(gray('/')) - - console.debug({hydrating: pkgs}) - const { pkgs: wet, dry } = await hydrate(pkgs) const {installed, pending} = await resolve(wet, { update }) - logger.clear() if (json) { logJSON({ status: "resolved", pkgs: pending.map(utils.pkg.str) }) + } else { + logger.clear() + console.debug({hydrating: pkgs}) } if (!dryrun && env.TEA_MAGIC?.split(':').includes("prompt")) { @@ -53,71 +47,14 @@ export default async function(pkgs: PackageSpecification[], update: boolean) { } for (const pkg of pending) { - const log_install_msg = (pkg: Package, title = 'installed') => { - if (json) { - logJSON({status: title, pkg: utils.pkg.str(pkg)}) - } else { - const str = pkg_prefix_str(pkg) - logger!.replace(`${title}: ${str}`, { prefix: false }) - } - } - let installation: Installation + const logger = useLogger().new(gray(utils.pkg.str(pkg))) if (dryrun) { installation = { pkg, path: prefix.join(pkg.project, `v${pkg.version}`) } - log_install_msg(pkg, 'imagined') + log_installed_msg(pkg, 'imagined', logger) } else { - let bytes = 0 - const timestamp = Date.now() - installation = await install(pkg, { - locking: () => { - if (!json) { - logger.replace(teal("locking")) - } else { - logJSON({status: "locking", pkg: utils.pkg.str(pkg) }) - } - }, - /// raw http info - downloading: ({pkg, src, dst, rcvd, total}) => { - if (json) { - logJSON({status: "downloading", "received": rcvd, "content-size": total, pkg, src, dst }) - } else if (verbosity >= 0) { - bytes = rcvd ?? 0 - } else if (total) { - logger.replace(`installing: ${pkg_prefix_str(pkg)} (${pretty_size(total)})`) - } else { - logger.replace(`installing: ${pkg_prefix_str(pkg)}`) - } - }, - installing: ({pkg, progress}) => { - if (json) { - logJSON({status: "installing", pkg, progress }) - } else if (verbosity >= 0) { - let s = teal("installing") - let pc = (progress ?? 0) * 100; - pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5% - - s += ` ${pc}%`.padEnd(4, ' ') - - const duration = Date.now() - timestamp - if (duration > 0) { - const speed = bytes / duration * 1000 - let dl = pretty_size(speed) - dl += "/s" - s += ` ${gray(dl)}` - } - - logger.replace(s) - } - }, - unlocking: (pkg: Package) => { - if (json) logJSON({status: "unlocking", pkg: utils.pkg.str(pkg) }) - }, - installed: (installation: Installation) => { - log_install_msg(installation.pkg) - } - }) + installation = await i(pkg, logger) await link(installation) } @@ -127,6 +64,76 @@ export default async function(pkgs: PackageSpecification[], update: boolean) { return { installed, dry } } +async function i(pkg: Package, logger: Logger) { + const { modifiers: { json, verbosity } } = useConfig() + const { logJSON, teal, gray } = useLogger() + + let bytes = 0 + let content_size: number | undefined + const timestamp = Date.now() + + return await install(pkg, { + locking: () => { + if (!json) { + logger.replace(teal("locking")) + } else { + logJSON({status: "locking", pkg: utils.pkg.str(pkg) }) + } + }, + /// raw http info + downloading: ({pkg, src, dst, rcvd, total}) => { + if (json) { + logJSON({status: "downloading", "received": rcvd, "content-size": total, pkg, src, dst }) + } else if (verbosity >= 0) { + bytes = rcvd ?? 0 + content_size = total + } else if (total) { + content_size = total + logger.replace(`${teal('installing')} ${gray(pretty_size(total))}`) + } else { + logger.replace(`${teal('installing')}`) + } + }, + installing: ({pkg, progress}) => { + if (json) { + logJSON({status: "installing", pkg, progress }) + } else if (verbosity >= 0) { + let s = teal("installing") + + let pc = (progress ?? 0) * 100; + pc = pc < 1 ? Math.round(pc) : Math.floor(pc); // don’t say 100% at 99.5% + + s += ` ${pc}%`.padEnd(4, ' ') + + const duration = Date.now() - timestamp + if (duration > 0) { + const speed = bytes / duration * 1000 + let dl = pretty_size(speed) + dl += "/s" + s += ` ${gray(dl)}` + } + + if (content_size) { + const a = pretty_size(bytes) + const b = pretty_size(content_size) + s += ` ${gray(`${a}/${b}`)}` + } + + logger.replace(s) + } + }, + unlocking: (pkg: Package) => { + if (json) logJSON({status: "unlocking", pkg: utils.pkg.str(pkg) }) + }, + installed: (installation: Installation) => { + log_installed_msg(installation.pkg, 'installed', logger) + } + }) +} + + +////////////////// utils ////////////////// + function pretty_size(n: number) { const units = ["B", "KiB", "MiB", "GiB", "TiB"] let i = 0 @@ -137,3 +144,21 @@ function pretty_size(n: number) { const precision = n < 10 ? 2 : n < 100 ? 1 : 0 return `${n.toFixed(precision)} ${units[i]}` } + +const log_installed_msg = (pkg: Package, title: string, logger: Logger) => { + const { prefix, modifiers: { json } } = useConfig() + const { gray, logJSON } = useLogger() + + const pkg_prefix_str = (pkg: Package) => [ + gray(prefix.prettyString()), + pkg.project, + `${gray('v')}${pkg.version}` + ].join(gray('/')) + + if (json) { + logJSON({status: title, pkg: utils.pkg.str(pkg)}) + } else { + const str = pkg_prefix_str(pkg) + logger!.replace(`${title}: ${str}`, { prefix: false }) + } +} From 6dc4a241b913b9604f4165a75414c72425538ed6 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 13:57:03 -0400 Subject: [PATCH 21/23] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 101a9cbd7..e923a5536 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

-# tea/cli 0.32.1 +# tea/cli 0.33.0 `tea` puts the whole open source ecosystem at your fingertips: From 53e198e5b14b5cbe595e1f4672e1f0bc83eabf6e Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 13:59:18 -0400 Subject: [PATCH 22/23] wip --- src/hooks/useConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 432f8dc84..5e7e14379 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -40,7 +40,7 @@ export interface Config extends ConfigBase { export default function(input?: Config): Config { if (!_internals.initialized()) { - const rv = useConfig(input ?? panic("useConfig() not initialized")) as Config + const rv = useConfig(input ?? ConfigDefault()) as Config return rv } else { if (input) console.warn("useConfig() already initialized, new parameters ignored") From d70425a927ded7945ca550c3900f50d528a19ed5 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 23 May 2023 14:11:30 -0400 Subject: [PATCH 23/23] wip --- src/hooks/useConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 5e7e14379..41872743b 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -3,7 +3,7 @@ import { parseBool } from "../args.ts" import { Flags } from "../args.ts" import { isNumber } from "is-what" import { utils, Path } from "tea" -const { flatmap, panic } = utils +const { flatmap } = utils import useConfig, { Config as ConfigBase, ConfigDefault as ConfigBaseDefault, _internals } from "tea/hooks/useConfig.ts"