@preventive/upstream 1.0.0-alpha.4 → 1.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/advisories.d.ts +1 -1
- package/package.json +10 -1
- package/semver.d.ts +2 -1
- package/src/advisories/index.js +2 -2
- package/src/advisories/repos.js +4 -12
- package/src/args.js +19 -0
- package/src/cache.js +19 -1
- package/src/cargo/crate.js +1 -6
- package/src/cargo/repos.js +5 -15
- package/src/github/client.js +1 -2
- package/src/github/read.js +5 -6
- package/src/npm/repos.js +2 -5
- package/src/npm/tarball.js +1 -6
- package/src/semver-peer.cjs +8 -0
- package/src/semver.js +26 -12
- package/src/soldeer.js +2 -3
- package/src/tree.js +7 -6
package/advisories.d.ts
CHANGED
|
@@ -89,5 +89,5 @@ export interface Advisory {
|
|
|
89
89
|
// renamed or blocked, add nothing. Any other failure, a malformed answer,
|
|
90
90
|
// or one about something not asked, throws: nothing is left out quietly.
|
|
91
91
|
// Sorted by ecosystem and name. Versions are matched by npm's semver, from
|
|
92
|
-
// the npm beside node.
|
|
92
|
+
// the npm beside node, or by the semver peer where there is no npm.
|
|
93
93
|
export function advisories(packages: Iterable<Package>, options?: AdvisoryOptions): Promise<Advisory[]>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/upstream",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.5",
|
|
4
4
|
"description": "Minimal npm, crates.io and Soldeer registry clients, a GitHub client, and a git checkout reader: the calls we use, nothing more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"src/package.js",
|
|
84
84
|
"src/pool.js",
|
|
85
85
|
"src/remote.js",
|
|
86
|
+
"src/semver-peer.cjs",
|
|
86
87
|
"src/semver.js",
|
|
87
88
|
"src/tree.js",
|
|
88
89
|
"src/soldeer.js"
|
|
@@ -110,5 +111,13 @@
|
|
|
110
111
|
"homepage": "https://github.com/PreventiveMeasures/libraries/tree/main/upstream",
|
|
111
112
|
"publishConfig": {
|
|
112
113
|
"access": "public"
|
|
114
|
+
},
|
|
115
|
+
"peerDependencies": {
|
|
116
|
+
"semver": "^7.5.2"
|
|
117
|
+
},
|
|
118
|
+
"peerDependenciesMeta": {
|
|
119
|
+
"semver": {
|
|
120
|
+
"optional": true
|
|
121
|
+
}
|
|
113
122
|
}
|
|
114
123
|
}
|
package/semver.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Hand-written against semver.js; a change to either belongs with the other.
|
|
2
2
|
|
|
3
3
|
// npm's own semver, borrowed from the npm beside node and passed through
|
|
4
|
-
// as it is
|
|
4
|
+
// as it is; where there is no npm, the `semver` package, an optional peer
|
|
5
|
+
// dependency. Without either, every call throws, except valid() on a plain
|
|
5
6
|
// MAJOR.MINOR.PATCH with no options.
|
|
6
7
|
export function satisfies(version: string, range: string, options?: { includePrerelease?: boolean; loose?: boolean }): boolean
|
|
7
8
|
export function compareVersions(a: string, b: string, options?: { loose?: boolean }): -1 | 0 | 1
|
package/src/advisories/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
|
|
3
|
-
import { assertArgs, assertBoolean, assertRepo, assertion, optional, sameName } from '../args.js'
|
|
3
|
+
import { assertArgs, assertBoolean, assertRepo, assertion, isIterable, optional, sameName } from '../args.js'
|
|
4
4
|
import { order } from './common.js'
|
|
5
5
|
import { GITHUB, SOLDEER, assertClient } from './github.js'
|
|
6
6
|
import { NPM } from './npm.js'
|
|
@@ -14,7 +14,7 @@ const assertVersions = assertion('a non-empty array', (value) => Array.isArray(v
|
|
|
14
14
|
// Ecosystem → name → { versions, github? }, a name given twice merged.
|
|
15
15
|
// Every package is checked before any is used.
|
|
16
16
|
function collect(packages) {
|
|
17
|
-
assert.ok(
|
|
17
|
+
assert.ok(isIterable(packages), 'advisories: packages must be an iterable of { ecosystem, name, github?, versions }')
|
|
18
18
|
const byEcosystem = new Map()
|
|
19
19
|
for (const pkg of packages) {
|
|
20
20
|
assertArgs('advisories', pkg, { ecosystem: assertEcosystem, name: null, github: optional(assertRepo), versions: assertVersions }, 'package')
|
package/src/advisories/repos.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { show } from '../args.js'
|
|
4
|
+
import { addRepos, readRepos } from '../cache.js'
|
|
5
5
|
import { lookUpCrateRepos } from '../cargo/repos.js'
|
|
6
6
|
import { PACKAGIST_REPO, SOLDEER_API, buildUrl, isNotFound, recover, request } from '../http.js'
|
|
7
7
|
import { lookUpPackageRepo } from '../npm/repos.js'
|
|
@@ -14,16 +14,8 @@ const PACKAGES_AT_ONCE = 8
|
|
|
14
14
|
// or it names no GitHub repo; any other failure throws. Only a repo found
|
|
15
15
|
// is cached, for a month.
|
|
16
16
|
async function cachedRepos(dir, names, fetchMissing) {
|
|
17
|
-
const repos =
|
|
18
|
-
|
|
19
|
-
const entry = await readRecord(dir, name)
|
|
20
|
-
if (isRepo(entry?.github)) repos.set(name, entry.github)
|
|
21
|
-
}
|
|
22
|
-
const missing = names.filter((name) => !repos.has(name))
|
|
23
|
-
for (const [name, github] of await fetchMissing(missing)) {
|
|
24
|
-
repos.set(name, github)
|
|
25
|
-
if (github) await writeRecord(dir, name, { github })
|
|
26
|
-
}
|
|
17
|
+
const repos = await readRepos(dir, names)
|
|
18
|
+
await addRepos(dir, repos, await fetchMissing(names.filter((name) => !repos.has(name))))
|
|
27
19
|
return repos
|
|
28
20
|
}
|
|
29
21
|
|
package/src/args.js
CHANGED
|
@@ -39,6 +39,7 @@ export function isRepo(value) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
42
|
+
export const isIterable = (value) => typeof value?.[Symbol.iterator] === 'function' && typeof value !== 'string'
|
|
42
43
|
export const sameName = (a, b) => typeof a === 'string' && a.toLowerCase() === b.toLowerCase()
|
|
43
44
|
export const isTagName = (value) => isString(value, 255) && !BAD_REF.test(value)
|
|
44
45
|
export const isRefName = (value) => isTagName(value) && !/^(?:@$|-)/u.test(value)
|
|
@@ -55,6 +56,16 @@ export function assertArgs(method, options, spec, name) {
|
|
|
55
56
|
for (const [key, check] of Object.entries(spec)) check?.(method, label(key), options[key])
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// The names a resolve*Repos call is given, each checked and given once,
|
|
60
|
+
// and its options.
|
|
61
|
+
export function assertNames(method, what, names, options, assertName) {
|
|
62
|
+
assert.ok(isIterable(names), `${method}: ${what} must be an iterable of names`)
|
|
63
|
+
assertArgs(method, options, { cachedOnly: optional(assertBoolean) })
|
|
64
|
+
const unique = [...new Set(names)]
|
|
65
|
+
for (const name of unique) assertName(method, 'name', name)
|
|
66
|
+
return unique
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
export const assertBoolean = assertion('a boolean', (value) => typeof value === 'boolean')
|
|
59
70
|
export const assertNumber = assertion('a positive integer', (value) => Number.isSafeInteger(value) && value > 0)
|
|
60
71
|
export const assertLine = assertion('a non-empty single line', (value) => isString(value, 1024) && value.trim() !== '')
|
|
@@ -84,5 +95,13 @@ export const assertSoldeerName = assertion('a Soldeer package name', matches(/^(
|
|
|
84
95
|
// Soldeer takes any version but an empty one. Its registry has semver
|
|
85
96
|
// (`5.7.0-rc.0`, `1.0.2-solc-0.8-simulate`), bare numbers and commit hashes.
|
|
86
97
|
export const assertSoldeerVersion = assertion('letters, digits, `.`, `_`, `+` and `-`', matches(/^(?=.{1,128}$)[\dA-Za-z][\w.+-]*$/u))
|
|
98
|
+
// A package's name and version, as a registry's getters take them.
|
|
99
|
+
const assertNameVersion = (assertName, assertVersion) => (method, name, version) => {
|
|
100
|
+
assertName(method, 'name', name)
|
|
101
|
+
assertVersion(method, 'version', version)
|
|
102
|
+
}
|
|
103
|
+
export const assertCrate = assertNameVersion(assertCrateName, assertCrateVersion)
|
|
104
|
+
export const assertPackage = assertNameVersion(assertPackageName, assertPackageVersion)
|
|
105
|
+
export const assertSoldeerPackage = assertNameVersion(assertSoldeerName, assertSoldeerVersion)
|
|
87
106
|
// As Cargo.lock and soldeer.lock write a checksum.
|
|
88
107
|
export const assertSha256 = assertion('a sha256 in lowercase hex', matches(/^[\da-f]{64}$/u))
|
package/src/cache.js
CHANGED
|
@@ -3,7 +3,7 @@ import { constants } from 'node:fs'
|
|
|
3
3
|
import { mkdir, open, rename, rm, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { dirname, join, resolve } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { assertDirectoryPath } from './args.js'
|
|
6
|
+
import { assertDirectoryPath, isRepo } from './args.js'
|
|
7
7
|
import { MAX_BYTES, decode } from './http.js'
|
|
8
8
|
|
|
9
9
|
const DIRS = new Set(['npm/repos', 'npm/tarballs', 'cargo/repos', 'cargo/crates', 'composer/repos', 'soldeer/repos', 'soldeer/zips', 'github/trees'])
|
|
@@ -93,3 +93,21 @@ export async function readRecord(dir, name) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export const writeRecord = (dir, name, value) => writeCacheJSON(dir, `${name}.json`, { at: Date.now(), name, ...value })
|
|
96
|
+
|
|
97
|
+
// Name → the GitHub repo cached for it, for those of `names` that have one.
|
|
98
|
+
export async function readRepos(dir, names) {
|
|
99
|
+
const repos = new Map()
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
const entry = await readRecord(dir, name)
|
|
102
|
+
if (isRepo(entry?.github)) repos.set(name, entry.github)
|
|
103
|
+
}
|
|
104
|
+
return repos
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// An answer into `repos`, and into the cache where it found a repo.
|
|
108
|
+
export async function addRepos(dir, repos, answer) {
|
|
109
|
+
for (const [name, github] of answer) {
|
|
110
|
+
repos.set(name, github)
|
|
111
|
+
if (github) await writeRecord(dir, name, { github })
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/cargo/crate.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readdir } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { assertCrate, assertSha256, printable, show } from '../args.js'
|
|
7
7
|
import { verifiedDownload } from '../download.js'
|
|
8
8
|
import { CRATES_INDEX, CRATES_STATIC, buildUrl, encodeSegment, request } from '../http.js'
|
|
9
9
|
|
|
@@ -37,11 +37,6 @@ async function localPaths(name, version) {
|
|
|
37
37
|
return (await readdir(root).catch(() => [])).map((registry) => join(root, registry, `${name}-${version}.crate`))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function assertCrate(method, name, version) {
|
|
41
|
-
assertCrateName(method, 'name', name)
|
|
42
|
-
assertCrateVersion(method, 'version', version)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
export async function verifyChecksum(name, version, checksum) {
|
|
46
41
|
assertCrate('verifyChecksum', name, version)
|
|
47
42
|
assertSha256('verifyChecksum', 'checksum', checksum)
|
package/src/cargo/repos.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { assertCrateName, assertNames, show } from '../args.js'
|
|
5
|
+
import { addRepos, readRepos } from '../cache.js'
|
|
6
6
|
import { CRATES_API, buildUrl, request } from '../http.js'
|
|
7
7
|
import { chunks } from '../pool.js'
|
|
8
8
|
import { githubRepoOfUrl } from '../remote.js'
|
|
@@ -32,11 +32,7 @@ async function askCrates(method, names) {
|
|
|
32
32
|
// for a month. A failed request throws, or with `soft` leaves its names
|
|
33
33
|
// out; `cachedOnly` asks nothing.
|
|
34
34
|
export async function lookUpCrateRepos(method, names, { soft = false, cachedOnly = false } = {}) {
|
|
35
|
-
const repos =
|
|
36
|
-
for (const name of names) {
|
|
37
|
-
const entry = await readRecord(DIR, name)
|
|
38
|
-
if (isRepo(entry?.github)) repos.set(name, entry.github)
|
|
39
|
-
}
|
|
35
|
+
const repos = await readRepos(DIR, names)
|
|
40
36
|
if (cachedOnly) return repos
|
|
41
37
|
for (const [i, chunk] of chunks(names.filter((name) => !repos.has(name)), PER_REQUEST).entries()) {
|
|
42
38
|
if (i > 0) await sleep(PACE_MS)
|
|
@@ -44,19 +40,13 @@ export async function lookUpCrateRepos(method, names, { soft = false, cachedOnly
|
|
|
44
40
|
if (soft) return new Map()
|
|
45
41
|
throw error
|
|
46
42
|
})
|
|
47
|
-
|
|
48
|
-
repos.set(name, github)
|
|
49
|
-
if (github) await writeRecord(DIR, name, { github })
|
|
50
|
-
}
|
|
43
|
+
await addRepos(DIR, repos, answer)
|
|
51
44
|
}
|
|
52
45
|
return repos
|
|
53
46
|
}
|
|
54
47
|
|
|
55
48
|
export async function resolveCrateRepos(crateNames, options = {}) {
|
|
56
|
-
|
|
57
|
-
assertArgs('resolveCrateRepos', options, { cachedOnly: optional(assertBoolean) })
|
|
58
|
-
const names = [...new Set(crateNames)]
|
|
59
|
-
for (const name of names) assertCrateName('resolveCrateRepos', 'name', name)
|
|
49
|
+
const names = assertNames('resolveCrateRepos', 'crateNames', crateNames, options, assertCrateName)
|
|
60
50
|
const repos = await lookUpCrateRepos('resolveCrateRepos', names, { soft: true, cachedOnly: options.cachedOnly })
|
|
61
51
|
return new Map(names.flatMap((name) => (repos.get(name) ? [[name, { github: repos.get(name) }]] : [])))
|
|
62
52
|
}
|
package/src/github/client.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
|
|
3
3
|
import { assertArgs, assertToken, assertTokenOrNull, assertUserAgent, optional } from '../args.js'
|
|
4
|
-
import { GITHUB_API, HttpError, buildUrl, request
|
|
4
|
+
import { GITHUB_API, HttpError, buildUrl, request } from '../http.js'
|
|
5
5
|
|
|
6
6
|
export const api = (segments, query) => buildUrl(GITHUB_API, segments, query)
|
|
7
7
|
export const repoApi = (repo, segments = [], query) => api(['repos', ...repo.split('/'), ...segments], query)
|
|
8
8
|
export const call = (headers, url, options) => request(url, { as: 'json', headers, ...options })
|
|
9
|
-
export const callWithHeaders = (headers, url, options) => requestWithHeaders(url, { as: 'json', headers, ...options })
|
|
10
9
|
// A repository renamed (a redirect, refused), deleted or blocked, not a
|
|
11
10
|
// failure that could pass.
|
|
12
11
|
export const isGone = (err) => err instanceof HttpError && [301, 404, 410, 451].includes(err.status)
|
package/src/github/read.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { Buffer } from 'node:buffer'
|
|
3
|
-
import { createHash } from 'node:crypto'
|
|
4
3
|
|
|
5
4
|
import { assertArgs, assertGhsa, assertLogin, assertNumber, assertPath, assertRef, assertRepo, assertSha, assertTagName, assertTreeId, isSha, isSha1, isTagName, optional, sameName, show } from '../args.js'
|
|
6
5
|
import { verifiedDownload } from '../download.js'
|
|
7
|
-
import { decode, encodeSegment } from '../http.js'
|
|
8
|
-
import { gitTreeOfListing } from '../tree.js'
|
|
9
|
-
import { api, bindMethods, call,
|
|
6
|
+
import { decode, encodeSegment, requestWithHeaders } from '../http.js'
|
|
7
|
+
import { gitTreeOfListing, objectId } from '../tree.js'
|
|
8
|
+
import { api, bindMethods, call, clientHeaders, isGone, repoApi } from './client.js'
|
|
10
9
|
|
|
11
10
|
const PER_PAGE = 100
|
|
12
11
|
const MAX_PAGES = 100
|
|
@@ -29,7 +28,7 @@ async function* pages(method, headers, pageUrl, maxPages = MAX_PAGES) {
|
|
|
29
28
|
async function* cursorPages(method, headers, pageUrl, maxPages = MAX_PAGES) {
|
|
30
29
|
const paging = { per_page: PER_PAGE }
|
|
31
30
|
for (let page = 1; ; page++) {
|
|
32
|
-
const answer = await
|
|
31
|
+
const answer = await requestWithHeaders(pageUrl(paging), { as: 'json', headers })
|
|
33
32
|
assert.ok(Array.isArray(answer.body), `${method}: expected an array for page ${page}`)
|
|
34
33
|
yield answer.body
|
|
35
34
|
const next = /<([^>]*)>\s*;\s*rel="next"/u.exec(answer.headers.get('link') ?? '')?.[1]
|
|
@@ -107,7 +106,7 @@ async function getRepoFile(headers, options) {
|
|
|
107
106
|
const bytes = file.encoding === 'none'
|
|
108
107
|
? await call({ ...headers, Accept: 'application/vnd.github.raw' }, repoApi(repo, ['git', 'blobs', file.sha]), { as: 'bytes' })
|
|
109
108
|
: Buffer.from(file.content, 'base64')
|
|
110
|
-
const blob =
|
|
109
|
+
const blob = objectId('blob', bytes).toString('hex')
|
|
111
110
|
assert.ok(bytes.length === file.size && blob === file.sha, `getRepoFile: ${show(path)} came back as blob ${blob}, not ${file.sha}`)
|
|
112
111
|
return decode(bytes, url)
|
|
113
112
|
}
|
package/src/npm/repos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { assertNames, assertPackageName, assertRepo, isRepo, show } from '../args.js'
|
|
4
4
|
import { readRecord, writeRecord } from '../cache.js'
|
|
5
5
|
import { NPM_REGISTRY, buildUrl, isNotFound, recover, request } from '../http.js'
|
|
6
6
|
import { assertRepoDirectory, getRepo, isRepoDirectory } from '../package.js'
|
|
@@ -42,10 +42,7 @@ export async function writePackageRepoCache(name, github, directory = '') {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export async function resolvePackageRepos(packageNames, options = {}) {
|
|
45
|
-
|
|
46
|
-
assertArgs('resolvePackageRepos', options, { cachedOnly: optional(assertBoolean) })
|
|
47
|
-
const names = [...new Set(packageNames)]
|
|
48
|
-
for (const name of names) assertPackageName('resolvePackageRepos', 'name', name)
|
|
45
|
+
const names = assertNames('resolvePackageRepos', 'packageNames', packageNames, options, assertPackageName)
|
|
49
46
|
const found = await pool(names, CONCURRENCY, (name) => (options.cachedOnly ? readPackageRepoCache(name) : lookUpPackageRepo(name)).catch(() => null))
|
|
50
47
|
return new Map(names.flatMap((name, i) => (found[i] ? [[name, found[i]]] : [])))
|
|
51
48
|
}
|
package/src/npm/tarball.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { assertArgs,
|
|
6
|
+
import { assertArgs, assertPackage, assertion, matches, show } from '../args.js'
|
|
7
7
|
import { verifiedDownload } from '../download.js'
|
|
8
8
|
import { NPM_REGISTRY, buildUrl, request } from '../http.js'
|
|
9
9
|
|
|
@@ -13,11 +13,6 @@ const DIR = 'npm/tarballs' // No expiry: the registry never takes a version twic
|
|
|
13
13
|
// last character before `==` two bits, so only A, Q, g or w is canonical.
|
|
14
14
|
const assertIntegrity = assertion('"sha512-" and a base64 sha512', matches(/^sha512-[\dA-Za-z+/]{85}[AQgw]==$/u))
|
|
15
15
|
|
|
16
|
-
function assertPackage(method, name, version) {
|
|
17
|
-
assertPackageName(method, 'name', name)
|
|
18
|
-
assertPackageVersion(method, 'version', version)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
// `{ tarball, integrity }` and nothing else, the tarball exactly the
|
|
22
17
|
// registry's own URL for that version. Each field is read once, and only
|
|
23
18
|
// as the object's own, into a copy that is checked and used from then on:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// The semver peer dependency by a plain require(), which a bundler follows
|
|
4
|
+
// and bundles: CommonJS for that alone. In an ESM bundle that leaves semver
|
|
5
|
+
// out, this require() is the bundler's stub, which throws.
|
|
6
|
+
module.exports = function requirePeer() {
|
|
7
|
+
return require('semver')
|
|
8
|
+
}
|
package/src/semver.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path'
|
|
4
|
+
import requirePeer from './semver-peer.cjs'
|
|
4
5
|
|
|
5
6
|
// npm's own semver, borrowed from the npm next to node rather than added
|
|
6
|
-
// as a dependency: the same implementation `npm audit` uses.
|
|
7
|
+
// as a dependency: the same implementation `npm audit` uses. Where there is
|
|
8
|
+
// no npm, as in a bundled serverless function, the `semver` package itself,
|
|
9
|
+
// an optional peer dependency.
|
|
7
10
|
|
|
8
11
|
// Plain releases that semver.valid answers unchanged, so it need not be
|
|
9
12
|
// loaded for them: no leading zeros, and at most 15 digits a part, under
|
|
@@ -11,26 +14,37 @@ import { basename, dirname, resolve } from 'node:path'
|
|
|
11
14
|
// to semver.
|
|
12
15
|
const PLAIN_RELEASE = /^(?:0|[1-9]\d{0,14})\.(?:0|[1-9]\d{0,14})\.(?:0|[1-9]\d{0,14})$/u
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
// What load() returns if it is semver, or null if it is not or throws.
|
|
18
|
+
function attempt(load) {
|
|
19
|
+
try {
|
|
20
|
+
const lib = load()
|
|
21
|
+
if (['compare', 'intersects', 'satisfies', 'valid', 'validRange'].every((name) => typeof lib?.[name] === 'function')) return lib
|
|
22
|
+
} catch {}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
function find() {
|
|
17
27
|
const argv0 = process.argv[0]
|
|
18
28
|
// Another host executable's `../lib` is not npm's.
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const lib =
|
|
25
|
-
if (
|
|
26
|
-
}
|
|
29
|
+
if (['node', 'node.exe'].includes(basename(argv0 ?? ''))) {
|
|
30
|
+
// POSIX keeps npm in <prefix>/lib beside <prefix>/bin/node; Windows, beside node.exe.
|
|
31
|
+
for (const prefix of ['../lib', '.']) {
|
|
32
|
+
// Required from itself, which a bundle of any format leaves alone.
|
|
33
|
+
const path = resolve(dirname(argv0), prefix, 'node_modules/npm/node_modules/semver')
|
|
34
|
+
const lib = attempt(() => createRequire(path)(path))
|
|
35
|
+
if (lib) return lib
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
|
-
|
|
38
|
+
// The peer, by the plain require() a bundler follows; or, where an ESM
|
|
39
|
+
// bundle leaves it out, by node's own require() from beside the bundle.
|
|
40
|
+
return attempt(requirePeer) ?? attempt(() => createRequire(import.meta.url)('semver'))
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
let found
|
|
44
|
+
|
|
31
45
|
function semver() {
|
|
32
46
|
if (found === undefined) found = find()
|
|
33
|
-
assert.ok(found, 'semver: no npm beside node to borrow it from')
|
|
47
|
+
assert.ok(found, 'semver: no npm beside node to borrow it from, and the semver peer dependency is not installed')
|
|
34
48
|
return found
|
|
35
49
|
}
|
|
36
50
|
|
package/src/soldeer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
|
|
3
|
-
import { assertSha256,
|
|
3
|
+
import { assertSha256, assertSoldeerPackage, show } from './args.js'
|
|
4
4
|
import { verifiedDownload } from './download.js'
|
|
5
5
|
import { SOLDEER_API, SOLDEER_REVISIONS, buildUrl, request } from './http.js'
|
|
6
6
|
|
|
@@ -20,8 +20,7 @@ async function zipUrl(name, version) {
|
|
|
20
20
|
// The registry has no hash to check a zip against, so the caller brings
|
|
21
21
|
// one, as soldeer.lock records it.
|
|
22
22
|
export async function getZip(name, version, checksum) {
|
|
23
|
-
|
|
24
|
-
assertSoldeerVersion('getZip', 'version', version)
|
|
23
|
+
assertSoldeerPackage('getZip', name, version)
|
|
25
24
|
assertSha256('getZip', 'checksum', checksum)
|
|
26
25
|
return await verifiedDownload({ method: 'getZip', dir: DIR, what: `${name}@${version}`, ext: 'zip', algorithm: 'sha256', expected: checksum, locate: () => zipUrl(name, version) })
|
|
27
26
|
}
|
package/src/tree.js
CHANGED
|
@@ -28,7 +28,7 @@ const octal = (header, start, end) => {
|
|
|
28
28
|
const text = field(header, start, end).trim()
|
|
29
29
|
return /^[0-7]{1,12}$/u.test(text) ? Number.parseInt(text, 8) : Number.NaN
|
|
30
30
|
}
|
|
31
|
-
const objectId = (type, content) => createHash('sha1').update(`${type} ${content.length}\0`).update(content).digest()
|
|
31
|
+
export const objectId = (type, content) => createHash('sha1').update(`${type} ${content.length}\0`).update(content).digest()
|
|
32
32
|
const checksum = (header) => header.reduce((sum, byte, i) => sum + (i >= 148 && i < 156 ? 32 : byte), 0)
|
|
33
33
|
const isZero = (bytes) => bytes.every((byte) => byte === 0)
|
|
34
34
|
|
|
@@ -64,9 +64,9 @@ function emptyDirs(dir, prefix = '') {
|
|
|
64
64
|
// Git sorts a subtree as if its name ended in `/`.
|
|
65
65
|
function treeId(dir) {
|
|
66
66
|
const entries = [...dir].map(([name, entry]) => (entry instanceof Map ? { mode: '40000', name, id: treeId(entry) } : { ...entry, name }))
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
return objectId('tree', Buffer.concat(
|
|
67
|
+
const keyed = entries.map((entry) => [entry.mode === '40000' ? `${entry.name}/` : entry.name, entry])
|
|
68
|
+
keyed.sort(([a], [b]) => (a < b ? -1 : Number(a > b)))
|
|
69
|
+
return objectId('tree', Buffer.concat(keyed.flatMap(([, { mode, name, id }]) => [Buffer.from(`${mode} ${name}\0`, 'latin1'), id])))
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const EMPTY_TREE = objectId('tree', Buffer.alloc(0)).toString('hex')
|
|
@@ -90,11 +90,10 @@ async function holdsNothing(sha, listed) {
|
|
|
90
90
|
// names any, and one is put back only once its own listings show it holds
|
|
91
91
|
// nothing.
|
|
92
92
|
async function putBackEmptyTrees(dir, sha, listed) {
|
|
93
|
-
if (treeId(dir).toString('hex') === sha) return
|
|
94
93
|
for (const entry of await listed(sha)) {
|
|
95
94
|
if (entry.type !== 'tree') continue
|
|
96
95
|
const here = dir.get(entry.name)
|
|
97
|
-
if (here instanceof Map) await putBackEmptyTrees(here, entry.sha, listed)
|
|
96
|
+
if (here instanceof Map && treeId(here).toString('hex') !== entry.sha) await putBackEmptyTrees(here, entry.sha, listed)
|
|
98
97
|
else if (here === undefined && await holdsNothing(entry.sha, listed)) dir.set(entry.name, subtree(entry.sha))
|
|
99
98
|
}
|
|
100
99
|
}
|
|
@@ -177,6 +176,8 @@ export async function gitTreeOfTarball(gzipped, { expected, list } = {}) {
|
|
|
177
176
|
if (!isSha1(sha)) return `no tree: an empty directory, ${JSON.stringify(path)}, and no submodule there`
|
|
178
177
|
dir.set(name, { mode: '160000', id: Buffer.from(sha, 'hex') })
|
|
179
178
|
}
|
|
179
|
+
const id = treeId(root).toString('hex')
|
|
180
|
+
if (id === expected) return id
|
|
180
181
|
await putBackEmptyTrees(root, expected, listed)
|
|
181
182
|
return treeId(root).toString('hex')
|
|
182
183
|
}
|