Skip to content

Commit 9c7a748

Browse files
authored
Semver at ranges (#40)
* drop @latest from the lib, this is cli territory * @range syntax is the concise representation 4 some
1 parent 7528c0a commit 9c7a748

File tree

8 files changed

+94
-27
lines changed

8 files changed

+94
-27
lines changed

src/plumbing/resolve.test.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { assert, assertEquals, fail, assertRejects } from "deno/testing/asserts.ts"
1+
// deno-lint-ignore-file require-await
2+
import { assert, assertEquals, fail, assertRejects } from "deno/assert/mod.ts"
23
import { Installation, Package, PackageRequirement } from "../types.ts"
34
import { useTestConfig } from "../hooks/useTestConfig.ts"
45
import useInventory from "../hooks/useInventory.ts"
@@ -9,14 +10,21 @@ import { stub } from "deno/testing/mock.ts"
910
import SemVer from "../utils/semver.ts"
1011
import Path from "../utils/Path.ts"
1112

12-
Deno.test("resolve cellar.has", async runner => {
13+
Deno.test("resolve cellar.has", {
14+
permissions: {'read': true, 'env': ["TMPDIR", "TMP", "TEMP", "HOME"], 'write': [Deno.env.get("TMPDIR") || Deno.env.get("TMP") || Deno.env.get("TEMP") || "/tmp"] }
15+
}, async runner => {
1316
const prefix = useTestConfig().prefix
1417
const pkg = { project: "foo", version: new SemVer("1.0.0") }
1518

1619
const cellar = useCellar()
17-
const has = (_: Path | Package | PackageRequirement) => {
18-
const a: Installation = {pkg, path: prefix.join(pkg.project, `v${pkg.version}`) }
19-
return Promise.resolve(a)
20+
const has = async (pkg_: Package | PackageRequirement | Path) => {
21+
if (pkg_ instanceof Path) fail()
22+
if (pkg.project == pkg_.project) {
23+
if ('constraint' in pkg_ && !pkg_.constraint.satisfies(pkg.version)) return
24+
if ('version' in pkg_ && !pkg_.version.eq(pkg.version)) return
25+
const a: Installation = {pkg, path: prefix.join(pkg.project, `v${pkg.version}`) }
26+
return a
27+
}
2028
}
2129

2230
await runner.step("happy path", async () => {
@@ -29,7 +37,7 @@ Deno.test("resolve cellar.has", async runner => {
2937
}))
3038

3139
try {
32-
const rv = await resolve([pkg])
40+
const rv = await resolve([pkg])
3341
assertEquals(rv.pkgs[0].project, pkg.project)
3442
assertEquals(rv.installed[0].pkg.project, pkg.project)
3543
} finally {
@@ -60,7 +68,7 @@ Deno.test("resolve cellar.has", async runner => {
6068
assert(errord)
6169
})
6270

63-
await runner.step("uses existing version if even if update set", async () => {
71+
await runner.step("uses existing version if it is the latest even if update set", async () => {
6472
const stub1 = stub(_internals, "useInventory", () => ({
6573
get: () => fail(),
6674
select: () => Promise.resolve(pkg.version),
@@ -78,6 +86,36 @@ Deno.test("resolve cellar.has", async runner => {
7886
stub2.restore()
7987
}
8088
})
89+
90+
await runner.step("updates version if latest is not installed", async runner => {
91+
const stub1 = stub(_internals, "useInventory", () => ({
92+
get: () => fail(),
93+
select: () => Promise.resolve(new SemVer("1.0.1")),
94+
}))
95+
const stub2 = stub(_internals, "useCellar", () => ({
96+
...cellar, has
97+
}))
98+
99+
try {
100+
await runner.step("update: true", async () => {
101+
const rv = await resolve([{ project: pkg.project, constraint: new semver.Range("^1") }], { update: true })
102+
assertEquals(rv.pkgs[0].project, pkg.project)
103+
assertEquals(rv.pending[0].project, pkg.project)
104+
assertEquals(rv.pending[0].version, new SemVer("1.0.1"))
105+
})
106+
107+
await runner.step("update: set", async () => {
108+
const update = new Set([pkg.project])
109+
const rv = await resolve([{ project: pkg.project, constraint: new semver.Range("^1") }], { update })
110+
assertEquals(rv.pkgs[0].project, pkg.project)
111+
assertEquals(rv.pending[0].project, pkg.project)
112+
assertEquals(rv.pending[0].version, new SemVer("1.0.1"))
113+
})
114+
} finally {
115+
stub1.restore()
116+
stub2.restore()
117+
}
118+
})
81119
})
82120

83121
const permissions = { net: false, read: true, env: ["TMPDIR", "HOME", "TMP", "TEMP"], write: true /*FIXME*/ }

src/plumbing/resolve.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class ResolveError extends TeaError {
3232
/// that resolve so if we are resolving `node>=12`, node 13 is installed, but
3333
/// node 19 is the latest we return node 13. if `update` is true we return node
3434
/// 19 and *you will need to install it*.
35-
export default async function resolve(reqs: (Package | PackageRequirement)[], {update}: {update: boolean} = {update: false}): Promise<Resolution> {
35+
export default async function resolve(reqs: (Package | PackageRequirement)[], {update}: {update: boolean | Set<string>} = {update: false}): Promise<Resolution> {
3636
const inventory = _internals.useInventory()
3737
const cellar = _internals.useCellar()
3838
const rv: Resolution = { pkgs: [], installed: [], pending: [] }
@@ -41,7 +41,8 @@ export default async function resolve(reqs: (Package | PackageRequirement)[], {u
4141
const promises: Promise<void>[] = []
4242

4343
for (const req of reqs) {
44-
if (!update && (installation = await cellar.has(req))) {
44+
const noup = !should_update(req.project)
45+
if (noup && (installation = await cellar.has(req))) {
4546
// if something is already installed that satisfies the constraint then use it
4647
rv.installed.push(installation)
4748
rv.pkgs.push(installation.pkg)
@@ -67,6 +68,10 @@ export default async function resolve(reqs: (Package | PackageRequirement)[], {u
6768
await Promise.all(promises)
6869

6970
return rv
71+
72+
function should_update(project: string) {
73+
return update === true || (update instanceof Set && update.has(project))
74+
}
7075
}
7176

7277
export const _internals = {

src/utils/misc.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Deno.test("array compact", () => {
7676
// will fail to compile if the compiler cannot infer the type of the compact() return
7777
assertEquals([1, 2, undefined, null, false, 3].compact()[0] + 1, 2)
7878

79+
// verifies transforming the type gives singular type return
80+
const foo = [1, 2, undefined, null, false, 3].compact((n) => isNumber(n) && `${n * 2}`)
81+
assertEquals(foo, ["2", "4", "6"])
82+
7983
const throws = () => {
8084
throw Error("test error")
8185
}

src/utils/misc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export { validate }
3232
declare global {
3333
interface Array<T> {
3434
compact(): Array<Exclude<T, boolean | null | undefined>>
35+
compact<S>(body: (t: T) => S | null | undefined | false): Array<S>
3536
compact<S>(body?: (t: T) => S | T | null | undefined | false, opts?: { rescue: boolean }): Array<S | T>
3637
}
3738

@@ -49,7 +50,7 @@ Set.prototype.insert = function<T>(t: T) {
4950
}
5051
}
5152

52-
Array.prototype.compact = function<T, S = T>(body?: (t: T) => S | null | undefined | false, opts?: { rescue: boolean }): S[] {
53+
Array.prototype.compact = function<T, S>(body?: (t: T) => S | null | undefined | false, opts?: { rescue: boolean }): S[] {
5354
const rv: S[] = []
5455
for (const e of this) {
5556
try {

src/utils/pkg.test.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,6 @@ Deno.test("pkg.str", async test => {
4646
})
4747

4848
Deno.test("pkg.parse", async test => {
49-
await test.step("@latest", () => {
50-
const { constraint } = pkg.parse("test@latest")
51-
assert(constraint.satisfies(new SemVer([5,0,0])))
52-
assert(constraint.satisfies(new SemVer([5,1,0])))
53-
assert(constraint.satisfies(new SemVer([6,0,0])))
54-
})
55-
5649
await test.step("@5", () => {
5750
const { constraint } = pkg.parse("test@5")
5851
assert(constraint.satisfies(new SemVer([5,0,0])))

src/utils/pkg.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,8 @@ export function parse(input: string): PackageRequirement {
88
if (!match[2]) match[2] = "*"
99

1010
const project = match[1]
11-
12-
if (match[2] == "@latest") {
13-
return { project, constraint: new semver.Range('*') }
14-
} else {
15-
const constraint = new semver.Range(match[2])
16-
return { project, constraint }
17-
}
11+
const constraint = new semver.Range(match[2])
12+
return { project, constraint }
1813
}
1914

2015
export function compare(a: Package, b: Package): number {

src/utils/semver.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// deno-lint-ignore-file no-explicit-any
2-
import { assert, assertEquals, assertFalse, assertThrows } from "deno/testing/asserts.ts"
2+
import { assert, assertEquals, assertFalse, assertThrows } from "deno/assert/mod.ts"
33
import SemVer, * as semver from "./semver.ts"
44

55

@@ -160,7 +160,8 @@ Deno.test("semver", async test => {
160160

161161
assertEquals(new semver.Range("@300").toString(), "^300")
162162
assertEquals(new semver.Range("@300.1").toString(), "~300.1")
163-
assertEquals(new semver.Range("@300.1.0").toString(), ">=300.1<300.1.1")
163+
assertEquals(new semver.Range("@300.1.0").toString(), "@300.1.0")
164+
assertEquals(new semver.Range(">=300.1.0<300.1.1").toString(), "@300.1.0")
164165
})
165166

166167
await test.step("intersection", async test => {
@@ -274,7 +275,7 @@ Deno.test("coverage", () => {
274275

275276
assertEquals(semver.Range.parse("1")?.toString(), new semver.Range("^1").toString())
276277
assertEquals(semver.Range.parse("1.1")?.toString(), new semver.Range("~1.1").toString())
277-
assertEquals(semver.Range.parse("1.1.2")?.toString(), new semver.Range(">=1.1.2<1.1.3").toString())
278+
assertEquals(semver.Range.parse("1.1.2")?.toString(), new semver.Range("@1.1.2").toString())
278279

279280
assertEquals(semver.Range.parse("a"), undefined)
280281

src/utils/semver.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,41 @@ export class Range {
214214
} else if (v2.major == Infinity) {
215215
const v = chomp(v1)
216216
return `>=${v}`
217+
} else if (at(v1, v2)) {
218+
return `@${v1}`
217219
} else {
218220
return `>=${chomp(v1)}<${chomp(v2)}`
219221
}
220222
}).join(",")
221223
}
224+
225+
function at(v1: SemVer, {components: cc2}: SemVer) {
226+
const cc1 = [...v1.components]
227+
228+
if (cc1.length > cc2.length) {
229+
return false
230+
}
231+
232+
// it's possible the components were short due to 0-truncation
233+
// add them back so our algo works
234+
while (cc1.length < cc2.length) {
235+
cc1.push(0)
236+
}
237+
238+
if (last(cc1) != last(cc2) - 1) {
239+
return false
240+
}
241+
242+
for (let i = 0; i < (cc1.length - 1); i++) {
243+
if (cc1[i] != cc2[i]) return false
244+
}
245+
246+
return true
247+
}
248+
249+
function last<T>(arr: number[]) {
250+
return arr[arr.length - 1]
251+
}
222252
}
223253

224254
// eq(that: Range): boolean {

0 commit comments

Comments
 (0)