dsh-plugin-upgrader 1.0.0 → 1.0.2
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/CHANGELOG.md +28 -0
- package/README.en.md +52 -21
- package/README.md +57 -20
- package/lib/client.js +959 -0
- package/lib/host/bundle.d.ts +52 -0
- package/lib/host/bundle.d.ts.map +1 -0
- package/lib/host/bundle.js +73 -0
- package/lib/host/bundle.js.map +1 -0
- package/lib/host/http.d.ts +46 -0
- package/lib/host/http.d.ts.map +1 -0
- package/lib/host/http.js +104 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/install.d.ts +71 -0
- package/lib/host/install.d.ts.map +1 -0
- package/lib/host/install.js +161 -0
- package/lib/host/install.js.map +1 -0
- package/lib/host/manager.d.ts +151 -0
- package/lib/host/manager.d.ts.map +1 -0
- package/lib/host/manager.js +28 -0
- package/lib/host/manager.js.map +1 -0
- package/lib/host/overview.d.ts +52 -0
- package/lib/host/overview.d.ts.map +1 -0
- package/lib/host/overview.js +109 -0
- package/lib/host/overview.js.map +1 -0
- package/lib/host/registry.d.ts +116 -0
- package/lib/host/registry.d.ts.map +1 -0
- package/lib/host/registry.js +177 -0
- package/lib/host/registry.js.map +1 -0
- package/lib/host/repo.d.ts +62 -0
- package/lib/host/repo.d.ts.map +1 -0
- package/lib/host/repo.js +102 -0
- package/lib/host/repo.js.map +1 -0
- package/lib/host/version.d.ts +67 -0
- package/lib/host/version.d.ts.map +1 -0
- package/lib/host/version.js +114 -0
- package/lib/host/version.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +254 -0
- package/lib/index.js.map +1 -0
- package/package.json +32 -14
- package/screenshots/history.png +0 -0
- package/screenshots/selection.png +0 -0
- package/screenshots.json +0 -4
- package/src/client.js +0 -1136
- package/src/index.js +0 -275
- package/src/lib/bundle.js +0 -64
- package/src/lib/http.js +0 -93
- package/src/lib/install.js +0 -163
- package/src/lib/overview.js +0 -108
- package/src/lib/registry.js +0 -188
- package/src/lib/repo.js +0 -91
- package/src/lib/version.js +0 -107
package/src/lib/overview.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The overview payload the Versions tab renders.
|
|
3
|
-
*
|
|
4
|
-
* One request answers everything the tab needs — installed bundles, their
|
|
5
|
-
* published versions, which of them are upgradable — because a per-bundle
|
|
6
|
-
* request would turn one page open into a dozen round trips.
|
|
7
|
-
*
|
|
8
|
-
* @module dsh-plugin-upgrader/lib/overview
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { compareVersions } from './version.js'
|
|
12
|
-
import { repoUrlOf } from './repo.js'
|
|
13
|
-
import { inlineIcon, pickText, registryBase, sourceKind } from './bundle.js'
|
|
14
|
-
import { cachedTags, fetchPackument, shapePackument } from './registry.js'
|
|
15
|
-
|
|
16
|
-
/** Inline data-URL icons above this size are dropped from the overview payload. */
|
|
17
|
-
const INLINE_ICON_LIMIT = 2048
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Build the overview the tab renders.
|
|
21
|
-
* @param {any} ctx - the plugin context.
|
|
22
|
-
* @param {Map<string, { at: number, data: any }>} cache - packument cache.
|
|
23
|
-
* @param {Map<string, { at: number, tags?: Set<string>, error?: string }>} tagCache - repository tag cache.
|
|
24
|
-
* @param {{ force?: boolean, locale?: 'zh' | 'en' }} options - refresh + language.
|
|
25
|
-
* @returns {Promise<any>} the overview payload.
|
|
26
|
-
*/
|
|
27
|
-
export async function buildOverview(ctx, cache, tagCache, options) {
|
|
28
|
-
const manager = ctx.get('pluginManager', false)
|
|
29
|
-
if (manager === undefined || manager === null) {
|
|
30
|
-
throw Object.assign(new Error('plugin manager unavailable'), { status: 503 })
|
|
31
|
-
}
|
|
32
|
-
const [bundles, registries] = await Promise.all([manager.listBundles(), manager.registries()])
|
|
33
|
-
const base = registryBase(registries)
|
|
34
|
-
const locale = options.locale === 'zh' ? 'zh' : 'en'
|
|
35
|
-
|
|
36
|
-
const rows = await Promise.all(
|
|
37
|
-
bundles.filter((bundle) => bundle.installed === true).map(async (bundle) => {
|
|
38
|
-
const kind = sourceKind(bundle.source)
|
|
39
|
-
/** Official rows live inside the installation and cannot be replaced. */
|
|
40
|
-
const managed = bundle.availability === 'profile' && bundle.installed === true
|
|
41
|
-
const entry = {
|
|
42
|
-
name: bundle.name,
|
|
43
|
-
version: bundle.version,
|
|
44
|
-
official: bundle.official === true,
|
|
45
|
-
availability: bundle.availability,
|
|
46
|
-
installed: bundle.installed === true,
|
|
47
|
-
enabled: bundle.enabled === true,
|
|
48
|
-
removable: bundle.removable === true,
|
|
49
|
-
managed,
|
|
50
|
-
source: bundle.source,
|
|
51
|
-
kind,
|
|
52
|
-
title: pickText(bundle.meta?.title, locale) ?? bundle.name,
|
|
53
|
-
description: pickText(bundle.meta?.description ?? bundle.description, locale),
|
|
54
|
-
icon: inlineIcon(bundle.meta?.icon, INLINE_ICON_LIMIT),
|
|
55
|
-
readOnlyReason: bundle.readOnlyReason,
|
|
56
|
-
error: bundle.error ?? null,
|
|
57
|
-
latest: undefined,
|
|
58
|
-
repoUrl: undefined,
|
|
59
|
-
upgradable: false,
|
|
60
|
-
versions: [],
|
|
61
|
-
tags: {},
|
|
62
|
-
totalVersions: 0,
|
|
63
|
-
versionError: undefined,
|
|
64
|
-
}
|
|
65
|
-
if (!managed || bundle.version === undefined) {
|
|
66
|
-
entry.versionError = managed ? 'version unknown' : 'not a profile-managed package'
|
|
67
|
-
return entry
|
|
68
|
-
}
|
|
69
|
-
if (kind !== 'registry' && kind !== 'unknown') {
|
|
70
|
-
entry.versionError = `${kind} source: no registry history`
|
|
71
|
-
return entry
|
|
72
|
-
}
|
|
73
|
-
const { data, error } = await fetchPackument(base, bundle.name, cache, options.force === true)
|
|
74
|
-
if (error !== undefined || data === undefined) {
|
|
75
|
-
entry.versionError = error ?? 'registry lookup failed'
|
|
76
|
-
return entry
|
|
77
|
-
}
|
|
78
|
-
const repoUrl = repoUrlOf(data)
|
|
79
|
-
const tagNames = repoUrl === undefined
|
|
80
|
-
? undefined
|
|
81
|
-
: await cachedTags(repoUrl, tagCache, options.force === true)
|
|
82
|
-
const shaped = shapePackument(bundle.name, data, bundle.version, tagNames)
|
|
83
|
-
entry.latest = shaped.latest
|
|
84
|
-
entry.repoUrl = shaped.repoUrl
|
|
85
|
-
entry.versions = shaped.versions
|
|
86
|
-
entry.tags = shaped.tags
|
|
87
|
-
entry.totalVersions = shaped.totalVersions ?? shaped.versions.length
|
|
88
|
-
entry.versionError = shaped.versionError
|
|
89
|
-
entry.upgradable =
|
|
90
|
-
entry.latest !== undefined && compareVersions(entry.latest, bundle.version) > 0
|
|
91
|
-
return entry
|
|
92
|
-
}),
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
rows.sort((left, right) => {
|
|
96
|
-
if (left.upgradable !== right.upgradable) return left.upgradable ? -1 : 1
|
|
97
|
-
if (left.managed !== right.managed) return left.managed ? -1 : 1
|
|
98
|
-
return left.name.localeCompare(right.name)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
generatedAt: Date.now(),
|
|
103
|
-
registry: base,
|
|
104
|
-
registries,
|
|
105
|
-
runtimeVersion: ctx.get('dshVersion', false) ?? null,
|
|
106
|
-
bundles: rows,
|
|
107
|
-
}
|
|
108
|
-
}
|
package/src/lib/registry.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registry and GitHub lookups.
|
|
3
|
-
*
|
|
4
|
-
* The browser half cannot reach npm reliably (CORS, private registries, tokens)
|
|
5
|
-
* and cannot reach the GitHub API at all without leaking rate limits to every
|
|
6
|
-
* page load, so both lookups happen here, behind a cache.
|
|
7
|
-
*
|
|
8
|
-
* @module dsh-plugin-upgrader/lib/registry
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { compareVersions, releasePathOf } from './version.js'
|
|
12
|
-
import { githubUrlOf } from './repo.js'
|
|
13
|
-
|
|
14
|
-
/** How long one registry packument stays fresh. */
|
|
15
|
-
export const PACKUMENT_TTL_MS = 5 * 60 * 1000
|
|
16
|
-
|
|
17
|
-
/** Registry lookups must not hang a page render. */
|
|
18
|
-
export const PACKUMENT_TIMEOUT_MS = 10 * 1000
|
|
19
|
-
|
|
20
|
-
/** Version lists are capped so one huge package cannot flood the tab. */
|
|
21
|
-
export const MAX_VERSIONS = 80
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* How long one repository's tag list stays fresh. The GitHub API allows 60
|
|
25
|
-
* unauthenticated requests an hour and one tag list costs a request per 100
|
|
26
|
-
* tags, so this is deliberately much longer than the packument TTL.
|
|
27
|
-
*/
|
|
28
|
-
export const TAGS_TTL_MS = 30 * 60 * 1000
|
|
29
|
-
|
|
30
|
-
/** A failed tag lookup is retried sooner than a good one is refreshed. */
|
|
31
|
-
export const TAGS_RETRY_MS = 5 * 60 * 1000
|
|
32
|
-
|
|
33
|
-
/** Tag lookups must not hang a page render. */
|
|
34
|
-
export const TAGS_TIMEOUT_MS = 8 * 1000
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Fetch one package's packument, with a short-lived cache.
|
|
38
|
-
* @param {string} base - registry base URL.
|
|
39
|
-
* @param {string} packageName - package name.
|
|
40
|
-
* @param {Map<string, { at: number, data: any }>} cache - shared cache.
|
|
41
|
-
* @param {boolean} force - bypass a fresh cache entry.
|
|
42
|
-
* @returns {Promise<{ data?: any, error?: string }>} the document or a reason.
|
|
43
|
-
*/
|
|
44
|
-
export async function fetchPackument(base, packageName, cache, force) {
|
|
45
|
-
const key = `${base}|${packageName}`
|
|
46
|
-
const hit = cache.get(key)
|
|
47
|
-
if (force !== true && hit !== undefined && Date.now() - hit.at < PACKUMENT_TTL_MS) {
|
|
48
|
-
return { data: hit.data }
|
|
49
|
-
}
|
|
50
|
-
const url = `${base}/${packageName.startsWith('@') ? '@' + encodeURIComponent(packageName.slice(1)) : encodeURIComponent(packageName)}`
|
|
51
|
-
try {
|
|
52
|
-
const response = await fetch(url, {
|
|
53
|
-
headers: { accept: 'application/json' },
|
|
54
|
-
signal: AbortSignal.timeout(PACKUMENT_TIMEOUT_MS),
|
|
55
|
-
})
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
return { error: `registry ${response.status}${response.status === 404 ? ' (not found)' : ''}` }
|
|
58
|
-
}
|
|
59
|
-
const data = await response.json()
|
|
60
|
-
cache.set(key, { at: Date.now(), data })
|
|
61
|
-
return { data }
|
|
62
|
-
} catch (error) {
|
|
63
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
64
|
-
return { error: `registry unreachable: ${message}` }
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Read a repository's tag names from the GitHub API.
|
|
70
|
-
*
|
|
71
|
-
* The git ref advertisement (`/info/refs?service=git-upload-pack`) would avoid
|
|
72
|
-
* the rate limit but stalls for most repositories behind this network, while the
|
|
73
|
-
* REST list answers in well under a second. Only the first page is read: the
|
|
74
|
-
* version list itself is capped at 80 newest rows, and tags are ordered newest
|
|
75
|
-
* first, so a repository with more than 100 tags still resolves every row that
|
|
76
|
-
* can be shown.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} repoUrl - the repository URL.
|
|
79
|
-
* @param {number} at - the time the lookup started.
|
|
80
|
-
* @returns {Promise<{ tags?: Set<string>, error?: string, at: number }>} the outcome.
|
|
81
|
-
*/
|
|
82
|
-
export async function fetchTags(repoUrl, at) {
|
|
83
|
-
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?\/?$/.exec(repoUrl)
|
|
84
|
-
if (match === null) return { error: 'not a github repository', at }
|
|
85
|
-
const [, owner, repo] = match
|
|
86
|
-
try {
|
|
87
|
-
const response = await fetch(
|
|
88
|
-
`https://api.github.com/repos/${owner}/${repo}/tags?per_page=100`,
|
|
89
|
-
{
|
|
90
|
-
headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-plugin-upgrader' },
|
|
91
|
-
signal: AbortSignal.timeout(TAGS_TIMEOUT_MS),
|
|
92
|
-
},
|
|
93
|
-
)
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
const suffix = response.status === 403 || response.status === 429 ? ' (rate limited)' : ''
|
|
96
|
-
return { error: `github ${response.status}${suffix}`, at }
|
|
97
|
-
}
|
|
98
|
-
const list = await response.json()
|
|
99
|
-
if (!Array.isArray(list)) return { error: 'github returned no tag list', at }
|
|
100
|
-
const tags = new Set()
|
|
101
|
-
for (const entry of list) {
|
|
102
|
-
if (typeof entry?.name === 'string') tags.add(entry.name)
|
|
103
|
-
}
|
|
104
|
-
return { tags, at }
|
|
105
|
-
} catch (error) {
|
|
106
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
107
|
-
return { error: `github unreachable: ${message}`, at }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* The tag names of a repository, cached.
|
|
113
|
-
*
|
|
114
|
-
* A tag list is the only way to know whether `/releases/tag/v<version>` exists.
|
|
115
|
-
* A failed lookup is cached too, but for less time: the tab must still render
|
|
116
|
-
* (with repository-level links) while GitHub is rate limiting.
|
|
117
|
-
*
|
|
118
|
-
* @param {string} repoUrl - the repository URL.
|
|
119
|
-
* @param {Map<string, { at: number, tags?: Set<string>, error?: string }>} cache - shared cache.
|
|
120
|
-
* @param {boolean} force - bypass a fresh cache entry.
|
|
121
|
-
* @returns {Promise<Set<string> | undefined>} the tag names, or undefined when unknown.
|
|
122
|
-
*/
|
|
123
|
-
export async function cachedTags(repoUrl, cache, force) {
|
|
124
|
-
const hit = cache.get(repoUrl)
|
|
125
|
-
if (hit !== undefined && force !== true) {
|
|
126
|
-
const ttl = hit.tags === undefined ? TAGS_RETRY_MS : TAGS_TTL_MS
|
|
127
|
-
if (Date.now() - hit.at < ttl) return hit.tags
|
|
128
|
-
}
|
|
129
|
-
const outcome = await fetchTags(repoUrl, Date.now())
|
|
130
|
-
cache.set(repoUrl, outcome)
|
|
131
|
-
return outcome.tags
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Trim one packument down to what the tab renders.
|
|
136
|
-
* @param {string} packageName - the package asked for.
|
|
137
|
-
* @param {any} packument - the full registry document.
|
|
138
|
-
* @param {string | undefined} installedVersion - the version in the profile.
|
|
139
|
-
* @param {Set<string> | undefined} tagNames - the repository's tag names, when known.
|
|
140
|
-
* @returns {{
|
|
141
|
-
* latest?: string,
|
|
142
|
-
* repoUrl?: string,
|
|
143
|
-
* versions: any[],
|
|
144
|
-
* tags: Record<string, string>,
|
|
145
|
-
* totalVersions?: number,
|
|
146
|
-
* versionError?: string,
|
|
147
|
-
* }} the slice.
|
|
148
|
-
*/
|
|
149
|
-
export function shapePackument(packageName, packument, installedVersion, tagNames) {
|
|
150
|
-
const versionsRecord = packument?.versions
|
|
151
|
-
if (versionsRecord === null || typeof versionsRecord !== 'object') {
|
|
152
|
-
return { versions: [], tags: {}, versionError: 'registry returned no versions' }
|
|
153
|
-
}
|
|
154
|
-
const tags = packument['dist-tags'] !== null && typeof packument['dist-tags'] === 'object'
|
|
155
|
-
? packument['dist-tags']
|
|
156
|
-
: {}
|
|
157
|
-
const newest = tags.latest
|
|
158
|
-
const timeRecord = packument.time !== null && typeof packument.time === 'object' ? packument.time : {}
|
|
159
|
-
const names = Object.keys(versionsRecord)
|
|
160
|
-
const newestManifest = typeof newest === 'string' ? versionsRecord[newest] : undefined
|
|
161
|
-
const repoUrl = githubUrlOf(packument, newestManifest)
|
|
162
|
-
const rows = names.map((version) => {
|
|
163
|
-
const manifest = versionsRecord[version] ?? {}
|
|
164
|
-
const engine = manifest?.engines?.dsh ?? manifest?.dsh?.engines?.dsh
|
|
165
|
-
return {
|
|
166
|
-
version,
|
|
167
|
-
time: typeof timeRecord[version] === 'string' ? timeRecord[version] : undefined,
|
|
168
|
-
deprecated: typeof manifest.deprecated === 'string' ? manifest.deprecated : undefined,
|
|
169
|
-
engine: typeof engine === 'string' ? engine : undefined,
|
|
170
|
-
current: version === installedVersion,
|
|
171
|
-
latest: version === newest,
|
|
172
|
-
url: repoUrl === undefined ? undefined : releasePathOf(tagNames, version),
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
rows.sort((left, right) => compareVersions(right.version, left.version))
|
|
176
|
-
const kept = rows.slice(0, MAX_VERSIONS)
|
|
177
|
-
const currentRow = rows.find((row) => row.current === true)
|
|
178
|
-
if (currentRow !== undefined && !kept.includes(currentRow)) kept.push(currentRow)
|
|
179
|
-
return {
|
|
180
|
-
latest: typeof newest === 'string' ? newest : undefined,
|
|
181
|
-
repoUrl,
|
|
182
|
-
versions: kept,
|
|
183
|
-
tags: Object.fromEntries(
|
|
184
|
-
Object.entries(tags).filter(([, value]) => typeof value === 'string'),
|
|
185
|
-
),
|
|
186
|
-
totalVersions: rows.length,
|
|
187
|
-
}
|
|
188
|
-
}
|
package/src/lib/repo.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Repository-URL normalisation.
|
|
3
|
-
*
|
|
4
|
-
* A registry packument can declare its `repository` in half a dozen shapes, and
|
|
5
|
-
* the tab only ever wants one thing out of it: the https URL of the project
|
|
6
|
-
* page. Turning that into data is pure work, so it lives here rather than in
|
|
7
|
-
* the request handler.
|
|
8
|
-
*
|
|
9
|
-
* @module dsh-plugin-upgrader/lib/repo
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Turn any of the shapes npm allows for `repository` into an https URL.
|
|
14
|
-
* Handles `owner/repo`, `github:owner/repo`, `git@host:owner/repo.git`,
|
|
15
|
-
* `git+https://…`, and plain URLs; returns `undefined` for anything else.
|
|
16
|
-
* @param {unknown} raw - the manifest field.
|
|
17
|
-
* @returns {string | undefined} an https URL without a trailing slash.
|
|
18
|
-
*/
|
|
19
|
-
export function normalizeRepoUrl(raw) {
|
|
20
|
-
const text = typeof raw === 'string'
|
|
21
|
-
? raw
|
|
22
|
-
: raw !== null && typeof raw === 'object' && typeof (/** @type {any} */ (raw).url) === 'string'
|
|
23
|
-
? /** @type {any} */ (raw).url
|
|
24
|
-
: undefined
|
|
25
|
-
if (text === undefined) return undefined
|
|
26
|
-
let value = text.trim()
|
|
27
|
-
if (value === '') return undefined
|
|
28
|
-
|
|
29
|
-
const shorthand = /^(?:github:)?([\w.-]+\/[\w.-]+)$/.exec(value)
|
|
30
|
-
if (shorthand !== null) value = `https://github.com/${shorthand[1]}`
|
|
31
|
-
else if (value.startsWith('git@')) value = `https://${value.slice(4).replace(':', '/')}`
|
|
32
|
-
else if (value.startsWith('ssh://git@')) value = `https://${value.slice('ssh://git@'.length)}`
|
|
33
|
-
else value = value.replace(/^git\+/, '')
|
|
34
|
-
|
|
35
|
-
value = value.replace(/\.git$/, '').replace(/\/+$/, '')
|
|
36
|
-
try {
|
|
37
|
-
const parsed = new URL(value)
|
|
38
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return undefined
|
|
39
|
-
parsed.hash = ''
|
|
40
|
-
parsed.search = ''
|
|
41
|
-
return parsed.toString().replace(/\/+$/, '')
|
|
42
|
-
} catch {
|
|
43
|
-
return undefined
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Is this a github.com URL?
|
|
49
|
-
* @param {string} url - an absolute URL.
|
|
50
|
-
* @returns {boolean} whether the host is GitHub.
|
|
51
|
-
*/
|
|
52
|
-
export function isGithubUrl(url) {
|
|
53
|
-
try {
|
|
54
|
-
return /^(www\.)?github\.com$/.test(new URL(url).hostname)
|
|
55
|
-
} catch {
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The project page a user would recognise as "the plugin's GitHub".
|
|
62
|
-
* `repository` wins; `homepage`/`bugs` are only used when they point at GitHub,
|
|
63
|
-
* so a marketing site never stands in for the source link.
|
|
64
|
-
* @param {any} packument - the registry document.
|
|
65
|
-
* @param {{ repository?: unknown, homepage?: unknown, bugs?: unknown } | undefined} manifest - one version manifest.
|
|
66
|
-
* @returns {string | undefined} an https GitHub URL.
|
|
67
|
-
*/
|
|
68
|
-
export function githubUrlOf(packument, manifest) {
|
|
69
|
-
const direct = [
|
|
70
|
-
normalizeRepoUrl(manifest?.repository),
|
|
71
|
-
normalizeRepoUrl(packument?.repository),
|
|
72
|
-
].find((url) => url !== undefined && url !== null && isGithubUrl(url))
|
|
73
|
-
if (direct !== undefined) return direct
|
|
74
|
-
const fallbacks = [
|
|
75
|
-
normalizeRepoUrl(packument?.homepage),
|
|
76
|
-
normalizeRepoUrl(manifest?.homepage),
|
|
77
|
-
normalizeRepoUrl(packument?.bugs),
|
|
78
|
-
]
|
|
79
|
-
return fallbacks.find((url) => url !== undefined && url !== null && isGithubUrl(url))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The repository a packument points at, independent of any version row.
|
|
84
|
-
* @param {any} packument - the registry document.
|
|
85
|
-
* @returns {string | undefined} an https GitHub URL.
|
|
86
|
-
*/
|
|
87
|
-
export function repoUrlOf(packument) {
|
|
88
|
-
const latest = packument?.['dist-tags']?.latest
|
|
89
|
-
const manifest = typeof latest === 'string' ? packument?.versions?.[latest] : undefined
|
|
90
|
-
return githubUrlOf(packument, manifest)
|
|
91
|
-
}
|
package/src/lib/version.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Version ordering and release-link helpers.
|
|
3
|
-
*
|
|
4
|
-
* Everything here is pure: no I/O, no clock, no ambient state. The host half
|
|
5
|
-
* uses it to sort published versions and to decide which GitHub page a version
|
|
6
|
-
* line opens, and the test suite exercises it directly.
|
|
7
|
-
*
|
|
8
|
-
* @module dsh-plugin-upgrader/lib/version
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Parse a semver string into comparable parts.
|
|
13
|
-
* @param {string} value - a version like `1.2.3-rc.1`.
|
|
14
|
-
* @returns {{ major: number, minor: number, patch: number, pre: string[] } | null} parts, or null.
|
|
15
|
-
*/
|
|
16
|
-
export function parseVersion(value) {
|
|
17
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
18
|
-
String(value ?? '').trim(),
|
|
19
|
-
)
|
|
20
|
-
if (match === null) return null
|
|
21
|
-
return {
|
|
22
|
-
major: Number(match[1]),
|
|
23
|
-
minor: Number(match[2]),
|
|
24
|
-
patch: Number(match[3]),
|
|
25
|
-
pre: match[4] === undefined ? [] : match[4].split('.'),
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Compare two prerelease identifier lists, semver order.
|
|
31
|
-
* @param {string[]} left - identifiers of the left version.
|
|
32
|
-
* @param {string[]} right - identifiers of the right version.
|
|
33
|
-
* @returns {number} negative, zero, or positive.
|
|
34
|
-
*/
|
|
35
|
-
export function comparePre(left, right) {
|
|
36
|
-
if (left.length === 0 && right.length === 0) return 0
|
|
37
|
-
if (left.length === 0) return 1
|
|
38
|
-
if (right.length === 0) return -1
|
|
39
|
-
const length = Math.max(left.length, right.length)
|
|
40
|
-
for (let index = 0; index < length; index += 1) {
|
|
41
|
-
const a = left[index]
|
|
42
|
-
const b = right[index]
|
|
43
|
-
if (a === undefined) return -1
|
|
44
|
-
if (b === undefined) return 1
|
|
45
|
-
const aNumeric = /^\d+$/.test(a)
|
|
46
|
-
const bNumeric = /^\d+$/.test(b)
|
|
47
|
-
if (aNumeric && bNumeric) {
|
|
48
|
-
const delta = Number(a) - Number(b)
|
|
49
|
-
if (delta !== 0) return delta
|
|
50
|
-
continue
|
|
51
|
-
}
|
|
52
|
-
if (aNumeric) return -1
|
|
53
|
-
if (bNumeric) return 1
|
|
54
|
-
if (a !== b) return a < b ? -1 : 1
|
|
55
|
-
}
|
|
56
|
-
return 0
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Compare two versions, semver order; unparsable pairs compare equal.
|
|
61
|
-
* @param {string} left - left version.
|
|
62
|
-
* @param {string} right - right version.
|
|
63
|
-
* @returns {number} negative when left is older.
|
|
64
|
-
*/
|
|
65
|
-
export function compareVersions(left, right) {
|
|
66
|
-
const a = parseVersion(left)
|
|
67
|
-
const b = parseVersion(right)
|
|
68
|
-
if (a === null || b === null) return 0
|
|
69
|
-
return (
|
|
70
|
-
a.major - b.major ||
|
|
71
|
-
a.minor - b.minor ||
|
|
72
|
-
a.patch - b.patch ||
|
|
73
|
-
comparePre(a.pre, b.pre)
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The tag a published version was released under.
|
|
79
|
-
* @param {string} version - the published version.
|
|
80
|
-
* @returns {string} `v<version>` unless the version already carries the `v`.
|
|
81
|
-
*/
|
|
82
|
-
export function tagOf(version) {
|
|
83
|
-
return version.startsWith('v') ? version : `v${version}`
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The GitHub path a version line opens.
|
|
88
|
-
*
|
|
89
|
-
* Users read a version line as "what did this release change", so the target is
|
|
90
|
-
* the release page. That only resolves when the repository actually tagged the
|
|
91
|
-
* version — `dsh-pet` publishes 59 versions with 8 tags — so the tag list
|
|
92
|
-
* decides, and a version without a tag of its own falls back to the repository's
|
|
93
|
-
* release feed rather than to a page GitHub would answer with 404. When the tag
|
|
94
|
-
* list cannot be read at all (rate limit, offline) the same feed is the only
|
|
95
|
-
* honest answer: guessing a tag would make one link in six a dead end.
|
|
96
|
-
*
|
|
97
|
-
* The result is the `releases` tail only: the row already carries the project
|
|
98
|
-
* URL, and repeating it on all ~80 rows costs tens of kilobytes.
|
|
99
|
-
*
|
|
100
|
-
* @param {Set<string> | undefined} tags - the repository's tag names, when known.
|
|
101
|
-
* @param {string} version - the published version.
|
|
102
|
-
* @returns {string} a protocol-relative path such as `/releases/tag/v1.2.3`.
|
|
103
|
-
*/
|
|
104
|
-
export function releasePathOf(tags, version) {
|
|
105
|
-
const tag = tagOf(version)
|
|
106
|
-
return tags?.has(tag) === true ? `/releases/tag/${tag}` : '/releases'
|
|
107
|
-
}
|