diff --git a/.github/deno-to-node.ts b/.github/deno-to-node.ts index 6ca4204..014e2e6 100755 --- a/.github/deno-to-node.ts +++ b/.github/deno-to-node.ts @@ -13,17 +13,17 @@ dependencies: npmjs.com: '*' ---*/ -import { build, emptyDir } from "https://deno.land/x/dnt@0.38.0/mod.ts"; -import SemVer from "../src/utils/semver.ts"; +import { build, emptyDir } from "https://deno.land/x/dnt@0.38.0/mod.ts" +import SemVer from "../src/utils/semver.ts" -await emptyDir("./dist"); +await emptyDir("./dist") const version = (() => { try { - return new SemVer(Deno.args[0]).toString(); + return new SemVer(Deno.args[0]).toString() } catch { console.warn("no version specified, do not release this!") - return '0.0.0' + return "0.0.0" } })() @@ -43,7 +43,7 @@ await build({ mappings: { "https://deno.land/x/is_what@v4.1.15/src/index.ts": "is-what", "https://deno.land/x/outdent@v0.8.0/mod.ts": "outdent", - "./src/utils/flock.deno.ts": "./src/utils/flock.node.ts" + "./src/utils/flock.deno.ts": "./src/utils/flock.node.ts", }, package: { name: "@teaxyz/lib", @@ -64,24 +64,24 @@ await build({ exports: { "./src/src/utils/semver": { //TODO remove when gui is updated to use `@teaxyz/lib/semver` - import: "./src/src/utils/semver.ts" + import: "./src/src/utils/semver.ts", }, "./semver": { import: "./esm/src/utils/semver.js", - require: "./script/src/utils/semver.js" + require: "./script/src/utils/semver.js", }, "./plumbing/*": { "import": "./esm/src/plumbing/*.js", - "require": "./script/src/plumbing/*.js" + "require": "./script/src/plumbing/*.js", }, "./hooks/*": { "import": "./esm/src/hooks/*.js", - "require": "./script/src/hooks/*.js" - } - } + "require": "./script/src/hooks/*.js", + }, + }, }, postBuild() { - Deno.copyFileSync("LICENSE.txt", "dist/LICENSE.txt"); - Deno.copyFileSync("README.md", "dist/README.md"); + Deno.copyFileSync("LICENSE.txt", "dist/LICENSE.txt") + Deno.copyFileSync("README.md", "dist/README.md") }, -}); +}) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3ad9aee..155491d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,20 +1,20 @@ { - "version": "0.2.0", - "configurations": [ - { - "request": "launch", - "name": "Debug Test", - "type": "node", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "deno", - "runtimeArgs": [ - "test", - "--unstable", - "--inspect-brk", - "--allow-all", - "${file}", - ], - "attachSimplePort": 9229 - } - ] + "version": "0.2.0", + "configurations": [ + { + "request": "launch", + "name": "Debug Test", + "type": "node", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "deno", + "runtimeArgs": [ + "test", + "--unstable", + "--inspect-brk", + "--allow-all", + "${file}" + ], + "attachSimplePort": 9229 + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index ce45680..9fe49ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,5 @@ "deno.enable": true, "deno.lint": true, "deno.unstable": true, - "deno.config": "deno.json" + "deno.config": "deno.jsonc" } diff --git a/README.md b/README.md index 64cfd7e..11f94fa 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,11 @@

- # libtea -tea aims to provide packaging primitives. This library is a route to that -goal. libtea can install and provide sandboxed environments for packages that -have no effect on the wider system without you or your user needing to install -[tea/cli]. +tea aims to provide packaging primitives. This library is a route to that goal. libtea can install +and provide sandboxed environments for packages that have no effect on the wider system without you +or your user needing to install [tea/cli]. ## Getting Started @@ -39,10 +37,10 @@ import * as tea from "https://deno.land/x/libtea/mod.ts" ## Usage ```ts -import { porcelain } from "@teaxyz/lib"; -const { run } = porcelain; +import { porcelain } from "@teaxyz/lib" +const { run } = porcelain -await run(`python -c 'print("Hello, World!")'`); +await run(`python -c 'print("Hello, World!")'`) // ^^ installs python and its deps (into ~/.tea/python.org/v3.x.y) // ^^ runs the command // ^^ output goes to the terminal @@ -53,73 +51,68 @@ await run(`python -c 'print("Hello, World!")'`); Capture stdout easily: ```ts -const { stdout } = await run(`ruby -e 'puts ", World!"'`, { stdout: true }); -console.log("Hello,", stdout); +const { stdout } = await run(`ruby -e 'puts ", World!"'`, { stdout: true }) +console.log("Hello,", stdout) ``` > `{ stderr: true }` also works. -If there’s a non-zero exit code, we `throw`. However, when you need to, -you can capture it instead: +If there’s a non-zero exit code, we `throw`. However, when you need to, you can capture it instead: ```ts -const { status } = await run(`perl -e 'exit(7)'`, { status: true }); -assert(status == 7); // ^^ didn’t throw! +const { status } = await run(`perl -e 'exit(7)'`, { status: true }) +assert(status == 7) // ^^ didn’t throw! ``` -> The run function’s options also takes `env` if you need to supplement or -> replace the inherited environment (which is passed by default). +> The run function’s options also takes `env` if you need to supplement or replace the inherited +> environment (which is passed by default). -Need a specific version of something? [tea][tea/cli] can install any version -of any package: +Need a specific version of something? [tea][tea/cli] can install any version of any package: ```ts -await run(["node^16", "-e", "console.log(process.version)"]); +await run(["node^16", "-e", "console.log(process.version)"]) // => v16.18.1 ``` -> Notice we passed args as `string[]`. This is also supported and is often -> preferable since shell quoting rules can be tricky. If you pass `string[]` -> we execute the command directly rather than via `/bin/sh`. +> Notice we passed args as `string[]`. This is also supported and is often preferable since shell +> quoting rules can be tricky. If you pass `string[]` we execute the command directly rather than +> via `/bin/sh`. -All of tea’s packages are relocatable so you can configure libtea to install -wherever you want: +All of tea’s packages are relocatable so you can configure libtea to install wherever you want: ```ts -import { hooks, Path, porcelain } from "tea"; -const { install } = porcelain; -const { useConfig } = hooks; +import { hooks, Path, porcelain } from "tea" +const { install } = porcelain +const { useConfig } = hooks -useConfig({ prefix: Path.home().join(".local/share/my-app") }); +useConfig({ prefix: Path.home().join(".local/share/my-app") }) // ^^ must be done **before** any other libtea calls -const go = await install("go.dev"); +const go = await install("go.dev") // ^^ go.path = /home/you/.local/share/my-app/go.dev/v1.20.4 ``` ### Designed for Composibility -The library is split into [plumbing](src/plumbing) and [porcelain](src/porcelain) (copying git’s lead). -The porcelain is what most people need, but if you need more control, dive -into the porcelain sources to see how to use the plumbing primitives to get -precisely what you need. +The library is split into [plumbing](src/plumbing) and [porcelain](src/porcelain) (copying git’s +lead). The porcelain is what most people need, but if you need more control, dive into the porcelain +sources to see how to use the plumbing primitives to get precisely what you need. -For example if you want to run a command with node’s `spawn` instead it is -simple enough to first use our porcelain `install` function then grab the -`env` you’ll need to pass to `spawn` using our `useShellEnv` hook. +For example if you want to run a command with node’s `spawn` instead it is simple enough to first +use our porcelain `install` function then grab the `env` you’ll need to pass to `spawn` using our +`useShellEnv` hook. Perhaps what you create should go into the porcelain? If so, please open a PR. ### Logging -Most functions take an optional `logger` parameter so you can output logging -information if you so choose. `tea/cli` has a fairly sophisticated logger, so -go check that out if you want. For our porcelain functions we provide a simple -debug-friendly logger (`ConsoleLogger`) that will output everything via -`console.error`: +Most functions take an optional `logger` parameter so you can output logging information if you so +choose. `tea/cli` has a fairly sophisticated logger, so go check that out if you want. For our +porcelain functions we provide a simple debug-friendly logger (`ConsoleLogger`) that will output +everything via `console.error`: ```ts -import { porcelain, plumbing, utils } from "tea" +import { plumbing, porcelain, utils } from "tea" const { ConsoleLogger } = utils const { run } = porcelain @@ -129,27 +122,23 @@ await run("youtube-dl youtu.be/xiq5euezOEQ", { logger }).exec() ### Caveats -We have our own implementation of semver because open source has existed for -decades and Semantic Versioning is much newer than that. Our implementation is -quite compatible but not completely so. Use our semver with libtea. -Our implementation is 100% compatible with strings output from node’s own -semver. +We have our own implementation of semver because open source has existed for decades and Semantic +Versioning is much newer than that. Our implementation is quite compatible but not completely so. +Use our semver with libtea. Our implementation is 100% compatible with strings output from node’s +own semver. -Setting `useConfig()` is not thread safe. Thus if you are using web workers -you must ensure the initial call to `useConfig()` is called on the main thread -before any other calls might happen. We call it explicitly in our code so you -will need to call it yourself in such a case. This is not ideal and we’d -appreciate your help in fixing it. +Setting `useConfig()` is not thread safe. Thus if you are using web workers you must ensure the +initial call to `useConfig()` is called on the main thread before any other calls might happen. We +call it explicitly in our code so you will need to call it yourself in such a case. This is not +ideal and we’d appreciate your help in fixing it. -The plumbing has no magic. Libraries need well defined behavior. -You’ll need to read the docs to use them effectively. +The plumbing has no magic. Libraries need well defined behavior. You’ll need to read the docs to use +them effectively. -libtea almost certainly will not work in a browser. Potentially it's possible. -The first step would be compiling our bottles to WASM. We could use your help -with that… +libtea almost certainly will not work in a browser. Potentially it's possible. The first step would +be compiling our bottles to WASM. We could use your help with that… -We use a hook-like pattern because it is great. This library is not itself -designed for React. +We use a hook-like pattern because it is great. This library is not itself designed for React. We support the same platforms as [tea/cli]. @@ -157,24 +146,21 @@ We support the same platforms as [tea/cli]. We can install anything in the [pantry]. -If something you need is not there, adding to the pantry has been designed to -be an easy and enjoyable process. Your contribution is both welcome and -desired! +If something you need is not there, adding to the pantry has been designed to be an easy and +enjoyable process. Your contribution is both welcome and desired! -To see what is available refer to the [pantry] docs or you can run: -`tea pkg search foo`. +To see what is available refer to the [pantry] docs or you can run: `tea pkg search foo`.   # Interesting Uses -* You can grab cURL’s CA certificates which we pkg and keep up to date - (`curl.se/ca-certs`). These are commonly needed across ecosystems but not - always easily accessible. -* grab libraries that wrappers need like openssl or sqlite -* run a real database (like postgres) easily -* load local AI models and their engines -* load libraries and then use ffi to load symbols +- You can grab cURL’s CA certificates which we pkg and keep up to date (`curl.se/ca-certs`). These + are commonly needed across ecosystems but not always easily accessible. +- grab libraries that wrappers need like openssl or sqlite +- run a real database (like postgres) easily +- load local AI models and their engines +- load libraries and then use ffi to load symbols   @@ -186,24 +172,23 @@ We would be thrilled to hear your ideas† or receive your pull requests. ## Anatomy -The code is written with Deno (just like [tea/cli]) but is compiled to a -node package for wider accessibility (and ∵ [tea/gui] is node/electron). +The code is written with Deno (just like [tea/cli]) but is compiled to a node package for wider +accessibility (and ∵ [tea/gui] is node/electron). -The library is architected into hooks, plumbing and porcelain. Where the hooks -represent the low level primitives of pkging, the plumbing glues those -primitives together into useful components and the porcelain is a user -friendly *façade* pattern for the plumbing. +The library is architected into hooks, plumbing and porcelain. Where the hooks represent the low +level primitives of pkging, the plumbing glues those primitives together into useful components and +the porcelain is a user friendly _façade_ pattern for the plumbing. ## Supporting Other Languages -We would love to port this code to every language. We are deliberately keeping -the scope *tight*. Probably we would prefer to have one repo per language. +We would love to port this code to every language. We are deliberately keeping the scope _tight_. +Probably we would prefer to have one repo per language. -tea has sensible rules for how packages are defined and installed so writing -a port should be simple. +tea has sensible rules for how packages are defined and installed so writing a port should be +simple. -We would love to explore how possible writing this in rust and then compiling -to WASM for all other languages would be. Can you help? +We would love to explore how possible writing this in rust and then compiling to WASM for all other +languages would be. Can you help? Open a [discussion] to start. @@ -217,7 +202,6 @@ Open a [discussion] to start.   - # Tasks Run eg. `xc coverage` or `xc bump patch`. @@ -233,8 +217,7 @@ open cov_profile/html/index.html ## Bump -Bumps version by creating a pre-release which then engages the deployment -infra in GitHub Actions. +Bumps version by creating a pre-release which then engages the deployment infra in GitHub Actions. Inputs: LEVEL diff --git a/deno.json b/deno.jsonc similarity index 89% rename from deno.json rename to deno.jsonc index 3c22ccd..4b2326e 100644 --- a/deno.json +++ b/deno.jsonc @@ -4,11 +4,8 @@ "strict": true }, "fmt": { - "files": { - "exclude": [ - "./" - ] - } + "semiColons": false, + "lineWidth": 100 }, "tea": { "dependencies": { diff --git a/examples/awscli/index.mjs b/examples/awscli/index.mjs index 6ecd398..31b954d 100644 --- a/examples/awscli/index.mjs +++ b/examples/awscli/index.mjs @@ -1,5 +1,5 @@ -import * as tea from '@teaxyz/lib'; -import * as awsclijs from 'aws-cli-js'; +import * as tea from "@teaxyz/lib" +import * as awsclijs from "aws-cli-js" const { Options, Aws } = awsclijs const { porcelain: { install }, hooks: { useShellEnv } } = tea @@ -9,13 +9,13 @@ const opts = new Options(process.env.AWS_ACCESS_KEY_ID, process.env.AWS_SECRET_A await installAwsCli() const aws = new Aws(opts) -const users = await aws.command('iam list-users') +const users = await aws.command("iam list-users") console.log(users) /////////////////////////////////////////// async function installAwsCli() { const { map, flatten } = useShellEnv() - const installations = await install('aws.amazon.com/cli') + const installations = await install("aws.amazon.com/cli") Object.assign(process.env, flatten(await map({ installations }))) } diff --git a/examples/whisper.js b/examples/whisper.js index bf5c773..65ccb96 100644 --- a/examples/whisper.js +++ b/examples/whisper.js @@ -8,10 +8,10 @@ const https = require("node:https") const { run } = porcelain const fs = require("node:fs") -const url = 'https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav' +const url = "https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav" -const fetch = new Promise(done => - https.get(url, rsp => - rsp.pipe(fs.createWriteStream("jfk.wav")).on('finish', done))) +const fetch = new Promise((done) => + https.get(url, (rsp) => rsp.pipe(fs.createWriteStream("jfk.wav")).on("finish", done)) +) -fetch.then(() =>run("whisper.cpp jfk.wav")) +fetch.then(() => run("whisper.cpp jfk.wav")) diff --git a/examples/whisper.mjs b/examples/whisper.mjs index fac539e..fcb00b6 100644 --- a/examples/whisper.mjs +++ b/examples/whisper.mjs @@ -8,10 +8,10 @@ import https from "node:https" const { run } = porcelain import fs from "node:fs" -const url = 'https://raw.githubusercontent.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav' +const url = "https://raw.githubusercontent.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav" -await new Promise(done => - https.get(url, rsp => - rsp.pipe(fs.createWriteStream("jfk.wav")).on('finish', done))) +await new Promise((done) => + https.get(url, (rsp) => rsp.pipe(fs.createWriteStream("jfk.wav")).on("finish", done)) +) await run("whisper.cpp jfk.wav") diff --git a/examples/whisper.ts b/examples/whisper.ts index 7230e75..fbd36ba 100755 --- a/examples/whisper.ts +++ b/examples/whisper.ts @@ -8,7 +8,7 @@ import { porcelain } from "https://raw.github.com/teaxyz/lib/v0/mod.ts" import { green } from "https://deno.land/std/fmt/colors.ts" const { run } = porcelain -const url = 'https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav' +const url = "https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav" const rsp = await fetch(url) await Deno.writeFile("jfk.wav", rsp.body!) diff --git a/fixtures/npm-integration-test/index.js b/fixtures/npm-integration-test/index.js index db1ac8c..9b5658c 100644 --- a/fixtures/npm-integration-test/index.js +++ b/fixtures/npm-integration-test/index.js @@ -5,4 +5,3 @@ const { ConsoleLogger } = require("@teaxyz/lib/plumbing/install") console.log(ConfigDefault(), ConsoleLogger()) run("ls -la") - diff --git a/mod.ts b/mod.ts index c12e8ce..f99879f 100644 --- a/mod.ts +++ b/mod.ts @@ -10,11 +10,16 @@ import * as pkg from "./src/utils/pkg.ts" import { panic, TeaError } from "./src/utils/error.ts" import useConfig from "./src/hooks/useConfig.ts" -import useOffLicense from "./src/hooks/useOffLicense.ts" +import useOffLicense from "./src/hooks/useOffLicense.ts" import useCache from "./src/hooks/useCache.ts" -import useCellar, { InstallationNotFoundError} from "./src/hooks/useCellar.ts" +import useCellar, { InstallationNotFoundError } from "./src/hooks/useCellar.ts" import useMoustaches from "./src/hooks/useMoustaches.ts" -import usePantry, { PantryError, PantryParseError, PantryNotFoundError, PackageNotFoundError } from "./src/hooks/usePantry.ts" +import usePantry, { + PackageNotFoundError, + PantryError, + PantryNotFoundError, + PantryParseError, +} from "./src/hooks/usePantry.ts" import useFetch from "./src/hooks/useFetch.ts" import useDownload, { DownloadError } from "./src/hooks/useDownload.ts" import useShellEnv from "./src/hooks/useShellEnv.ts" @@ -30,7 +35,12 @@ import run, { RunError } from "./src/porcelain/run.ts" import porcelain_install from "./src/porcelain/install.ts" const utils = { - pkg, host, flatmap, validate, panic, ConsoleLogger + pkg, + host, + flatmap, + validate, + panic, + ConsoleLogger, } const hooks = { @@ -52,27 +62,34 @@ const plumbing = { link, install, resolve, - which + which, } const porcelain = { install: porcelain_install, - run + run, } const hacks = { - validatePackageRequirement + validatePackageRequirement, } export { - utils, hooks, plumbing, porcelain, hacks, + DownloadError, + hacks, + hooks, + InstallationNotFoundError, + PackageNotFoundError, + PantryError, + PantryNotFoundError, + PantryParseError, + plumbing, + porcelain, + ResolveError, + RunError, semver, TeaError, - RunError, - ResolveError, - PantryError, PantryParseError, PantryNotFoundError, PackageNotFoundError, - InstallationNotFoundError, - DownloadError + utils, } /// export types diff --git a/src/hooks/useCache.test.ts b/src/hooks/useCache.test.ts index e58b261..a44ce49 100644 --- a/src/hooks/useCache.test.ts +++ b/src/hooks/useCache.test.ts @@ -11,22 +11,22 @@ Deno.test("useCache", () => { const stowage = StowageNativeBottle({ pkg: { project: "foo/bar", version: new SemVer("1.0.0") }, - compression: "xz" - }); + compression: "xz", + }) assertEquals(useCache().path(stowage), cache.join(`foo∕bar-1.0.0+${hw}.tar.xz`)) const stowage2: Stowage = { - type: 'bottle', + type: "bottle", pkg: stowage.pkg, host: { platform: "linux", arch: "aarch64" }, - compression: 'xz' + compression: "xz", } assertEquals(useCache().path(stowage2), cache.join("foo∕bar-1.0.0+linux+aarch64.tar.xz")) const stowage3: Stowage = { pkg: stowage.pkg, type: "src", - extname: ".tgz" + extname: ".tgz", } assertEquals(useCache().path(stowage3), cache.join("foo∕bar-1.0.0.tgz")) }) diff --git a/src/hooks/useCache.ts b/src/hooks/useCache.ts index bf83809..2cca994 100644 --- a/src/hooks/useCache.ts +++ b/src/hooks/useCache.ts @@ -11,7 +11,7 @@ const path = (stowage: Stowage) => { const stem = pkg.project.replaceAll("/", "∕") let filename = `${stem}-${pkg.version}` - if (type == 'bottle') { + if (type == "bottle") { const { platform, arch } = stowage.host ?? host() filename += `+${platform}+${arch}.tar.${stowage.compression}` } else { diff --git a/src/hooks/useCellar.test.ts b/src/hooks/useCellar.test.ts index 753c881..6cecea2 100644 --- a/src/hooks/useCellar.test.ts +++ b/src/hooks/useCellar.test.ts @@ -7,7 +7,7 @@ import useCellar from "./useCellar.ts" Deno.test("useCellar.resolve()", async () => { useTestConfig() - const pkgrq = { project: "python.org", version: new SemVer("3.11.3")} + const pkgrq = { project: "python.org", version: new SemVer("3.11.3") } const installation = await install(pkgrq) await useCellar().resolve(installation) @@ -15,7 +15,9 @@ Deno.test("useCellar.resolve()", async () => { await useCellar().resolve({ project: "python.org", constraint: new semver.Range("^3") }) await useCellar().resolve(installation.path) - assertRejects(() => useCellar().resolve({ project: "python.org", constraint: new semver.Range("@300")})) + assertRejects(() => + useCellar().resolve({ project: "python.org", constraint: new semver.Range("@300") }) + ) }) Deno.test("useCellar.has()", async () => { diff --git a/src/hooks/useCellar.ts b/src/hooks/useCellar.ts index ea4e016..a835838 100644 --- a/src/hooks/useCellar.ts +++ b/src/hooks/useCellar.ts @@ -1,4 +1,4 @@ -import { Package, PackageRequirement, Installation } from "../types.ts" +import { Installation, Package, PackageRequirement } from "../types.ts" import { TeaError } from "../utils/error.ts" import * as pkgutils from "../utils/pkg.ts" import SemVer from "../utils/semver.ts" @@ -24,7 +24,8 @@ export default function useCellar() { const keg = (pkg: Package) => shelf(pkg.project).join(`v${pkg.version}`) /// returns the `Installation` if the pkg is installed - const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(InstallationNotFoundError) + const has = (pkg: Package | PackageRequirement | Path) => + resolve(pkg).swallow(InstallationNotFoundError) return { has, @@ -41,13 +42,13 @@ export default function useCellar() { if (!d.isDirectory()) return [] const rv: Installation[] = [] - for await (const [path, {name, isDirectory}] of d.ls()) { + for await (const [path, { name, isDirectory }] of d.ls()) { try { if (!isDirectory) continue - if (!name.startsWith("v") || name == 'var') continue + if (!name.startsWith("v") || name == "var") continue const version = new SemVer(name) - if (await vacant(path)) continue // failed build probs - rv.push({path, pkg: {project, version}}) + if (await vacant(path)) continue // failed build probs + rv.push({ path, pkg: { project, version } }) } catch { //noop: other directories can exist } @@ -59,7 +60,7 @@ export default function useCellar() { /// 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 } + if ("pkg" in pkg) return pkg // ^^ is `Installation` const { prefix } = config @@ -68,17 +69,18 @@ export default function useCellar() { const version = new SemVer(path.basename()) const project = path.parent().relative({ to: prefix }) return { - path, pkg: { project, version } + 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 versions = installations.map(({ pkg: { version } }) => version) const version = pkg.constraint.max(versions) if (version) { - const path = installations.find(({pkg: {version: v}}) => v.eq(version))!.path + const path = installations.find(({ pkg: { version: v } }) => v.eq(version))!.path return { path, pkg: { project: pkg.project, version } } } else { throw new InstallationNotFoundError(pkg) @@ -96,8 +98,8 @@ export default function useCellar() { async function vacant(path: Path): Promise { if (!path.isDirectory()) { return true - } else for await (const _ of path.ls()) { - return false - } + } else {for await (const _ of path.ls()) { + return false + }} return true } diff --git a/src/hooks/useConfig.test.ts b/src/hooks/useConfig.test.ts index 709a449..b84456b 100644 --- a/src/hooks/useConfig.test.ts +++ b/src/hooks/useConfig.test.ts @@ -1,4 +1,10 @@ -import { assert, assertEquals, assertFalse, assertThrows, assertMatch } from "deno/testing/asserts.ts" +import { + assert, + assertEquals, + assertFalse, + assertMatch, + assertThrows, +} from "deno/testing/asserts.ts" import { _internals, ConfigDefault } from "./useConfig.ts" import { useTestConfig } from "./useTestConfig.ts" import Path from "../utils/Path.ts" @@ -13,7 +19,7 @@ Deno.test("useConfig", () => { } config = ConfigDefault({ TEA_PANTRY_PATH: "/foo:/bar", CI: "true" }) - assertEquals(config.pantries.map(x => x.string), ["/foo", "/bar"]) + assertEquals(config.pantries.map((x) => x.string), ["/foo", "/bar"]) assertEquals(config.options.compression, "gz") assertFalse(_internals.boolize("false")) diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 5a0fe37..277caff 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -9,7 +9,7 @@ export interface Config { options: { /// prefer xz or gz for bottle downloads - compression: 'xz' | 'gz' + compression: "xz" | "gz" } UserAgent?: string @@ -18,14 +18,17 @@ export interface Config { } export function ConfigDefault(env = Deno.env.toObject()): Config { - const prefix = flatmap(env['TEA_PREFIX']?.trim(), x => new Path(x)) ?? Path.home().join('.tea') - const pantries = env['TEA_PANTRY_PATH']?.split(":").compact(x => flatmap(x.trim(), x => Path.abs(x) ?? Path.cwd().join(x))) ?? [] - const cache = Path.abs(env['TEA_CACHE_DIR']) ?? prefix.join('tea.xyz/var/www') - const isCI = boolize(env['CI']) ?? false - const UserAgent = flatmap(getv(), v => `tea.lib/${v}`) ?? 'tea.lib' + const prefix = flatmap(env["TEA_PREFIX"]?.trim(), (x) => new Path(x)) ?? Path.home().join(".tea") + const pantries = + env["TEA_PANTRY_PATH"]?.split(":").compact((x) => + flatmap(x.trim(), (x) => Path.abs(x) ?? Path.cwd().join(x)) + ) ?? [] + const cache = Path.abs(env["TEA_CACHE_DIR"]) ?? prefix.join("tea.xyz/var/www") + const isCI = boolize(env["CI"]) ?? false + const UserAgent = flatmap(getv(), (v) => `tea.lib/${v}`) ?? "tea.lib" //TODO prefer 'xz' on Linux (as well) if supported - const compression = !isCI && host().platform == 'darwin' ? 'xz' : 'gz' + const compression = !isCI && host().platform == "darwin" ? "xz" : "gz" return { prefix, @@ -35,22 +38,22 @@ export function ConfigDefault(env = Deno.env.toObject()): Config { options: { compression, }, - git: git(prefix, env.PATH) + git: git(prefix, env.PATH), } } function getv(): string | undefined { - if (typeof Deno === 'undefined') { + if (typeof Deno === "undefined") { const url = new URL(import.meta.url) const path = new Path(url.pathname).parent().parent().parent().join("package.json") const blob = Deno.readFileSync(path.string) const txt = new TextDecoder().decode(blob) const { version } = JSON.parse(txt) - return typeof version == 'string' ? version : undefined + return typeof version == "string" ? version : undefined } } -const gt = globalThis as unknown as {xyz_tea_config?: Config} +const gt = globalThis as unknown as { xyz_tea_config?: Config } export default function useConfig(input?: Config): Config { // storing on globalThis so our config is shared across @@ -58,18 +61,18 @@ export default function useConfig(input?: Config): Config { if (!gt.xyz_tea_config || input) { gt.xyz_tea_config = input ?? ConfigDefault() } - return {...gt.xyz_tea_config} // copy to prevent mutation + return { ...gt.xyz_tea_config } // copy to prevent mutation } function boolize(input: string | undefined): boolean | undefined { switch (input?.trim()?.toLowerCase()) { - case '0': - case 'false': - case 'no': + case "0": + case "false": + case "no": return false - case '1': - case 'true': - case 'yes': + case "1": + case "true": + case "yes": return true } } @@ -84,7 +87,6 @@ function initialized() { export const _internals = { reset, initialized, boolize } - /// 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 @@ -99,10 +101,10 @@ function git(_prefix: Path, PATH?: string): Path | 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 (host().platform == "darwin") { if (new Path("/Library/Developer/CommandLineTools/usr/bin/git").isExecutableFile()) return rv if (new Path("/Applications/Xcode.app").isDirectory()) return rv - return // don’t use `git` + return // don’t use `git` } return rv?.join("bin/git") diff --git a/src/hooks/useDownload.test.ts b/src/hooks/useDownload.test.ts index 927aa23..f5d5afe 100644 --- a/src/hooks/useDownload.test.ts +++ b/src/hooks/useDownload.test.ts @@ -2,14 +2,14 @@ import { useTestConfig } from "./useTestConfig.ts" import { assert } from "deno/testing/asserts.ts" import useDownload from "./useDownload.ts" -Deno.test("etag-mtime-check", async runner => { +Deno.test("etag-mtime-check", async (runner) => { useTestConfig({ TEA_CACHE_DIR: Deno.makeTempDirSync() }) const src = new URL("https://dist.tea.xyz/ijg.org/versions.txt") const { download, cache } = useDownload() await runner.step("download", async () => { - await download({src}) + await download({ src }) const mtimePath = cache({ for: src }).join("mtime") const etagPath = cache({ for: src }).join("etag") @@ -29,12 +29,15 @@ Deno.test("etag-mtime-check", async runner => { await runner.step("second download doesn’t http", async () => { let n = 0 - await download({src}, blob => { n += blob.length; return Promise.resolve() }) // for coverage + await download({ src }, (blob) => { + n += blob.length + return Promise.resolve() + }) // for coverage assert(n > 0) }) await runner.step("second download doesn’t http and is fine if we do nothing", async () => { - const dst = await download({src}) + const dst = await download({ src }) assert(dst.isFile()) }) }) diff --git a/src/hooks/useDownload.ts b/src/hooks/useDownload.ts index 0dae3f2..68a70a4 100644 --- a/src/hooks/useDownload.ts +++ b/src/hooks/useDownload.ts @@ -1,7 +1,7 @@ import { deno } from "../deps.ts" const { crypto: crypto_, streams: { writeAll } } = deno const { toHashString, crypto } = crypto_ -import { TeaError, panic } from "../utils/error.ts" +import { panic, TeaError } from "../utils/error.ts" import useConfig from "./useConfig.ts" import useFetch from "./useFetch.ts" import Path from "../utils/Path.ts" @@ -12,7 +12,7 @@ interface DownloadOptions { src: URL dst?: Path headers?: Record - logger?: (info: {src: URL, dst: Path, rcvd?: number, total?: number }) => void + logger?: (info: { src: URL; dst: Path; rcvd?: number; total?: number }) => void } export class DownloadError extends TeaError { @@ -20,9 +20,9 @@ export class DownloadError extends TeaError { src: URL headers?: Record - constructor(status: number, opts: { src: URL, headers?: Record}) { + constructor(status: number, opts: { src: URL; headers?: Record }) { super(`http: ${status}: ${opts.src}`) - this.name = 'DownloadError' + this.name = "DownloadError" this.status = status this.src = opts.src this.headers = opts.headers @@ -31,7 +31,10 @@ export class DownloadError extends TeaError { const tmpname = (dst: Path) => dst.parent().join(dst.basename() + ".incomplete") -async function download(opts: DownloadOptions, chunk?: (blob: Uint8Array) => Promise): Promise { +async function download( + opts: DownloadOptions, + chunk?: (blob: Uint8Array) => Promise, +): Promise { const [dst, stream] = await the_meat(opts) if (stream || chunk) { @@ -39,8 +42,8 @@ async function download(opts: DownloadOptions, chunk?: (blob: Uint8Array) => Pro const writer = await (() => { if (stream) { - dst.parent().mkdir('p') - return Deno.open(tmpname(dst).string, {write: true, create: true, truncate: true}) + dst.parent().mkdir("p") + return Deno.open(tmpname(dst).string, { write: true, create: true, truncate: true }) } })() @@ -63,12 +66,12 @@ async function download(opts: DownloadOptions, chunk?: (blob: Uint8Array) => Pro return dst } -function cache({ for: url }: {for: URL}): Path { +function cache({ for: url }: { for: URL }): Path { return useConfig().cache .join(url.protocol.slice(0, -1)) .join(url.hostname) .join(hash()) - .mkdir('p') + .mkdir("p") function hash() { let key = url.pathname @@ -82,15 +85,15 @@ function cache({ for: url }: {for: URL}): Path { export default function useDownload() { return { download, - cache + cache, } } - /// internal -async function the_meat({ src, logger, headers, dst }: DownloadOptions): Promise<[Path, ReadableStream | undefined, number | undefined]> -{ +async function the_meat( + { src, logger, headers, dst }: DownloadOptions, +): Promise<[Path, ReadableStream | undefined, number | undefined]> { const hash = cache({ for: src }) const mtime_entry = hash.join("mtime") const etag_entry = hash.join("etag") @@ -116,36 +119,43 @@ async function the_meat({ src, logger, headers, dst }: DownloadOptions): Prom const rsp = await useFetch(src, { headers }) switch (rsp.status) { - case 200: { - const sz = parseInt(rsp.headers.get("Content-Length")!).chuzzle() - - if (logger) logger({ src, dst, total: sz }) - - const reader = rsp.body ?? 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 (!logger) { - return [dst, reader, sz] - } else { - let n = 0 - return [dst, reader.pipeThrough(new TransformStream({ - transform: (buf, controller) => { - n += buf.length - logger({ src, dst: dst!, rcvd: n, total: sz }) - controller.enqueue(buf) - }})), sz] + case 200: { + const sz = parseInt(rsp.headers.get("Content-Length")!).chuzzle() + + if (logger) logger({ src, dst, total: sz }) + + const reader = rsp.body ?? 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 (!logger) { + return [dst, reader, sz] + } else { + let n = 0 + return [ + dst, + reader.pipeThrough( + new TransformStream({ + transform: (buf, controller) => { + n += buf.length + logger({ src, dst: dst!, rcvd: n, total: sz }) + controller.enqueue(buf) + }, + }), + ), + sz, + ] + } } - } - case 304: { - const sz = (await Deno.stat(dst.string)).size - if (logger) logger({ src, dst, rcvd: sz, total: sz }) - return [dst, undefined, sz] - } - default: - throw new DownloadError(rsp.status, { src, headers }) + case 304: { + const sz = (await Deno.stat(dst.string)).size + if (logger) logger({ src, dst, rcvd: sz, total: sz }) + return [dst, undefined, sz] + } + default: + throw new DownloadError(rsp.status, { src, headers }) } } diff --git a/src/hooks/useFetch.test.ts b/src/hooks/useFetch.test.ts index 761cee4..25ae85f 100644 --- a/src/hooks/useFetch.test.ts +++ b/src/hooks/useFetch.test.ts @@ -1,8 +1,7 @@ -import { stub, assertSpyCallArgs } from "deno/testing/mock.ts" +import { assertSpyCallArgs, stub } from "deno/testing/mock.ts" import useConfig, { _internals, ConfigDefault } from "./useConfig.ts" import useFetch from "./useFetch.ts" - Deno.test("fetch user-agent header check", async () => { /// doesn't work inside DNT because fetch is shimmed to undici if (Deno.env.get("NODE")) return @@ -10,22 +9,22 @@ Deno.test("fetch user-agent header check", async () => { const UserAgent = "tests/1.2.3" _internals.reset() - useConfig({...ConfigDefault(), UserAgent}); + useConfig({ ...ConfigDefault(), UserAgent }) - const url = "https://example.com"; + const url = "https://example.com" const fetchStub = stub( globalThis, "fetch", () => Promise.resolve(new Response("")), - ); + ) try { - await useFetch(url, {}); + await useFetch(url, {}) } finally { - fetchStub.restore(); + fetchStub.restore() } assertSpyCallArgs(fetchStub, 0, [url, { - headers: {"User-Agent": UserAgent} - }]); -}); + headers: { "User-Agent": UserAgent }, + }]) +}) diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts index c86f515..85e8b78 100644 --- a/src/hooks/useFetch.ts +++ b/src/hooks/useFetch.ts @@ -1,7 +1,10 @@ import useConfig from "./useConfig.ts" // 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 { +export default function useFetch( + input: string | URL | Request, + init?: RequestInit | undefined, +): Promise { const { UserAgent } = useConfig() const requestInit = init ?? {} as RequestInit if (UserAgent) { diff --git a/src/hooks/useInventory.ts b/src/hooks/useInventory.ts index 903fad4..ee3e42e 100644 --- a/src/hooks/useInventory.ts +++ b/src/hooks/useInventory.ts @@ -19,7 +19,7 @@ const select = async (rq: PackageRequirement | Package) => { if ("constraint" in rq) { return rq.constraint.max(versions) - } else if (versions.find(x => x.eq(rq.version))) { + } else if (versions.find((x) => x.eq(rq.version))) { return rq.version } } @@ -27,24 +27,24 @@ const select = async (rq: PackageRequirement | Package) => { 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 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) { - throw new DownloadError(rsp.status, {src: url}) + throw new DownloadError(rsp.status, { src: url }) } const releases = await rsp.text() - let versions = releases.split("\n").compact(x => new SemVer(x)) + let versions = releases.split("\n").compact((x) => new SemVer(x)) if (versions.length < 1) throw new Error() - if (rq.project == 'openssl.org') { + if (rq.project == "openssl.org") { // workaround our previous sins const v = new SemVer("1.1.118") - versions = versions.filter(x => x.neq(v)) + versions = versions.filter((x) => x.neq(v)) } return versions diff --git a/src/hooks/useMoustaches.test.ts b/src/hooks/useMoustaches.test.ts index e32636e..955179a 100644 --- a/src/hooks/useMoustaches.test.ts +++ b/src/hooks/useMoustaches.test.ts @@ -10,7 +10,7 @@ Deno.test("useMoustaches", () => { const pkg: Package = { project: "tea.xyz/test", - version: new SemVer("1.0.0") + version: new SemVer("1.0.0"), } const tokens = moustaches.tokenize.all(pkg, []) diff --git a/src/hooks/useMoustaches.ts b/src/hooks/useMoustaches.ts index 49902cf..720ca83 100644 --- a/src/hooks/useMoustaches.ts +++ b/src/hooks/useMoustaches.ts @@ -1,7 +1,7 @@ import useConfig from "./useConfig.ts" import SemVer from "../utils/semver.ts" import useCellar from "./useCellar.ts" -import { Package, Installation } from "../types.ts" +import { Installation, Package } from "../types.ts" import host from "../utils/host.ts" import * as os from "node:os" @@ -9,18 +9,18 @@ function tokenizePackage(pkg: Package) { return [{ from: "prefix", to: useCellar().keg(pkg).string }] } -function tokenizeVersion(version: SemVer, prefix = 'version') { +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, 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 }, + { 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}) + if ("tag" in version) { + rv.push({ from: `${prefix}.tag`, to: (version as unknown as { tag: string }).tag }) } return rv } @@ -29,32 +29,33 @@ function tokenizeVersion(version: SemVer, prefix = 'version') { 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: os.cpus().length.toString() } + { from: "hw.arch", to: arch }, + { from: "hw.target", to: target }, + { from: "hw.platform", to: platform }, + { from: "hw.concurrency", to: os.cpus().length.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) +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, + ) } -export default function() { +export default function () { const config = useConfig() const base = { apply, tokenize: { version: tokenizeVersion, host: tokenizeHost, - pkg: tokenizePackage - } + pkg: tokenizePackage, + }, } const deps = (deps: Installation[]) => { - const map: {from: string, to: string}[] = [] + 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(...base.tokenize.version(dep.pkg.version, `deps.${dep.pkg.project}.version`)) @@ -76,7 +77,9 @@ export default function() { apply: base.apply, tokenize: { ...base.tokenize, - deps, tea, all - } + deps, + tea, + all, + }, } } diff --git a/src/hooks/useOffLicense.ts b/src/hooks/useOffLicense.ts index a3d3ffb..351db9f 100644 --- a/src/hooks/useOffLicense.ts +++ b/src/hooks/useOffLicense.ts @@ -2,7 +2,7 @@ import { Stowage } from "../types.ts" import host from "../utils/host.ts" import Path from "../utils/Path.ts" -type Type = 's3' +type Type = "s3" export default function useOffLicense(_type: Type) { return { url, key } @@ -10,15 +10,15 @@ export default function useOffLicense(_type: Type) { function key(stowage: Stowage) { let rv = Path.root.join(stowage.pkg.project) - if (stowage.type == 'bottle') { + 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') { + if (stowage.type == "bottle") { fn += `.tar.${stowage.compression}` } else { - fn += stowage.extname + fn += stowage.extname } return rv.join(fn).string.slice(1) } diff --git a/src/hooks/usePantry.test.ts b/src/hooks/usePantry.test.ts index 56d08c1..63d7c71 100644 --- a/src/hooks/usePantry.test.ts +++ b/src/hooks/usePantry.test.ts @@ -19,26 +19,26 @@ Deno.test("which()", async () => { Deno.test("provider()", async () => { const provides = await usePantry().project("npmjs.com").provider() - const foo = provides!('truffle') - assertEquals(foo![0], 'npx') + const foo = provides!("truffle") + assertEquals(foo![0], "npx") }) Deno.test("available()", async () => { - const stubber = stub(_internals, 'platform', () => "darwin" as "darwin" | "linux") + const stubber = stub(_internals, "platform", () => "darwin" as "darwin" | "linux") assert(await usePantry().project("agpt.co").available()) stubber.restore() }) Deno.test("runtime.env", async () => { const TEA_PANTRY_PATH = new Path(Deno.env.get("SRCROOT")!).join("fixtures").string - const { prefix } = useTestConfig({ TEA_PANTRY_PATH }) + const { prefix } = useTestConfig({ TEA_PANTRY_PATH }) const deps = [{ pkg: { project: "bar.com", - version: new SemVer("1.2.3") + version: new SemVer("1.2.3"), }, - path: prefix.join("bar.com/v1.2.3") + path: prefix.join("bar.com/v1.2.3"), }] const env = await usePantry().project("foo.com").runtime.env(new SemVer("2.3.4"), deps) @@ -49,7 +49,7 @@ Deno.test("runtime.env", async () => { }) Deno.test("missing()", () => { - useTestConfig({TEA_PANTRY_PATH: "/a"}) + useTestConfig({ TEA_PANTRY_PATH: "/a" }) assert(usePantry().missing()) }) diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts index c03bcf5..d1a8e18 100644 --- a/src/hooks/usePantry.ts +++ b/src/hooks/usePantry.ts @@ -1,7 +1,7 @@ import { is_what, PlainObject } from "../deps.ts" const { isNumber, isPlainObject, isString, isArray, isPrimitive, isBoolean } = is_what import { validatePackageRequirement } from "../utils/hacks.ts" -import { Package, Installation } from "../types.ts" +import { Installation, Package } from "../types.ts" import useMoustaches from "./useMoustaches.ts" import { TeaError } from "../utils/error.ts" import { validate } from "../utils/misc.ts" @@ -15,8 +15,7 @@ export interface Interpreter { args: string[] } -export class PantryError extends TeaError -{} +export class PantryError extends TeaError {} export class PantryParseError extends PantryError { project: string @@ -46,7 +45,7 @@ export class PantryNotFoundError extends PantryError { export default function usePantry() { const config = useConfig() - const prefix = config.prefix.join('tea.xyz/var/pantry/projects') + const prefix = config.prefix.join("tea.xyz/var/pantry/projects") async function* ls(): AsyncGenerator { const seen = new Set() @@ -73,9 +72,12 @@ export default function usePantry() { let memo: Promise | undefined - return () => memo ?? (memo = filename.readYAML() - .then(validate.obj) - .catch(cause => { throw new PantryParseError(project, filename, cause) })) + return () => + memo ?? (memo = filename.readYAML() + .then(validate.obj) + .catch((cause) => { + throw new PantryParseError(project, filename, cause) + })) } throw new PackageNotFoundError(project) })() @@ -93,7 +95,8 @@ export default function usePantry() { if (!platforms) return true if (isString(platforms)) platforms = [platforms] if (!isArray(platforms)) throw new PantryParseError(project) - return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`) + return platforms.includes(host().platform) || + platforms.includes(`${host().platform}/${host().arch}`) } const drydeps = async () => parse_pkgs_node((await yaml()).dependencies) @@ -106,7 +109,7 @@ export default function usePantry() { } if (!isArray(node)) throw new PantryParseError(project) - return node.compact(x => { + return node.compact((x) => { if (isPlainObject(x)) { x = x["executable"] } @@ -127,12 +130,12 @@ export default function usePantry() { const cmds = validate.arr(yaml.cmds) return (binname: string) => { if (!cmds.includes(binname)) return - const args = yaml['args'] + const args = yaml["args"] if (isPlainObject(args)) { if (args[binname]) { return get_args(args[binname]) } else { - return get_args(args['...']) + return get_args(args["..."]) } } else { return get_args(args) @@ -152,12 +155,12 @@ export default function usePantry() { companions, runtime: { env: runtime_env, - deps: drydeps + deps: drydeps, }, available, provides, provider, - yaml + yaml, } } @@ -171,7 +174,7 @@ export default function usePantry() { //TODO not very performant due to serial awaits const rv: Foo[] = [] for await (const pkg of ls()) { - const proj = {...project(pkg.project), ...pkg} + const proj = { ...project(pkg.project), ...pkg } if (pkg.project.toLowerCase() == name) { rv.push(proj) continue @@ -179,15 +182,17 @@ export default function usePantry() { const yaml = await proj.yaml() if (yaml["display-name"]?.toLowerCase() == name) { rv.push(proj) - } else if ((await proj.provides()).map(x => x.toLowerCase()).includes(name)) { + } else if ((await proj.provides()).map((x) => x.toLowerCase()).includes(name)) { rv.push(proj) } } return rv } - async function which({ interprets: extension }: { interprets: string }): Promise { - if (extension[0] == '.') extension = extension.slice(1) + async function which( + { interprets: extension }: { interprets: string }, + ): Promise { + if (extension[0] == ".") extension = extension.slice(1) if (!extension) return for await (const pkg of ls()) { const yml = await project(pkg).yaml() @@ -195,8 +200,10 @@ export default function usePantry() { if (!isPlainObject(node)) continue try { const { extensions, args } = yml["interprets"] - if ((isString(extensions) && extensions === extension) || - (isArray(extensions) && extensions.includes(extension))) { + if ( + (isString(extensions) && extensions === extension) || + (isArray(extensions) && extensions.includes(extension)) + ) { return { project: pkg.project, args: isArray(args) ? args : [args] } } } catch { @@ -208,7 +215,7 @@ export default function usePantry() { const missing = () => { try { - return !pantry_paths().some(x => x.exists()) + return !pantry_paths().some((x) => x.exists()) } catch (e) { if (e instanceof PantryNotFoundError) { return true @@ -234,7 +241,7 @@ export default function usePantry() { parse_pkgs_node, expand_env_obj, missing, - neglected + neglected, } function pantry_paths(): Path[] { @@ -262,8 +269,7 @@ export function parse_pkgs_node(node: any) { platform_reduce(node) return Object.entries(node) - .compact(([project, constraint]) => - validatePackageRequirement(project, constraint)) + .compact(([project, constraint]) => validatePackageRequirement(project, constraint)) } /// expands platform specific keys into the object @@ -275,7 +281,7 @@ function platform_reduce(env: PlainObject) { 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]] + if ((match = key.match(/^(aarch64|x86-64)$/))) return [, match[1]] return [] })() @@ -300,8 +306,12 @@ function platform_reduce(env: PlainObject) { } } -export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installation[]): Record { - const env = {...env_} +export function expand_env_obj( + env_: PlainObject, + pkg: Package, + deps: Installation[], +): Record { + const env = { ...env_ } platform_reduce(env) @@ -309,7 +319,7 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati for (let [key, value] of Object.entries(env)) { if (isArray(value)) { - value = value.map(x => transform(x)).join(" ") + value = value.map((x) => transform(x)).join(" ") } else { value = transform(value) } @@ -321,7 +331,9 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati // deno-lint-ignore no-explicit-any function transform(value: any): string { - if (!isPrimitive(value)) throw new PantryParseError(pkg.project, undefined, JSON.stringify(value)) + if (!isPrimitive(value)) { + throw new PantryParseError(pkg.project, undefined, JSON.stringify(value)) + } if (isBoolean(value)) { return value ? "1" : "0" @@ -331,8 +343,8 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati 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 ~ + { 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) diff --git a/src/hooks/useShellEnv.ts b/src/hooks/useShellEnv.ts index 35fe290..c136a76 100644 --- a/src/hooks/useShellEnv.ts +++ b/src/hooks/useShellEnv.ts @@ -4,19 +4,19 @@ import usePantry from "./usePantry.ts" import host from "../utils/host.ts" 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' + "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] @@ -24,28 +24,27 @@ interface Options { installations: Installation[] } -export default function() { +export default function () { return { map, expand, - flatten + flatten, } } /// returns an environment that supports the provided packages -async function map({installations}: Options): Promise> { +async function map({ installations }: Options): Promise> { const vars: Partial>> = {} - const isMac = host().platform == 'darwin' + const isMac = host().platform == "darwin" - const projects = new Set(installations.map(x => x.pkg.project)) - const has_cmake = projects.has('cmake.org') + 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("tea: env is being duped:", installation.pkg.project) } @@ -57,7 +56,10 @@ async function map({installations}: Options): Promise> } if (archaic) { - vars.LIBRARY_PATH = compact_add(vars.LIBRARY_PATH, installation.path.join("lib").chuzzle()?.string) + vars.LIBRARY_PATH = compact_add( + vars.LIBRARY_PATH, + installation.path.join("lib").chuzzle()?.string, + ) vars.CPATH = compact_add(vars.CPATH, installation.path.join("include").chuzzle()?.string) } @@ -65,11 +67,14 @@ async function map({installations}: Options): Promise> 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").chuzzle()?.string) + if (projects.has("gnu.org/autoconf")) { + vars.ACLOCAL_PATH = compact_add( + vars.ACLOCAL_PATH, + installation.path.join("share/aclocal").chuzzle()?.string, + ) } - if (installation.pkg.project === 'openssl.org') { + if (installation.pkg.project === "openssl.org") { const certPath = installation.path.join("ssl/cert.pem").chuzzle()?.string // this is a single file, so we assume a // valid entry is correct @@ -80,17 +85,20 @@ async function map({installations}: Options): Promise> } // pantry configured runtime environment - const runtime = await usePantry().project(installation.pkg).runtime.env(installation.pkg.version, installations) + const runtime = await usePantry().project(installation.pkg).runtime.env( + installation.pkg.version, + installations, + ) 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) { + // 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 @@ -119,32 +127,33 @@ async function map({installations}: Options): Promise> function suffixes(key: EnvKey) { switch (key) { - case 'PATH': + case "PATH": return ["bin", "sbin"] - case 'MANPATH': + case "MANPATH": return ["man", "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 + 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 = '' + let rv = "" for (const [key, value] of Object.entries(env)) { if (value.length == 0) continue rv += `export ${key}="${value.join(":")}"\n` @@ -168,23 +177,23 @@ function compact_add(set: OrderedSet | undefined, item: T | null | undefin } class OrderedSet { - private items: T[]; - private set: Set; + private items: T[] + private set: Set constructor() { - this.items = []; - this.set = new Set(); + this.items = [] + this.set = new Set() } add(item: T): void { if (!this.set.has(item)) { - this.items.push(item); - this.set.add(item); + this.items.push(item) + this.set.add(item) } } toArray(): T[] { - return [...this.items]; + return [...this.items] } isEmpty(): boolean { diff --git a/src/hooks/useSync.test.ts b/src/hooks/useSync.test.ts index 5d4983f..2933af6 100644 --- a/src/hooks/useSync.test.ts +++ b/src/hooks/useSync.test.ts @@ -3,7 +3,7 @@ import { assert } from "deno/testing/asserts.ts" import usePantry from "./usePantry.ts" import useSync from "./useSync.ts" -Deno.test("useSync", async runner => { +Deno.test("useSync", async (runner) => { await runner.step("w/o git", async () => { const TEA_PREFIX = Deno.makeTempDirSync() const conf = useTestConfig({ TEA_PREFIX, TEA_PANTRY_PATH: `${TEA_PREFIX}/tea.xyz/var/pantry` }) @@ -13,7 +13,11 @@ Deno.test("useSync", async runner => { await runner.step("w/git", async () => { const TEA_PREFIX = Deno.makeTempDirSync() - const conf = useTestConfig({ TEA_PREFIX, TEA_PANTRY_PATH: `${TEA_PREFIX}/tea.xyz/var/pantry`, PATH: "/usr/bin" }) + const conf = useTestConfig({ + TEA_PREFIX, + TEA_PANTRY_PATH: `${TEA_PREFIX}/tea.xyz/var/pantry`, + PATH: "/usr/bin", + }) assert(conf.git !== undefined) await test() diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts index 8fa5b5d..0b735ba 100644 --- a/src/hooks/useSync.ts +++ b/src/hooks/useSync.ts @@ -16,13 +16,13 @@ interface Logger { syncd(path: Path): void } -export default async function(logger?: Logger) { +export default async function (logger?: Logger) { const pantry_dir = usePantry().prefix.parent() logger?.syncing(pantry_dir) - const { rid } = await Deno.open(pantry_dir.mkdir('p').string) - await flock(rid, 'ex') + const { rid } = await Deno.open(pantry_dir.mkdir("p").string) + await flock(rid, "ex") try { //TODO if there was already a lock, just wait on it, don’t do the following stuff @@ -32,11 +32,17 @@ export default async function(logger?: Logger) { if (git_dir.join("HEAD").isFile()) { await git("-C", git_dir, "fetch", "--quiet", "origin", "--force", "main:main") } else { - await git("clone", "--quiet", "--bare", "--depth=1", "https://github.com/teaxyz/pantry", git_dir) + await git( + "clone", + "--quiet", + "--bare", + "--depth=1", + "https://github.com/teaxyz/pantry", + git_dir, + ) } await git("--git-dir", git_dir, "--work-tree", pantry_dir, "checkout", "--quiet", "--force") - } catch { // git failure or no git installed // ∴ download the latest tarball and uncompress over the top @@ -45,9 +51,9 @@ export default async function(logger?: Logger) { const proc = Deno.run({ cmd: ["tar", "xz", "--strip-components=1"], cwd: pantry_dir.string, - stdin: "piped" + stdin: "piped", }) - await useDownload().download({ src }, blob => writeAll(proc.stdin, blob)) + await useDownload().download({ src }, (blob) => writeAll(proc.stdin, blob)) proc.stdin.close() if (!(await proc.status()).success) { @@ -55,10 +61,9 @@ export default async function(logger?: Logger) { } proc.close() - } finally { - await flock(rid, 'un') - Deno.close(rid) // docs aren't clear if we need to do this or not + await flock(rid, "un") + Deno.close(rid) // docs aren't clear if we need to do this or not } logger?.syncd(pantry_dir) @@ -68,8 +73,8 @@ export default async function(logger?: Logger) { async function git(...args: (string | Path)[]) { const { git } = useConfig() - if (!git) throw new Error("no-git") // caught above to trigger http download instead - await run({cmd: [git, ...args]}) + if (!git) throw new Error("no-git") // caught above to trigger http download instead + await run({ cmd: [git, ...args] }) } export interface RunOptions { @@ -77,8 +82,8 @@ export interface RunOptions { } async function run(opts: RunOptions) { - const cmd = opts.cmd.map(x => `${x}`) - const proc = Deno.run({ ...opts, cmd, stdout: 'null', clearEnv: true }) + const cmd = opts.cmd.map((x) => `${x}`) + const proc = Deno.run({ ...opts, cmd, stdout: "null", clearEnv: true }) try { const exit = await proc.status() if (!exit.success) throw new Error(`run.exit(${exit.code})`) diff --git a/src/plumbing/hydrate.test.ts b/src/plumbing/hydrate.test.ts index 8001f18..27f0a07 100644 --- a/src/plumbing/hydrate.test.ts +++ b/src/plumbing/hydrate.test.ts @@ -5,38 +5,38 @@ import * as semver from "../utils/semver.ts" import hydrate from "./hydrate.ts" describe("hydrate()", () => { - it("hydrates.1", async function() { + it("hydrates.1", async function () { const pkgs = [ - { project: 'nodejs.org', constraint: new semver.Range('*') }, - { project: 'nodejs.org', constraint: new semver.Range('>=18.14') } + { 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') + assertEquals(rv1.toString(), ">=18.14") - const rv = await hydrate(pkgs, (_a: PackageRequirement, _b: boolean) => Promise.resolve([])) + 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') { + if (pkg.project === "nodejs.org") { nodes++ - assertEquals(pkg.constraint.toString(), '>=18.14') + assertEquals(pkg.constraint.toString(), ">=18.14") } } assertEquals(nodes, 1) }) - it("hydrates.2", async function() { + it("hydrates.2", async function () { const pkgs = [ - { project: 'pipenv.pypa.io', constraint: new semver.Range('*') }, - { project: 'python.org', constraint: new semver.Range('~3.9') } + { 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') { + if (pkg.project === "pipenv.pypa.io") { return Promise.resolve([ - { project: 'python.org', constraint: new semver.Range('>=3.7') } + { project: "python.org", constraint: new semver.Range(">=3.7") }, ]) } else { return Promise.resolve([]) @@ -45,8 +45,8 @@ describe("hydrate()", () => { let nodes = 0 for (const pkg of rv.pkgs) { - if (pkg.project === 'python.org') { - assertEquals(pkg.constraint.toString(), '~3.9') + if (pkg.project === "python.org") { + assertEquals(pkg.constraint.toString(), "~3.9") nodes++ } } @@ -54,16 +54,16 @@ describe("hydrate()", () => { assertEquals(nodes, 1) }) - it("hydrates.3", async function() { + it("hydrates.3", async function () { const pkgs = [ - { project: 'pipenv.pypa.io', constraint: new semver.Range('*') }, - { project: 'python.org', constraint: new semver.Range('~3.9') } + { 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') { + if (pkg.project === "pipenv.pypa.io") { return Promise.resolve([ - { project: 'python.org', constraint: new semver.Range('~3.9.1') } + { project: "python.org", constraint: new semver.Range("~3.9.1") }, ]) } else { return Promise.resolve([]) @@ -72,8 +72,8 @@ describe("hydrate()", () => { let nodes = 0 for (const pkg of rv.pkgs) { - if (pkg.project === 'python.org') { - assertEquals(pkg.constraint.toString(), '~3.9.1') + if (pkg.project === "python.org") { + assertEquals(pkg.constraint.toString(), "~3.9.1") nodes++ } } diff --git a/src/plumbing/hydrate.ts b/src/plumbing/hydrate.ts index dff956a..de6e90c 100644 --- a/src/plumbing/hydrate.ts +++ b/src/plumbing/hydrate.ts @@ -1,10 +1,9 @@ -import { PackageRequirement, Package } from "../types.ts" +import { Package, PackageRequirement } from "../types.ts" import * as semver from "../utils/semver.ts" import usePantry from "../hooks/usePantry.ts" import { is_what } from "../deps.ts" const { isArray } = is_what - //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 @@ -12,7 +11,6 @@ const { isArray } = is_what //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[] @@ -38,13 +36,12 @@ const get = (x: PackageRequirement) => usePantry().project(x).runtime.deps() export default async function hydrate( input: (PackageRequirement | Package)[] | (PackageRequirement | Package), get_deps: (pkg: PackageRequirement, dry: boolean) => Promise = get, -): Promise -{ +): Promise { if (!isArray(input)) input = [input] - const dry = condense(input.map(spec => { + const dry = condense(input.map((spec) => { if ("version" in spec) { - return {project: spec.project, constraint: new semver.Range(`=${spec.version}`)} + return { project: spec.project, constraint: new semver.Range(`=${spec.version}`) } } else { return spec } @@ -52,7 +49,7 @@ export default async function hydrate( const graph: Record = {} const bootstrap = new Set() - const initial_set = new Set(dry.map(x => x.project)) + const initial_set = new Set(dry.map((x) => x.project)) const stack: Node[] = [] // Starting the DFS loop for each package in the dry list @@ -96,23 +93,25 @@ export default async function hydrate( // Sorting and constructing the return value const pkgs = Object.values(graph) .sort((a, b) => b.count() - a.count()) - .map(({pkg}) => pkg) + .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)) + 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 + 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) + const found = out.find((x) => x.project === pkg.project) if (found) { found.constraint = semver.intersect(found.constraint, pkg.constraint) } else { @@ -122,7 +121,6 @@ function condense(pkgs: PackageRequirement[]) { return out } - /////////////////////////////////////////////////////////////////////////// lib class Node { parent: Node | undefined diff --git a/src/plumbing/install.test.ts b/src/plumbing/install.test.ts index 1b0a034..c0e89fb 100644 --- a/src/plumbing/install.test.ts +++ b/src/plumbing/install.test.ts @@ -5,10 +5,10 @@ import { stub } from "deno/testing/mock.ts" import SemVer from "../utils/semver.ts" import { Package } from "../types.ts" -Deno.test("install()", async runner => { +Deno.test("install()", async (runner) => { const pkg: Package = { project: "tea.xyz/brewkit", - version: new SemVer("0.30.0") + version: new SemVer("0.30.0"), } const conf = useTestConfig() @@ -16,7 +16,7 @@ Deno.test("install()", async runner => { await runner.step("download & install", async () => { // for coverage const logger = ConsoleLogger() - const stubber = stub(console, "error", x => assert(x)) + const stubber = stub(console, "error", (x) => assert(x)) const installation = await install(pkg, logger) @@ -44,7 +44,7 @@ Deno.test("install()", async runner => { Deno.test("install locks", async () => { const pkg: Package = { project: "tea.xyz/brewkit", - version: new SemVer("0.30.0") + version: new SemVer("0.30.0"), } const conf = useTestConfig() @@ -55,7 +55,7 @@ Deno.test("install locks", async () => { locking: () => {}, installed: () => {}, installing: () => assertFalse(unlocked_once), - unlocking: () => unlocked_once = true + unlocking: () => unlocked_once = true, } const installer1 = install(pkg, logger) diff --git a/src/plumbing/install.ts b/src/plumbing/install.ts index f9bbb99..6ba39e1 100644 --- a/src/plumbing/install.ts +++ b/src/plumbing/install.ts @@ -1,7 +1,7 @@ // deno-lint-ignore-file no-deprecated-deno-api // ^^ dnt doesn’t support Deno.Command yet so we’re stuck with the deprecated Deno.run for now -import { Package, Installation, StowageNativeBottle } from "../types.ts" +import { Installation, Package, StowageNativeBottle } from "../types.ts" import useOffLicense from "../hooks/useOffLicense.ts" import useDownload from "../hooks/useDownload.ts" import { flock } from "../utils/flock.deno.ts" @@ -21,13 +21,13 @@ export default async function install(pkg: Package, logger?: Logger): Promise { + logger: (info) => { logger?.downloading?.({ pkg, ...info }) total ??= info.total - } - }, blob => { + }, + }, (blob) => { n += blob.length hasher.update(blob) logger?.installing?.({ pkg, progress: total ? n / total : total }) @@ -78,7 +80,7 @@ export default async function install(pkg: Package, logger?: Logger): Promise { +Deno.test("plumbing.link", async (runner) => { const pkg: Package = { project: "tea.xyz/brewkit", - version: new SemVer("0.30.0") + version: new SemVer("0.30.0"), } await runner.step("link()", async () => { @@ -16,7 +16,7 @@ Deno.test("plumbing.link", async runner => { const installation = await install(pkg) await link(installation) - await link(installation) // test that calling twice serially works + await link(installation) // test that calling twice serially works /// test symlinks work assert(installation.path.parent().join("v*").isDirectory()) diff --git a/src/plumbing/link.ts b/src/plumbing/link.ts index afdd3eb..55cc98a 100644 --- a/src/plumbing/link.ts +++ b/src/plumbing/link.ts @@ -1,5 +1,5 @@ import SemVer, * as semver from "../utils/semver.ts" -import { Package, Installation } from "../types.ts" +import { Installation, Package } from "../types.ts" import useCellar from "../hooks/useCellar.ts" import { panic } from "../utils/error.ts" import fs from "node:fs/promises" @@ -11,11 +11,11 @@ export default async function link(pkg: Package | Installation) { const versions = (await useCellar() .ls(installation.pkg.project)) - .map(({pkg: {version}, path}) => [version, path] as [SemVer, Path]) - .sort(([a],[b]) => a.compare(b)) + .map(({ pkg: { version }, path }) => [version, path] as [SemVer, Path]) + .sort(([a], [b]) => a.compare(b)) if (versions.length <= 0) { - const err = new Error('no versions') + const err = new Error("no versions") err.cause = pkg throw err } @@ -24,7 +24,7 @@ export default async function link(pkg: Package | Installation) { 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() + 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 @@ -32,7 +32,7 @@ export default async function link(pkg: Package | Installation) { 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() + 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 @@ -40,7 +40,7 @@ export default async function link(pkg: Package | Installation) { await makeSymlink(`v${pkg.version.major}`) if (pkg.version.eq(newest[0])) { - await makeSymlink('v*') + await makeSymlink("v*") } async function makeSymlink(symname: string) { @@ -54,22 +54,23 @@ export default async function link(pkg: Package | Installation) { } catch (err) { // we were deleted by another thing linking simultaneously //FIXME our flock should surround the link step too - if (err.code != 'ENOENT') throw err + if (err.code != "ENOENT") throw err } } await Deno.symlink( - installation.path.basename(), // makes it relative + installation.path.basename(), // makes it relative shelf.join(symname).rm().string, - {type: 'dir'}) - } catch (err) { - if (err instanceof Deno.errors.AlreadyExists || err.code === 'EEXIST') { - //FIXME race condition for installing the same pkg simultaneously - // real fix is to lock around the entire download/untar/link process - return - } else { - throw err - } + { type: "dir" }, + ) + } catch (err) { + if (err instanceof Deno.errors.AlreadyExists || err.code === "EEXIST") { + //FIXME race condition for installing the same pkg simultaneously + // real fix is to lock around the entire download/untar/link process + return + } else { + throw err } + } } } diff --git a/src/plumbing/resolve.test.ts b/src/plumbing/resolve.test.ts index 9fc500d..a86eb1b 100644 --- a/src/plumbing/resolve.test.ts +++ b/src/plumbing/resolve.test.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file require-await -import { assert, assertEquals, fail, assertRejects } from "deno/assert/mod.ts" +import { assert, assertEquals, assertRejects, fail } from "deno/assert/mod.ts" import { Installation, Package, PackageRequirement } from "../types.ts" import { useTestConfig } from "../hooks/useTestConfig.ts" import useInventory from "../hooks/useInventory.ts" @@ -11,8 +11,12 @@ import SemVer from "../utils/semver.ts" import Path from "../utils/Path.ts" Deno.test("resolve cellar.has", { - permissions: {'read': true, 'env': ["TMPDIR", "TMP", "TEMP", "HOME"], 'write': [Deno.env.get("TMPDIR") || Deno.env.get("TMP") || Deno.env.get("TEMP") || "/tmp"] } -}, async runner => { + permissions: { + "read": true, + "env": ["TMPDIR", "TMP", "TEMP", "HOME"], + "write": [Deno.env.get("TMPDIR") || Deno.env.get("TMP") || Deno.env.get("TEMP") || "/tmp"], + }, +}, async (runner) => { const prefix = useTestConfig().prefix const pkg = { project: "foo", version: new SemVer("1.0.0") } @@ -20,9 +24,9 @@ Deno.test("resolve cellar.has", { const has = async (pkg_: Package | PackageRequirement | Path) => { if (pkg_ instanceof Path) fail() if (pkg.project == pkg_.project) { - if ('constraint' in pkg_ && !pkg_.constraint.satisfies(pkg.version)) return - if ('version' in pkg_ && !pkg_.version.eq(pkg.version)) return - const a: Installation = {pkg, path: prefix.join(pkg.project, `v${pkg.version}`) } + if ("constraint" in pkg_ && !pkg_.constraint.satisfies(pkg.version)) return + if ("version" in pkg_ && !pkg_.version.eq(pkg.version)) return + const a: Installation = { pkg, path: prefix.join(pkg.project, `v${pkg.version}`) } return a } } @@ -30,14 +34,15 @@ Deno.test("resolve cellar.has", { await runner.step("happy path", async () => { const stub1 = stub(_internals, "useInventory", () => ({ get: () => fail(), - select: () => Promise.resolve(pkg.version) + select: () => Promise.resolve(pkg.version), })) const stub2 = stub(_internals, "useCellar", () => ({ - ...cellar, has + ...cellar, + has, })) try { - const rv = await resolve([pkg]) + const rv = await resolve([pkg]) assertEquals(rv.pkgs[0].project, pkg.project) assertEquals(rv.installed[0].pkg.project, pkg.project) } finally { @@ -53,7 +58,7 @@ Deno.test("resolve cellar.has", { })) const stub2 = stub(_internals, "useCellar", () => ({ ...cellar, - has: () => Promise.resolve(undefined) + has: () => Promise.resolve(undefined), })) let errord = false @@ -74,7 +79,8 @@ Deno.test("resolve cellar.has", { select: () => Promise.resolve(pkg.version), })) const stub2 = stub(_internals, "useCellar", () => ({ - ...cellar, has + ...cellar, + has, })) try { @@ -87,38 +93,51 @@ Deno.test("resolve cellar.has", { } }) - await runner.step("updates version if latest is not installed when update is set", async runner => { - const stub1 = stub(_internals, "useInventory", () => ({ - get: () => fail(), - select: () => Promise.resolve(new SemVer("1.0.1")), - })) - const stub2 = stub(_internals, "useCellar", () => ({ - ...cellar, has - })) - - try { - await runner.step("update: true", async () => { - const rv = await resolve([{ project: pkg.project, constraint: new semver.Range("^1") }], { update: true }) - assertEquals(rv.pkgs[0].project, pkg.project) - assertEquals(rv.pending[0].project, pkg.project) - assertEquals(rv.pending[0].version, new SemVer("1.0.1")) - }) - - await runner.step("update: set", async () => { - const update = new Set([pkg.project]) - const rv = await resolve([{ project: pkg.project, constraint: new semver.Range("^1") }], { update }) - assertEquals(rv.pkgs[0].project, pkg.project) - assertEquals(rv.pending[0].project, pkg.project) - assertEquals(rv.pending[0].version, new SemVer("1.0.1")) - }) - } finally { - stub1.restore() - stub2.restore() - } - }) + await runner.step( + "updates version if latest is not installed when update is set", + async (runner) => { + const stub1 = stub(_internals, "useInventory", () => ({ + get: () => fail(), + select: () => Promise.resolve(new SemVer("1.0.1")), + })) + const stub2 = stub(_internals, "useCellar", () => ({ + ...cellar, + has, + })) + + try { + await runner.step("update: true", async () => { + const rv = await resolve([{ project: pkg.project, constraint: new semver.Range("^1") }], { + update: true, + }) + assertEquals(rv.pkgs[0].project, pkg.project) + assertEquals(rv.pending[0].project, pkg.project) + assertEquals(rv.pending[0].version, new SemVer("1.0.1")) + }) + + await runner.step("update: set", async () => { + const update = new Set([pkg.project]) + const rv = await resolve([{ project: pkg.project, constraint: new semver.Range("^1") }], { + update, + }) + assertEquals(rv.pkgs[0].project, pkg.project) + assertEquals(rv.pending[0].project, pkg.project) + assertEquals(rv.pending[0].version, new SemVer("1.0.1")) + }) + } finally { + stub1.restore() + stub2.restore() + } + }, + ) }) -const permissions = { net: false, read: true, env: ["TMPDIR", "HOME", "TMP", "TEMP"], write: true /*FIXME*/ } +const permissions = { + net: false, + read: true, + env: ["TMPDIR", "HOME", "TMP", "TEMP"], + write: true, /*FIXME*/ +} // https://github.com/teaxyz/cli/issues/655 Deno.test("postgres@500 fails", { permissions }, async () => { @@ -126,7 +145,7 @@ Deno.test("postgres@500 fails", { permissions }, async () => { const pkg = { project: "posqtgres.org", - version: new SemVer("15.0.1") + version: new SemVer("15.0.1"), } const select = useInventory().select @@ -136,7 +155,7 @@ Deno.test("postgres@500 fails", { permissions }, async () => { })) const pkgs = [ - { project: pkg.project, constraint: new semver.Range('@500') } + { project: pkg.project, constraint: new semver.Range("@500") }, ] try { @@ -151,14 +170,14 @@ Deno.test("postgres@500 fails", { permissions }, async () => { Deno.test("postgres@500 fails if installed", { permissions }, async () => { const pkg = { project: "posqtgres.org", - version: new SemVer("15.0.1") + version: new SemVer("15.0.1"), } const prefix = useTestConfig().prefix const cellar = useCellar() const has = (b: Path | Package | PackageRequirement) => { if ("constraint" in b && b.constraint.satisfies(pkg.version)) { - const a: Installation = {pkg, path: prefix.join(pkg.project, `v${pkg.version}`) } + const a: Installation = { pkg, path: prefix.join(pkg.project, `v${pkg.version}`) } return Promise.resolve(a) } else { return Promise.resolve(undefined) @@ -172,11 +191,11 @@ Deno.test("postgres@500 fails if installed", { permissions }, async () => { })) const stub2 = stub(_internals, "useCellar", () => ({ ...cellar, - has + has, })) const pkgs = [ - { project: pkg.project, constraint: new semver.Range('@500') } + { project: pkg.project, constraint: new semver.Range("@500") }, ] try { diff --git a/src/plumbing/resolve.ts b/src/plumbing/resolve.ts index d3106b7..bc93071 100644 --- a/src/plumbing/resolve.ts +++ b/src/plumbing/resolve.ts @@ -1,4 +1,4 @@ -import { Package, PackageRequirement, Installation } from "../types.ts" +import { Installation, Package, PackageRequirement } from "../types.ts" import useInventory from "../hooks/useInventory.ts" import { str as pkgstr } from "../utils/pkg.ts" import useCellar from "../hooks/useCellar.ts" @@ -32,7 +32,10 @@ export class ResolveError extends TeaError { /// 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 | Set} = {update: false}): Promise { +export default async function resolve( + reqs: (Package | PackageRequirement)[], + { update }: { update: boolean | Set } = { update: false }, +): Promise { const inventory = _internals.useInventory() const cellar = _internals.useCellar() const rv: Resolution = { pkgs: [], installed: [], pending: [] } @@ -47,7 +50,7 @@ export default async function resolve(reqs: (Package | PackageRequirement)[], {u rv.installed.push(installation) rv.pkgs.push(installation.pkg) } else { - const promise = inventory.select(req).then(async version => { + const promise = inventory.select(req).then(async (version) => { if (!version) { throw new ResolveError(req) } @@ -76,5 +79,5 @@ export default async function resolve(reqs: (Package | PackageRequirement)[], {u export const _internals = { useInventory, - useCellar + useCellar, } diff --git a/src/plumbing/which.test.ts b/src/plumbing/which.test.ts index 8b71c63..9594472 100644 --- a/src/plumbing/which.test.ts +++ b/src/plumbing/which.test.ts @@ -3,22 +3,22 @@ import { isArray } from "is-what" import which from "./which.ts" Deno.test("which('ls')", async () => { - const foo = await which('ls') + const foo = await which("ls") assert(!isArray(foo)) assert(foo) }) Deno.test("which('kill-port')", async () => { - const foo = await which('kill-port') + const foo = await which("kill-port") assert(!isArray(foo)) assert(foo) - const bar = await which('kill-port', { providers: false }) + const bar = await which("kill-port", { providers: false }) assertEquals(bar, undefined) }) Deno.test("which('nvim')", async () => { - const foo = await which('kill-port', { all: true }) + const foo = await which("kill-port", { all: true }) assert(isArray(foo)) assert(foo.length) }) diff --git a/src/plumbing/which.ts b/src/plumbing/which.ts index c69d47d..1468128 100644 --- a/src/plumbing/which.ts +++ b/src/plumbing/which.ts @@ -6,12 +6,19 @@ export type WhichResult = PackageRequirement & { shebang: string[] } - -export default async function which(arg0: string, opts?: { providers?: boolean }): Promise; -export default async function which(arg0: string, opts: { providers?: boolean, all: false }): Promise; -export default async function which(arg0: string, opts: { providers?: boolean, all: true }): Promise; -export default async function which(arg0: string, opts_?: { providers?: boolean, all?: boolean }) { - +export default async function which( + arg0: string, + opts?: { providers?: boolean }, +): Promise +export default async function which( + arg0: string, + opts: { providers?: boolean; all: false }, +): Promise +export default async function which( + arg0: string, + opts: { providers?: boolean; all: true }, +): Promise +export default async function which(arg0: string, opts_?: { providers?: boolean; all?: boolean }) { const opts = { providers: opts_?.providers ?? true, all: opts_?.all ?? false } const rv: WhichResult[] = [] @@ -29,7 +36,7 @@ export default async function which(arg0: string, opts_?: { providers?: boolean, } } -async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerator { +async function* _which(arg0: string, opts: { providers: boolean }): AsyncGenerator { arg0 = arg0.trim() /// sanitize and reject anything with path components if (!arg0 || arg0.includes("/")) return @@ -43,16 +50,16 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat for (const f of found) yield f found = [] } - const p = pantry.project(entry).provides().then(providers => { + const p = pantry.project(entry).provides().then((providers) => { for (const provider of providers) { if (provider == arg0) { const constraint = new semver.Range("*") - found.push({...entry, constraint, shebang: [provider] }) + found.push({ ...entry, constraint, shebang: [provider] }) } else if (arg0.startsWith(provider)) { // eg. `node^16` symlink try { const constraint = new semver.Range(arg0.substring(provider.length)) - found.push({...entry, constraint, shebang: [provider] }) + found.push({ ...entry, constraint, shebang: [provider] }) } catch { // not a valid semver range; fallthrough } @@ -64,13 +71,13 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat let rx = /({{\s*version\.(marketing|major)\s*}})/ let match = provider.match(rx) if (!match?.index) continue - const regx = match[2] == 'major' ? '\\d+' : '\\d+\\.\\d+' + const regx = match[2] == "major" ? "\\d+" : "\\d+\\.\\d+" const foo = subst(match.index, match.index + match[1].length, provider, `(${regx})`) rx = new RegExp(`^${foo}$`) match = arg0.match(rx) if (match) { const constraint = new semver.Range(`~${match[1]}`) - found.push({...entry, constraint, shebang: [arg0] }) + found.push({ ...entry, constraint, shebang: [arg0] }) } } } @@ -79,14 +86,16 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat promises.push(p) if (opts.providers) { - const pp = pantry.project(entry).provider().then(f => { + const pp = pantry.project(entry).provider().then((f) => { if (!f) return const rv = f(arg0) - if (rv) found.push({ - ...entry, - constraint: new semver.Range('*'), - shebang: [...rv, arg0] - }) + if (rv) { + found.push({ + ...entry, + constraint: new semver.Range("*"), + shebang: [...rv, arg0], + }) + } }) promises.push(pp) } @@ -101,6 +110,6 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat } } -const subst = function(start: number, end: number, input: string, what: string) { +const subst = function (start: number, end: number, input: string, what: string) { return input.substring(0, start) + what + input.substring(end) } diff --git a/src/porcelain/install.test.ts b/src/porcelain/install.test.ts index b07a0eb..237297a 100644 --- a/src/porcelain/install.test.ts +++ b/src/porcelain/install.test.ts @@ -8,7 +8,7 @@ import type { Package } from "../types.ts" Deno.test("porcelain.install.1", async () => { useTestConfig() const installations = await install("tea.xyz/brewkit") - const projects = new Set(installations.map(x => x.pkg.project)) + const projects = new Set(installations.map((x) => x.pkg.project)) assert(projects.has("tea.xyz/brewkit")) }) @@ -20,14 +20,14 @@ Deno.test("porcelain.install.2", async () => { Deno.test("porcelain.install.3", async () => { useTestConfig() const installations = await install(["tea.xyz/brewkit@0.31", "zlib.net"]) - const projects = new Set(installations.map(x => x.pkg.project)) + const projects = new Set(installations.map((x) => x.pkg.project)) assert(projects.has("tea.xyz/brewkit")) assert(projects.has("zlib.net")) }) Deno.test("porcelain.install.4", async () => { useTestConfig() - await install([{ project: 'tea.xyz/brewkit', constraint: new semver.Range("^0.31") }]) + await install([{ project: "tea.xyz/brewkit", constraint: new semver.Range("^0.31") }]) }) Deno.test("porcelain.install.resolved", async () => { @@ -36,11 +36,11 @@ Deno.test("porcelain.install.resolved", async () => { let resolution: Resolution = { pkgs: [] as Package[] } as Resolution const logger = { ...ConsoleLogger(), - resolved: (r: Resolution) => resolution = r + resolved: (r: Resolution) => resolution = r, } await install("tea.xyz/brewkit^0.32", logger) const resolvedProjects = resolution.pkgs.map((p: Package) => p.project) - assertArrayIncludes(resolvedProjects, [ "deno.land", "gnu.org/bash", "tea.xyz", "tea.xyz/brewkit"]) + assertArrayIncludes(resolvedProjects, ["deno.land", "gnu.org/bash", "tea.xyz", "tea.xyz/brewkit"]) }) diff --git a/src/porcelain/install.ts b/src/porcelain/install.ts index bf97a75..2aa4910 100644 --- a/src/porcelain/install.ts +++ b/src/porcelain/install.ts @@ -1,4 +1,7 @@ -import install, { Logger as BaseLogger, ConsoleLogger as BaseConsoleLogger } from "../plumbing/install.ts" +import install, { + ConsoleLogger as BaseConsoleLogger, + Logger as BaseLogger, +} from "../plumbing/install.ts" import { Installation, PackageSpecification } from "../types.ts" import resolve, { Resolution } from "../plumbing/resolve.ts" import usePantry from "../hooks/usePantry.ts" @@ -22,15 +25,19 @@ export function ConsoleLogger(prefix?: any): Logger { prefix = prefix ? `${prefix}: ` : "" return { ...BaseConsoleLogger(prefix), - progress: function() { console.error(`${prefix}progress`, ...arguments) }, + progress: function () { + console.error(`${prefix}progress`, ...arguments) + }, } } /// eg. install("python.org~3.10") -export default async function(pkgs: PackageSpecification[] | string[] | string, logger?: Logger): Promise { - +export default async function ( + pkgs: PackageSpecification[] | string[] | string, + logger?: Logger, +): Promise { if (isString(pkgs)) pkgs = pkgs.split(/\s+/) - pkgs = pkgs.map(pkg => isString(pkg) ? parse(pkg) : pkg) + pkgs = pkgs.map((pkg) => isString(pkg) ? parse(pkg) : pkg) const pantry = usePantry() @@ -47,8 +54,10 @@ export default async function(pkgs: PackageSpecification[] | string[] | string, const { pending, installed } = resolution logger = WrapperLogger(pending, logger) const installers = pending - .map(pkg => install(pkg, logger) - .then(i => link(i).then(() => i))) + .map((pkg) => + install(pkg, logger) + .then((i) => link(i).then(() => i)) + ) installed.push(...await Promise.all(installers)) @@ -58,13 +67,13 @@ export default async function(pkgs: PackageSpecification[] | string[] | string, function WrapperLogger(pending: PackageSpecification[], logger?: Logger): Logger | undefined { if (!logger?.progress) return logger - const projects = pending.map(pkg => pkg.project) + const projects = pending.map((pkg) => pkg.project) const totals: Record = {} const progresses: Record = {} return { ...logger, - downloading: args => { - const { pkg: {project}, total } = args + downloading: (args) => { + const { pkg: { project }, total } = args if (total) { totals[project] = total updateProgress() @@ -73,8 +82,8 @@ function WrapperLogger(pending: PackageSpecification[], logger?: Logger): Logger logger.downloading(args) } }, - installing: args => { - const { pkg: {project}, progress } = args + installing: (args) => { + const { pkg: { project }, progress } = args if (progress) { progresses[project] = progress updateProgress() @@ -82,7 +91,7 @@ function WrapperLogger(pending: PackageSpecification[], logger?: Logger): Logger if (logger?.installing) { logger.installing(args) } - } + }, } function updateProgress() { diff --git a/src/porcelain/run.test.ts b/src/porcelain/run.test.ts index 7c634e6..904e5fe 100644 --- a/src/porcelain/run.test.ts +++ b/src/porcelain/run.test.ts @@ -2,12 +2,15 @@ import { assertEquals, assertMatch, assertRejects } from "deno/testing/asserts.t import { useTestConfig } from "../hooks/useTestConfig.ts" import run from "./run.ts" -Deno.test("porcelain.run", async runner => { +Deno.test("porcelain.run", async (runner) => { await runner.step("std", async () => { useTestConfig() - const { stdout, stderr, status } = await run(`python -c 'print(1)'`) as unknown as - { stdout: string, stderr: string, status: number } - // ^^ type system hack to ensure we don’t actually capture the stdout/stderr + const { stdout, stderr, status } = await run(`python -c 'print(1)'`) as unknown as { + stdout: string + stderr: string + status: number + } + // ^^ type system hack to ensure we don’t actually capture the stdout/stderr assertEquals(stdout, "") assertEquals(stderr, "") assertEquals(status, 0) @@ -16,28 +19,34 @@ Deno.test("porcelain.run", async runner => { // we had a scenario where no args would truncate the cmd-name await runner.step("no args works", async () => { useTestConfig() - const { stdout, stderr, status } = await run(`ls`) as unknown as { stdout: string, stderr: string, status: number } + const { stdout, stderr, status } = await run(`ls`) as unknown as { + stdout: string + stderr: string + status: number + } assertEquals(stdout, "") assertEquals(stderr, "") assertEquals(status, 0) }) - await runner.step("node^16", async runner => { + await runner.step("node^16", async (runner) => { useTestConfig() await runner.step("string", async () => { - const { stdout } = await run('node^16 --version', { stdout: true }) + const { stdout } = await run("node^16 --version", { stdout: true }) assertMatch(stdout, /^v16\./) }) await runner.step("array", async () => { - const { stdout } = await run(['node^16', '--version'], { stdout: true }) + const { stdout } = await run(["node^16", "--version"], { stdout: true }) assertMatch(stdout, /^v16\./) }) }) await runner.step("env", async () => { useTestConfig() - await run(['node', '-e', 'if (process.env.FOO !== "FOO") throw new Error()'], { env: { FOO: "FOO" }}) + await run(["node", "-e", 'if (process.env.FOO !== "FOO") throw new Error()'], { + env: { FOO: "FOO" }, + }) }) await runner.step("status", async () => { @@ -53,19 +62,29 @@ Deno.test("porcelain.run", async runner => { await runner.step("stdout", async () => { useTestConfig() - const { stdout } = await run(['python', '-c', "import os; print(os.getenv('FOO'))"], { stdout: true, env: { FOO: "FOO" } }) + const { stdout } = await run(["python", "-c", "import os; print(os.getenv('FOO'))"], { + stdout: true, + env: { FOO: "FOO" }, + }) assertEquals(stdout, "FOO\n") }) await runner.step("stderr", async () => { useTestConfig() - const { stderr } = await run(['node', '-e', "console.error(process.env.FOO)"], { stderr: true, env: { FOO: "BAR" } }) + const { stderr } = await run(["node", "-e", "console.error(process.env.FOO)"], { + stderr: true, + env: { FOO: "BAR" }, + }) assertEquals(stderr, "BAR\n") }) await runner.step("all", async () => { useTestConfig() - const { stderr, stdout, status } = await run(['node', '-e', "console.error(1); console.log(2); process.exit(3)"], { stderr: true, stdout: true, status: true }) + const { stderr, stdout, status } = await run([ + "node", + "-e", + "console.error(1); console.log(2); process.exit(3)", + ], { stderr: true, stdout: true, status: true }) assertEquals(stderr, "1\n") assertEquals(stdout, "2\n") assertEquals(status, 3) diff --git a/src/porcelain/run.ts b/src/porcelain/run.ts index 2f239a9..d15e933 100644 --- a/src/porcelain/run.ts +++ b/src/porcelain/run.ts @@ -1,6 +1,6 @@ import install, { Logger } from "../plumbing/install.ts" -import useShellEnv from '../hooks/useShellEnv.ts' -import usePantry from '../hooks/usePantry.ts' +import useShellEnv from "../hooks/useShellEnv.ts" +import usePantry from "../hooks/usePantry.ts" import hydrate from "../plumbing/hydrate.ts" import resolve from "../plumbing/resolve.ts" import { TeaError } from "../utils/error.ts" @@ -27,20 +27,45 @@ type Cmd = string | (string | Path)[] /// if you pass a single string we call that string via /bin/sh /// if you don’t want that pass an array of args -export default async function run(cmd: Cmd, opts?: OptsEx): Promise; -export default async function run(cmd: Cmd, opts: {stdout: true} & OptsEx): Promise<{ stdout: string }>; -export default async function run(cmd: Cmd, opts: {stderr: true} & OptsEx): Promise<{ stderr: string }>; -export default async function run(cmd: Cmd, opts: {status: true} & OptsEx): Promise<{ status: number }>; -export default async function run(cmd: Cmd, opts: {stdout: true, stderr: true} & OptsEx): Promise<{ stdout: string, stderr: string }>; -export default async function run(cmd: Cmd, opts: {stdout: true, status: true} & OptsEx): Promise<{ stdout: string, status: number }>; -export default async function run(cmd: Cmd, opts: {stderr: true, status: true} & OptsEx): Promise<{ stderr: string, status: number }>; -export default async function run(cmd: Cmd, opts: {stdout: true, stderr: true, status: true } & OptsEx): Promise<{ stdout: string, stderr: string, status: number }>; -export default async function run(cmd: Cmd, opts?: Options): Promise { - +export default async function run(cmd: Cmd, opts?: OptsEx): Promise +export default async function run( + cmd: Cmd, + opts: { stdout: true } & OptsEx, +): Promise<{ stdout: string }> +export default async function run( + cmd: Cmd, + opts: { stderr: true } & OptsEx, +): Promise<{ stderr: string }> +export default async function run( + cmd: Cmd, + opts: { status: true } & OptsEx, +): Promise<{ status: number }> +export default async function run( + cmd: Cmd, + opts: { stdout: true; stderr: true } & OptsEx, +): Promise<{ stdout: string; stderr: string }> +export default async function run( + cmd: Cmd, + opts: { stdout: true; status: true } & OptsEx, +): Promise<{ stdout: string; status: number }> +export default async function run( + cmd: Cmd, + opts: { stderr: true; status: true } & OptsEx, +): Promise<{ stderr: string; status: number }> +export default async function run( + cmd: Cmd, + opts: { stdout: true; stderr: true; status: true } & OptsEx, +): Promise<{ stdout: string; stderr: string; status: number }> +export default async function run( + cmd: Cmd, + opts?: Options, +): Promise< + void | { stdout?: string | undefined; stderr?: string | undefined; status?: number | undefined } +> { const { usesh, arg0: whom } = (() => { if (!isArray(cmd)) { const s = cmd.trim() - const i = s.indexOf(' ') + const i = s.indexOf(" ") if (i == -1) { cmd = [] return { usesh: false, arg0: s } @@ -50,37 +75,37 @@ export default async function run(cmd: Cmd, opts?: Options): Promise x.toString())] + ? ["-c", `${shebang.join(" ")} ${cmd}`] + : [...shebang, ...(cmd as (string | Path)[]).map((x) => x.toString())] return new Promise((resolve, reject) => { const proc = spawn(arg0, args, { env, stdio: [ "pipe", - opts?.stdout ? 'pipe' : 'inherit', - opts?.stderr ? 'pipe' : 'inherit' - ] + opts?.stdout ? "pipe" : "inherit", + opts?.stderr ? "pipe" : "inherit", + ], }) - let stdout = '', stderr = '' - proc.stdout?.on('data', data => stdout += data) - proc.stderr?.on('data', data => stderr += data) - proc.on('close', status => { + let stdout = "", stderr = "" + proc.stdout?.on("data", (data) => stdout += data) + proc.stderr?.on("data", (data) => stderr += data) + proc.on("close", (status) => { if (status && !opts?.status) { - const err = new RunError('EIO', `${cmd} exited with: ${status}`) + const err = new RunError("EIO", `${cmd} exited with: ${status}`) err.cause = status reject(err) } else { @@ -91,7 +116,11 @@ export default async function run(cmd: Cmd, opts?: Options): Promise, logger: Logger | undefined) { +async function setup( + cmd: string, + env: Record, + logger: Logger | undefined, +) { const pantry = usePantry() const sh = useShellEnv() @@ -100,7 +129,7 @@ async function setup(cmd: string, env: Record, logge } const wut = await which(cmd) - if (!wut) throw new RunError('ENOENT', `No project in pantry provides ${cmd}`) + if (!wut) throw new RunError("ENOENT", `No project in pantry provides ${cmd}`) const { pkgs } = await hydrate(wut) const { pending, installed } = await resolve(pkgs) @@ -125,8 +154,7 @@ async function setup(cmd: string, env: Record, logge return { env: sh.flatten(pkgenv), shebang: wut.shebang } } - -type RunErrorCode = 'ENOENT' | 'EUSAGE' | 'EIO' +type RunErrorCode = "ENOENT" | "EUSAGE" | "EIO" export class RunError extends TeaError { code: RunErrorCode diff --git a/src/types.ts b/src/types.ts index 4adc232..1757893 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import host, { SupportedPlatform, SupportedArchitecture } from "./utils/host.ts" +import host, { SupportedArchitecture, SupportedPlatform } from "./utils/host.ts" import SemVer, { Range } from "./utils/semver.ts" import Path from "./utils/Path.ts" @@ -21,19 +21,19 @@ export interface Installation { /// remotely available package content (bottles or source tarball) export type Stowage = { - type: 'src' + type: "src" pkg: Package extname: string } | { - type: 'bottle' + type: "bottle" pkg: Package - compression: 'xz' | 'gz' - host?: { platform: SupportedPlatform, arch: SupportedArchitecture } + 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' } +export function StowageNativeBottle(opts: { pkg: Package; compression: "xz" | "gz" }): Stowage { + return { ...opts, host: host(), type: "bottle" } } diff --git a/src/utils/Path.test.ts b/src/utils/Path.test.ts index 673ec57..910aa2a 100644 --- a/src/utils/Path.test.ts +++ b/src/utils/Path.test.ts @@ -1,17 +1,17 @@ import { assert, assertEquals, assertFalse, assertThrows } from "deno/testing/asserts.ts" import Path from "./Path.ts" -Deno.test("test Path", async test => { +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-"}) + const tmp = Path.mktemp({ prefix: "tea-" }) assert(tmp.isEmpty()) const child = tmp.join("a/b/c") assertFalse(child.parent().isDirectory()) - child.parent().mkdir('p') + child.parent().mkdir("p") assert(child.parent().isDirectory()) assertThrows(() => child.readlink()) // not found @@ -23,15 +23,14 @@ Deno.test("test Path", async test => { assertFalse(tmp.isEmpty()) assertEquals(child.readlink(), child) // not a link - assertEquals(new Path("/").string, "/") }) await test.step("write and read", async () => { - const tmp = Path.mktemp({prefix: "tea-"}) + const tmp = Path.mktemp({ prefix: "tea-" }) const data = tmp.join("test.dat") - data.write({text: "hello\nworld"}) + data.write({ text: "hello\nworld" }) const lines = await asyncIterToArray(data.readLines()) assertEquals(lines, ["hello", "world"]) @@ -44,7 +43,7 @@ Deno.test("test Path", async test => { }) await test.step("test walk", async () => { - const tmp = Path.mktemp({prefix: "tea-"}) + const tmp = Path.mktemp({ prefix: "tea-" }) const a = tmp.join("a").mkdir() a.join("a1").touch() @@ -63,28 +62,28 @@ Deno.test("test Path", async test => { const walked = (await asyncIterToArray(tmp.walk())) .map(([path, entry]) => { - return {name: path.basename(), isDir: entry.isDirectory} + 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}, + { 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 }, ]) }) await test.step("test symlink created", () => { - const tmp = Path.mktemp({prefix: "tea-"}).join("foo").mkdir() + const tmp = Path.mktemp({ prefix: "tea-" }).join("foo").mkdir() const a = tmp.join("a").touch() const b = tmp.join("b") - b.ln('s', { target: a }) + b.ln("s", { target: a }) assertEquals(b.readlink(), a) assert(b.isSymlink()) }) @@ -114,7 +113,7 @@ Deno.test("Path.join()", () => { }) Deno.test("Path.isExecutableFile()", () => { - const tmp = Path.mktemp({prefix: "tea-"}).mkdir() + const tmp = Path.mktemp({ prefix: "tea-" }).mkdir() const executable = tmp.join("executable").touch() executable.chmod(0o755) const notExecutable = tmp.join("not-executable").touch() @@ -129,7 +128,7 @@ Deno.test("Path.extname()", () => { }) Deno.test("Path.mv()", () => { - const tmp = Path.mktemp({prefix: "tea-"}) + const tmp = Path.mktemp({ prefix: "tea-" }) const a = tmp.join("a").touch() const b = tmp.join("b") @@ -150,7 +149,7 @@ Deno.test("Path.mv()", () => { }) Deno.test("Path.cp()", () => { - const tmp = Path.mktemp({prefix: "tea-"}).mkdir() + const tmp = Path.mktemp({ prefix: "tea-" }).mkdir() const a = tmp.join("a").touch() const b = tmp.join("b").mkdir() @@ -167,9 +166,9 @@ Deno.test("Path.relative()", () => { }) Deno.test("Path.realpath()", () => { - const tmp = Path.mktemp({prefix: "tea-"}).mkdir() + const tmp = Path.mktemp({ prefix: "tea-" }).mkdir() const a = tmp.join("a").touch() - const b = tmp.join("b").ln('s', { target: a }) + const b = tmp.join("b").ln("s", { target: a }) assertEquals(b.realpath(), a.realpath()) }) @@ -190,21 +189,21 @@ Deno.test("Path.chuzzle()", () => { }) Deno.test("Path.ls()", async () => { - const tmp = Path.mktemp({prefix: "tea-"}).mkdir() + const tmp = Path.mktemp({ prefix: "tea-" }).mkdir() tmp.join("a").touch() tmp.join("b").touch() tmp.join("c").mkdir() - const entries = (await asyncIterToArray(tmp.ls())).map(([,{name}]) => name) + const entries = (await asyncIterToArray(tmp.ls())).map(([, { name }]) => name) assertEquals(entries.sort(), ["a", "b", "c"]) }) -async function asyncIterToArray (iter: AsyncIterable){ - const result = []; - for await(const i of iter) { - result.push(i); +async function asyncIterToArray(iter: AsyncIterable) { + const result = [] + for await (const i of iter) { + result.push(i) } - return result; + return result } Deno.test("ctor throws", () => { @@ -212,4 +211,4 @@ Deno.test("ctor throws", () => { assertThrows(() => new Path(" ")) assertThrows(() => new Path(" \n ")) assertThrows(() => new Path(" / ")) -}) \ No newline at end of file +}) diff --git a/src/utils/Path.ts b/src/utils/Path.ts index a52e8c8..5d38473 100644 --- a/src/utils/Path.ts +++ b/src/utils/Path.ts @@ -28,13 +28,14 @@ export default class Path { static home(): Path { return new Path( (() => { - switch (Deno.build.os) { - case "windows": - return Deno.env.get("USERPROFILE")! - default: - return Deno.env.get("HOME")! - } - })()) + switch (Deno.build.os) { + case "windows": + return Deno.env.get("USERPROFILE")! + default: + return Deno.env.get("HOME")! + } + })(), + ) } /// normalizes the path @@ -42,13 +43,15 @@ export default class Path { constructor(input: string | Path) { if (input instanceof Path) { this.string = input.string - } else if (!input || input[0] != '/') { + } else if (!input || input[0] != "/") { throw new Error(`invalid absolute path: ${input}`) } else { this.string = sys.normalize(input) // ^^ seemingly doesn’t normalize trailing slashes away - if (this.string != "/") while (this.string.endsWith("/")) { - this.string = this.string.slice(0, -1) + if (this.string != "/") { + while (this.string.endsWith("/")) { + this.string = this.string.slice(0, -1) + } } } } @@ -78,10 +81,10 @@ export default class Path { } catch (err) { const code = err.code switch (code) { - case 'EINVAL': - return this // is file - case 'ENOENT': - throw err // there is no symlink at this path + case "EINVAL": + return this // is file + case "ENOENT": + throw err // there is no symlink at this path } throw err } @@ -106,8 +109,8 @@ export default class Path { /// 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] == '/') { + const joined = components.filter((x) => x).join("/") + if (joined[0] == "/") { return new Path(joined) } else if (joined) { return new Path(`${this.string}/${joined}`) @@ -205,16 +208,16 @@ export default class Path { } components(): string[] { - return this.string.split('/') + return this.string.split("/") } - static mktemp(opts?: { prefix?: string, dir?: Path }): Path { - let {prefix, dir} = opts ?? {} + static mktemp(opts?: { prefix?: string; dir?: Path }): Path { + let { prefix, dir } = opts ?? {} dir ??= new Path(os.tmpdir()) prefix ??= "" - if (!prefix.startsWith('/')) prefix = `/${prefix}` + if (!prefix.startsWith("/")) prefix = `/${prefix}` // not using deno.makeTempDirSync because it's bugg’d and the node shim doesn’t handler `dir` - const rv = mkdtempSync(`${dir.mkdir('p')}${prefix}`) + const rv = mkdtempSync(`${dir.mkdir("p")}${prefix}`) return new Path(rv) } @@ -254,7 +257,7 @@ export default class Path { 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 { + 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 @@ -267,13 +270,13 @@ export default class Path { //FIXME operates in ”force” mode //TODO needs a recursive option - cp({into}: {into: Path}): Path { + cp({ into }: { into: Path }): Path { const dst = into.join(this.basename()) Deno.copyFileSync(this.string, dst.string) return dst } - rm({recursive} = {recursive: false}) { + rm({ recursive } = { recursive: false }) { if (this.exists()) { try { Deno.removeSync(this.string, { recursive }) @@ -285,12 +288,12 @@ export default class Path { } } } - return this // may seem weird but I've had cases where I wanted to chain + return this // may seem weird but I've had cases where I wanted to chain } - mkdir(opts?: 'p'): Path { + mkdir(opts?: "p"): Path { if (!this.isDirectory()) { - Deno.mkdirSync(this.string, { recursive: opts == 'p' }) + Deno.mkdirSync(this.string, { recursive: opts == "p" }) } return this } @@ -313,7 +316,7 @@ export default class Path { /// `this` is the symlink that is created pointing at `target` /// in Path.ts we always create `this`, our consistency helps with the notoriously difficuly argument order of `ln -s` /// note symlink is full and absolute path - ln(_: 's', {target}: { target: Path }): Path { + ln(_: "s", { target }: { target: Path }): Path { Deno.symlinkSync(target.string, this.string) return this } @@ -325,10 +328,10 @@ export default class Path { async *readLines(): AsyncIterableIterator { const fd = Deno.openSync(this.string) try { - for await (const line of readLines(fd)) + for await (const line of readLines(fd)) { yield line } - finally { + } finally { fd.close() } } @@ -347,10 +350,14 @@ export default class Path { } readJSON(): Promise { - return this.read().then(x => JSON.parse(x)) + return this.read().then((x) => JSON.parse(x)) } - write({ force, ...content }: ({text: string} | {json: PlainObject, space?: number}) & {force?: boolean}): Path { + 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() @@ -366,7 +373,7 @@ export default class Path { touch(): Path { //FIXME work more as expected - return this.write({force: true, text: ""}) + return this.write({ force: true, text: "" }) } chmod(mode: number): Path { @@ -379,8 +386,8 @@ export default class Path { } relative({ to: base }: { to: Path }): string { - const pathComps = ['/'].concat(this.string.split("/").filter(x=>x)) - const baseComps = ['/'].concat(base.string.split("/").filter(x=>x)) + 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("/") @@ -393,7 +400,7 @@ export default class Path { newBaseComps.shift() } - const relComps = Array.from({ length: newBaseComps.length } , () => "..") + const relComps = Array.from({ length: newBaseComps.length }, () => "..") relComps.push(...newPathComps) return relComps.join("/") } @@ -404,13 +411,15 @@ export default class Path { } prettyString(): string { - return this.string.replace(new RegExp(`^${Path.home()}`), '~') + 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.string.startsWith(cwd.string) ? `./${this.relative({ to: cwd })}` : this.prettyString() + return this.string.startsWith(cwd.string) + ? `./${this.relative({ to: cwd })}` + : this.prettyString() } [Symbol.for("Deno.customInspect")]() { diff --git a/src/utils/error.test.ts b/src/utils/error.test.ts index cf71e9a..c497bc0 100644 --- a/src/utils/error.test.ts +++ b/src/utils/error.test.ts @@ -1,7 +1,7 @@ import { assertRejects, assertThrows } from "deno/testing/asserts.ts" import { panic } from "../utils/error.ts" -Deno.test("errors", async test => { +Deno.test("errors", async (test) => { await test.step("panic", () => { assertThrows(() => panic("test msg"), "test msg") }) @@ -12,11 +12,8 @@ Deno.test("errors", async test => { }) }) -class FooError extends Error -{} +class FooError extends Error {} -class BarError extends Error -{} +class BarError extends Error {} -class BazError extends BarError -{} +class BazError extends BarError {} diff --git a/src/utils/error.ts b/src/utils/error.ts index 15b5bc4..0e5a8a0 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -10,10 +10,10 @@ declare global { } } -Promise.prototype.swallow = function(errorClass?: new (...args: any) => any) { +Promise.prototype.swallow = function (errorClass?: new (...args: any) => any) { return this.catch((err: unknown) => { if (errorClass && !(err instanceof errorClass)) { - throw err; + throw err } }) } diff --git a/src/utils/flock.deno.ts b/src/utils/flock.deno.ts index 4248be5..8827e1c 100644 --- a/src/utils/flock.deno.ts +++ b/src/utils/flock.deno.ts @@ -1,5 +1,5 @@ -export async function flock(fd: number, op: 'ex' | 'un') { - if (op == 'ex') { +export async function flock(fd: number, op: "ex" | "un") { + if (op == "ex") { await Deno.flock(fd, true) } else { await Deno.funlock(fd) diff --git a/src/utils/flock.node.ts b/src/utils/flock.node.ts index a16ccb4..ff1b9d4 100644 --- a/src/utils/flock.node.ts +++ b/src/utils/flock.node.ts @@ -1,18 +1,18 @@ -import koffi from 'npm:koffi@2' +import koffi from "npm:koffi@2" import * as util from "node:util" import host from "./host.ts" -const filename = host().platform == 'darwin' ? '/usr/lib/libSystem.dylib' : 'libc.so.6' +const filename = host().platform == "darwin" ? "/usr/lib/libSystem.dylib" : "libc.so.6" const libc = koffi.load(filename) -const LOCK_EX = 2; -const LOCK_UN = 8; +const LOCK_EX = 2 +const LOCK_UN = 8 -const cflock = libc.func('int flock(int, int)'); -const flockAsync = util.promisify(cflock.async); +const cflock = libc.func("int flock(int, int)") +const flockAsync = util.promisify(cflock.async) -async function flock(fd: number, op: 'un' | 'ex') { - const rv = await flockAsync(fd, op == 'ex' ? LOCK_EX : LOCK_UN); +async function flock(fd: number, op: "un" | "ex") { + const rv = await flockAsync(fd, op == "ex" ? LOCK_EX : LOCK_UN) if (rv === -1) { throw new Error("flock failed") // TODO read errno } diff --git a/src/utils/hacks.test.ts b/src/utils/hacks.test.ts index e76176e..ff08e29 100644 --- a/src/utils/hacks.test.ts +++ b/src/utils/hacks.test.ts @@ -28,7 +28,8 @@ Deno.test({ fn: () => { const result = validatePackageRequirement("apple.com/xcode/clt", "*") assertEquals(result, undefined) -}}) + }, +}) Deno.test("validatePackageRequirement - linux hack", () => { if (host().platform !== "linux") return diff --git a/src/utils/hacks.ts b/src/utils/hacks.ts index fbf4ce1..89f1303 100644 --- a/src/utils/hacks.ts +++ b/src/utils/hacks.ts @@ -5,19 +5,24 @@ import * as semver from "./semver.ts" import host from "./host.ts" import { TeaError } from "../../mod.ts" -export function validatePackageRequirement(project: string, constraint: unknown): PackageRequirement | undefined -{ - if (host().platform == 'darwin' && (project == "apple.com/xcode/clt" || project == "tea.xyz/gx/make")) { +export function validatePackageRequirement( + project: string, + constraint: unknown, +): PackageRequirement | undefined { + if ( + host().platform == "darwin" && + (project == "apple.com/xcode/clt" || project == "tea.xyz/gx/make") + ) { // Apple will error out and prompt the user to install when the tool is used - return // compact this dep away + return // compact this dep away } - if (host().platform == 'linux' && project == "tea.xyz/gx/make") { + if (host().platform == "linux" && project == "tea.xyz/gx/make") { project = "gnu.org/make" - constraint = '*' + constraint = "*" } - if (constraint == 'c99' && project == 'tea.xyz/gx/cc') { - constraint = '^0.1' + if (constraint == "c99" && project == "tea.xyz/gx/cc") { + constraint = "^0.1" } if (isNumber(constraint)) { @@ -33,6 +38,6 @@ export function validatePackageRequirement(project: string, constraint: unknown) return { project, - constraint: constraint as semver.Range + constraint: constraint as semver.Range, } } diff --git a/src/utils/host.test.ts b/src/utils/host.test.ts index f590648..f6293e5 100644 --- a/src/utils/host.test.ts +++ b/src/utils/host.test.ts @@ -7,26 +7,26 @@ Deno.test("host()", async () => { const { platform, arch } = host() switch (uname[0]) { - case "Darwin": - assertEquals(platform, "darwin") - break - case "Linux": - assertEquals(platform, "linux") - break - default: - fail() + case "Darwin": + assertEquals(platform, "darwin") + break + case "Linux": + assertEquals(platform, "linux") + break + default: + fail() } switch (uname[1]) { - case "aarch64": - case "arm64": - assertEquals(arch, "aarch64") - break - case "x86_64": - assertEquals(arch, "x86-64") - break - default: - fail() + case "aarch64": + case "arm64": + assertEquals(arch, "aarch64") + break + case "x86_64": + assertEquals(arch, "x86-64") + break + default: + fail() } async function run(cmd: string) { diff --git a/src/utils/host.ts b/src/utils/host.ts index cbd6705..ad2852a 100644 --- a/src/utils/host.ts +++ b/src/utils/host.ts @@ -2,7 +2,7 @@ import process from "node:process" // 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"] as const +export const SupportedPlatforms = ["darwin", "linux", "windows"] as const export type SupportedPlatform = typeof SupportedPlatforms[number] export const SupportedArchitectures = ["x86-64", "aarch64"] as const @@ -19,25 +19,27 @@ export default function host(): HostReturnValue { const platform = (() => { const platform = _internals.platform() switch (platform) { - case "darwin": - case "linux": - case "windows": - return platform - default: - console.warn(`operating incognito as linux (${platform})`) - return 'linux' - }})() + case "darwin": + case "linux": + case "windows": + return platform + default: + console.warn(`operating incognito as linux (${platform})`) + return "linux" + } + })() const arch = (() => { const arch = _internals.arch() switch (arch) { - case "arm64": - return "aarch64" - case "x64": - return "x86-64" - default: - throw new Error(`unsupported-arch: ${arch}`) - }})() + case "arm64": + return "aarch64" + case "x64": + return "x86-64" + default: + throw new Error(`unsupported-arch: ${arch}`) + } + })() const { target } = Deno.build @@ -45,13 +47,13 @@ export default function host(): HostReturnValue { platform, arch, target, - build_ids: [platform, arch] + build_ids: [platform, arch], } } const _internals = { arch: () => process.arch, - platform: () => Deno.build.os + platform: () => Deno.build.os, } export { _internals } diff --git a/src/utils/misc.test.ts b/src/utils/misc.test.ts index 577d092..bde00f1 100644 --- a/src/utils/misc.test.ts +++ b/src/utils/misc.test.ts @@ -16,7 +16,7 @@ Deno.test("validate array", () => { }) Deno.test("validate obj", () => { - assertEquals(validate.obj({a: 1}), {a: 1}) + assertEquals(validate.obj({ a: 1 }), { a: 1 }) assertThrows(() => validate.obj("jkl"), "not-array: jkl") }) @@ -29,7 +29,7 @@ Deno.test("flatmap", () => { throw Error("test error") } - assertEquals(flatmap(1, throws, {rescue: true}), undefined) + assertEquals(flatmap(1, throws, { rescue: true }), undefined) assertThrows(() => flatmap(1, throws), "test error") }) @@ -47,7 +47,7 @@ Deno.test("async flatmap", async () => { assertEquals(await flatmap(producer(undefined), add), undefined) assertEquals(await flatmap(producer(1), (_n) => undefined), undefined) - assertEquals(await flatmap(producer(1, Error()), add, {rescue: true}), undefined) + assertEquals(await flatmap(producer(1, Error()), add, { rescue: true }), undefined) await assertRejects(() => flatmap(producer(1, Error()), add, undefined)) }) @@ -61,8 +61,8 @@ Deno.test("chuzzle", () => { Deno.test("set insert", () => { const s = new Set([1, 2, 3]) - assertEquals(s.insert(1), {inserted: false}) - assertEquals(s.insert(4), {inserted: true}) + assertEquals(s.insert(1), { inserted: false }) + assertEquals(s.insert(4), { inserted: true }) assertEquals(s.size, 4) assertEquals(s.has(1), true) @@ -83,6 +83,10 @@ Deno.test("array compact", () => { const throws = () => { throw Error("test error") } - assertEquals([()=>1, ()=>2, throws, ()=>3].compact((n) => n() * 2, { rescue: true }), [2, 4, 6]) - assertThrows(() => [()=>1, ()=>2, throws, ()=>3].compact((n) => n() * 2)) + assertEquals([() => 1, () => 2, throws, () => 3].compact((n) => n() * 2, { rescue: true }), [ + 2, + 4, + 6, + ]) + assertThrows(() => [() => 1, () => 2, throws, () => 3].compact((n) => n() * 2)) }) diff --git a/src/utils/misc.ts b/src/utils/misc.ts index b32a4a0..4c6e727 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -4,9 +4,9 @@ import { is_what, PlainObject } from "../deps.ts" const { isPlainObject, isArray } = is_what function validate_str(input: unknown): 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}`) + 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 } @@ -23,17 +23,17 @@ function validate_arr(input: unknown): Array { const validate = { str: validate_str, obj: validate_plain_obj, - arr: validate_arr + arr: validate_arr, } export { validate } ////////////////////////////////////////////////////////////// base extensions -type Falsy = false | 0 | '' | null | undefined; +type Falsy = false | 0 | "" | null | undefined declare global { interface Array { - compact(): Array>; + compact(): Array> compact(body: (t: T) => S | Falsy): Array compact(body?: (t: T) => S | T | Falsy, opts?: { rescue: boolean }): Array } @@ -43,16 +43,19 @@ declare global { } } -Set.prototype.insert = function(t: T) { +Set.prototype.insert = function (t: T) { if (this.has(t)) { - return {inserted: false} + return { inserted: false } } else { this.add(t) - return {inserted: true} + return { inserted: true } } } -Array.prototype.compact = function(body?: (t: T) => S | Falsy, opts?: { rescue: boolean }): S[] { +Array.prototype.compact = function ( + body?: (t: T) => S | Falsy, + opts?: { rescue: boolean }, +): S[] { const rv: S[] = [] for (const e of this) { try { @@ -65,18 +68,33 @@ Array.prototype.compact = function(body?: (t: T) => S | Falsy, opts?: { re return rv } -export function flatmap(t: T | Falsy, body: (t: T) => S | Falsy, opts?: {rescue?: boolean}): S | undefined; -export function flatmap(t: Promise, body: (t: T) => Promise, opts?: {rescue?: boolean}): Promise; -export function flatmap(t: Promise | (T | Falsy), body: (t: T) => (S | Falsy) | Promise, opts?: {rescue?: boolean}): Promise | (S | undefined) { +export function flatmap( + t: T | Falsy, + body: (t: T) => S | Falsy, + opts?: { rescue?: boolean }, +): S | undefined +export function flatmap( + t: Promise, + body: (t: T) => Promise, + opts?: { rescue?: boolean }, +): Promise +export function flatmap( + t: Promise | (T | Falsy), + body: (t: T) => (S | Falsy) | Promise, + opts?: { rescue?: boolean }, +): Promise | (S | undefined) { try { if (t instanceof Promise) { - const foo = t.then(t => { + const foo = t.then((t) => { if (!t) return const s = body(t) as Promise if (!s) return const bar = s - .then(body => body || undefined) - .catch(err => { if (!opts?.rescue) throw err; else return undefined } ) + .then((body) => body || undefined) + .catch((err) => { + if (!opts?.rescue) throw err + else return undefined + }) return bar }) return foo @@ -108,10 +126,10 @@ declare global { } } -String.prototype.chuzzle = function() { +String.prototype.chuzzle = function () { return this.trim() || undefined } -Number.prototype.chuzzle = function() { +Number.prototype.chuzzle = function () { return Number.isNaN(this) ? undefined : this as number } diff --git a/src/utils/pkg.test.ts b/src/utils/pkg.test.ts index 4f25468..0350f86 100644 --- a/src/utils/pkg.test.ts +++ b/src/utils/pkg.test.ts @@ -2,13 +2,13 @@ import { assert, assertEquals, assertFalse, assertThrows } from "deno/testing/as import SemVer, { Range } from "./semver.ts" import * as pkg from "./pkg.ts" -Deno.test("pkg.str", async test => { +Deno.test("pkg.str", async (test) => { let out: string await test.step("precise", () => { out = pkg.str({ project: "test", - version: new SemVer("1.2.3") + version: new SemVer("1.2.3"), }) assertEquals(out, "test=1.2.3") }) @@ -17,17 +17,19 @@ Deno.test("pkg.str", async test => { await test.step(range, () => { out = pkg.str({ project: "test", - constraint: new Range(range) + 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"]]) { + 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) + constraint: new Range(range), }) assertEquals(out, `test${expected}`) }) @@ -38,33 +40,33 @@ Deno.test("pkg.str", async test => { out = pkg.str({ project: "test", - constraint + constraint, }) assert(constraint.single()) assertEquals(out, `test=1.2.3`) }) }) -Deno.test("pkg.parse", async test => { +Deno.test("pkg.parse", async (test) => { 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]))) + 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]))) + 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]))) + 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", () => { @@ -73,11 +75,11 @@ Deno.test("pkg.parse", async test => { await test.step("leading & trailing space", () => { const { constraint } = pkg.parse(" test@5\t") - assert(constraint.satisfies(new SemVer([5,0,0]))) + assert(constraint.satisfies(new SemVer([5, 0, 0]))) }) }) -Deno.test("pkg.compare", async test => { +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") } diff --git a/src/utils/pkg.ts b/src/utils/pkg.ts index 3a488c5..04b0910 100644 --- a/src/utils/pkg.ts +++ b/src/utils/pkg.ts @@ -13,9 +13,7 @@ export function parse(input: string): PackageRequirement { } export function compare(a: Package, b: Package): number { - return a.project === b.project - ? a.version.compare(b.version) - : a.project.localeCompare(b.project) + return a.project === b.project ? a.version.compare(b.version) : a.project.localeCompare(b.project) } export function str(pkg: Package | PackageRequirement): string { diff --git a/src/utils/semver.test.ts b/src/utils/semver.test.ts index bfe453c..c7a27dc 100644 --- a/src/utils/semver.test.ts +++ b/src/utils/semver.test.ts @@ -2,10 +2,15 @@ import { assert, assertEquals, assertFalse, assertThrows } from "deno/assert/mod.ts" import SemVer, * as semver from "./semver.ts" - -Deno.test("semver", async test => { +Deno.test("semver", async (test) => { await test.step("sort", () => { - const input = [new SemVer([1,2,3]), new SemVer("10.3.4"), new SemVer("1.2.4"), semver.parse("1.2.3.1")!, new SemVer("2.3.4")] + const input = [ + new SemVer([1, 2, 3]), + new SemVer("10.3.4"), + new SemVer("1.2.4"), + semver.parse("1.2.3.1")!, + new SemVer("2.3.4"), + ] const sorted1 = [...input].sort(semver.compare) const sorted2 = [...input].sort() @@ -16,7 +21,13 @@ Deno.test("semver", async test => { }) await test.step("calver sort", () => { - const input = [new SemVer([1,2,3]), new SemVer("2.3.4"), new SemVer("2023.03.04"), semver.parse("1.2.3.1")!, new SemVer([3,4,5])] + const input = [ + new SemVer([1, 2, 3]), + new SemVer("2.3.4"), + new SemVer("2023.03.04"), + semver.parse("1.2.3.1")!, + new SemVer([3, 4, 5]), + ] const sorted1 = [...input].sort(semver.compare) const sorted2 = [...input].sort() @@ -63,11 +74,11 @@ Deno.test("semver", async test => { assertEquals(new SemVer("v1").toString(), "1.0.0") assertEquals(new SemVer("9e").toString(), "9e") - assertEquals(new SemVer("9e").components, [9,5]) + 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("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]) + assertEquals(new SemVer("1.1.1q").components, [1, 1, 1, 17]) }) await test.step("ranges", () => { @@ -164,7 +175,7 @@ Deno.test("semver", async test => { assertEquals(new semver.Range(">=300.1.0<300.1.1").toString(), "@300.1.0") }) - await test.step("intersection", async test => { + 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") @@ -248,10 +259,10 @@ Deno.test("semver", async test => { }) Deno.test("coverage", () => { - assert(new SemVer("1.2.3").eq(new SemVer([1,2,3]))) - assert(new SemVer("1.2.3").neq(new SemVer([1,2,4]))) - assert(new SemVer("1.2.3").lt(new SemVer([1,2,4]))) - assert(new SemVer("1.2.4").gt(new SemVer([1,2,3]))) + assert(new SemVer("1.2.3").eq(new SemVer([1, 2, 3]))) + assert(new SemVer("1.2.3").neq(new SemVer([1, 2, 4]))) + assert(new SemVer("1.2.3").lt(new SemVer([1, 2, 4]))) + assert(new SemVer("1.2.4").gt(new SemVer([1, 2, 3]))) assertThrows(() => new SemVer("1.q.3")) @@ -283,13 +294,15 @@ Deno.test("coverage", () => { assert(new semver.Range("*").satisfies(new SemVer("1.2.3"))) - assertEquals(new semver.Range("^1").max([new SemVer("1.2.3"), new SemVer("1.2.4")]), new SemVer("1.2.4")) + assertEquals( + new semver.Range("^1").max([new SemVer("1.2.3"), new SemVer("1.2.4")]), + new SemVer("1.2.4"), + ) assertEquals(new semver.Range("*").single(), undefined) assert(semver.intersect(new semver.Range("*"), new semver.Range("^2"))) assert(semver.intersect(new semver.Range("^2"), new semver.Range("*"))) - assertEquals(new semver.Range("^1.2.0").toString(), "^1.2") }) diff --git a/src/utils/semver.ts b/src/utils/semver.ts index ed6109a..8b07598 100644 --- a/src/utils/semver.ts +++ b/src/utils/semver.ts @@ -4,8 +4,8 @@ import { isArray, isString } from "https://deno.land/x/is_what@v4.1.15/src/index /** * 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 + * 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 { @@ -23,10 +23,10 @@ export default class SemVer { readonly pretty?: string constructor(input: string | number[] | Range | SemVer) { - if (typeof input == 'string') { - const vprefix = input.startsWith('v') + if (typeof input == "string") { + const vprefix = input.startsWith("v") const raw = vprefix ? input.slice(1) : input - const parts = raw.split('.') + const parts = raw.split(".") let pretty_is_raw = false this.components = parts.flatMap((x, index) => { const match = x.match(/^(\d+)([a-z])$/) @@ -37,7 +37,7 @@ export default class SemVer { 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` + const n = parseInt(x) // parseInt will parse eg. `5-start` to `5` if (isNaN(n)) throw new Error(`invalid version: ${input}`) return [n] } else { @@ -54,7 +54,7 @@ export default class SemVer { this.pretty = v.pretty } else { this.components = [...input] - this.raw = input.join('.') + this.raw = input.join(".") } this.major = this.components[0] @@ -62,7 +62,7 @@ export default class SemVer { this.patch = this.components[2] ?? 0 function char_to_num(c: string) { - return c.charCodeAt(0) - 'a'.charCodeAt(0) + 1 + return c.charCodeAt(0) - "a".charCodeAt(0) + 1 } } @@ -70,7 +70,7 @@ export default class SemVer { return this.pretty ?? (this.components.length <= 3 ? `${this.major}.${this.minor}.${this.patch}` - : this.components.join('.')) + : this.components.join(".")) } eq(that: SemVer): boolean { @@ -124,11 +124,11 @@ export function isValid(input: string) { /// 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)[] | '*' + readonly set: ([SemVer, SemVer] | SemVer)[] | "*" constructor(input: string | ([SemVer, SemVer] | SemVer)[]) { if (input === "*") { - this.set = '*' + this.set = "*" } else if (!isString(input)) { this.set = input } else { @@ -136,7 +136,7 @@ export class Range { const err = () => new Error(`invalid semver range: ${input}`) - this.set = input.split(/(?:,|\s*\|\|\s*)/).map(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]) @@ -145,46 +145,49 @@ export class Range { } 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 + // 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]) + case "@": { + // @ 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 (ie. ~5.1) + // @5.1.0 => latest 5.1.0 (usually 5.1.0 since most stuff hasn't got more digits) + const parts = match[2].split(".").map((x) => parseInt(x)) + v1 = new SemVer(parts) + const last = parts.pop()! + v2 = new SemVer([...parts, last + 1]) + return [v1, v2] } - 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]) - case "@": { - // @ 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 (ie. ~5.1) - // @5.1.0 => latest 5.1.0 (usually 5.1.0 since most stuff hasn't got more digits) - const parts = match[2].split(".").map(x => parseInt(x)) - v1 = new SemVer(parts) - const last = parts.pop()! - v2 = new SemVer([...parts, last + 1]) - return [v1, v2] - }} + } } throw err() }) @@ -199,10 +202,10 @@ export class Range { } toString(): string { - if (this.set === '*') { - return '*' + if (this.set === "*") { + return "*" } else { - return this.set.map(v => { + 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) { @@ -222,7 +225,7 @@ export class Range { }).join(",") } - function at(v1: SemVer, {components: cc2}: SemVer) { + function at(v1: SemVer, { components: cc2 }: SemVer) { const cc1 = [...v1.components] if (cc1.length > cc2.length) { @@ -282,10 +285,10 @@ export class Range { } satisfies(version: SemVer): boolean { - if (this.set === '*') { + if (this.set === "*") { return true } else { - return this.set.some(v => { + return this.set.some((v) => { if (isArray(v)) { const [v1, v2] = v return version.compare(v1) >= 0 && version.compare(v2) < 0 @@ -297,11 +300,11 @@ export class Range { } max(versions: SemVer[]): SemVer | undefined { - return versions.filter(x => this.satisfies(x)).sort((a,b) => a.compare(b)).pop() + return versions.filter((x) => this.satisfies(x)).sort((a, b) => a.compare(b)).pop() } single(): SemVer | undefined { - if (this.set === '*') return + if (this.set === "*") return if (this.set.length > 1) return return isArray(this.set[0]) ? undefined : this.set[0] } @@ -320,9 +323,8 @@ function zip(a: T[], b: U[]) { return rv } - function _compare(a: SemVer, b: SemVer): number { - for (const [c,d] of zip(cmpcomponents(a), cmpcomponents(b))) { + for (const [c, d] of zip(cmpcomponents(a), cmpcomponents(b))) { if (c != d) return (c ?? 0) - (d ?? 0) } return 0 @@ -331,7 +333,7 @@ function _compare(a: SemVer, b: SemVer): number { /// we worry that one day we will severely regret this but… it’s what we do for now function cmpcomponents(v: SemVer) { if (v.major > 1996 && v.major != Infinity) { - return [0,0,0, ...v.components] + return [0, 0, 0, ...v.components] } else { return v.components } @@ -339,10 +341,9 @@ function _compare(a: SemVer, b: SemVer): number { } export { _compare as compare } - export function intersect(a: Range, b: Range): Range { - if (b.set === '*') return a - if (a.set === '*') return b + if (b.set === "*") return a + if (a.set === "*") return b // calculate the intersection between two semver.Ranges const set: ([SemVer, SemVer] | SemVer)[] = [] @@ -377,10 +378,9 @@ export function intersect(a: Range, b: Range): Range { 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' + return v.toString().replace(/(\.0)+$/g, "") || "0" }