dsh-plugin-rack 0.1.2 → 0.1.3
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/README.md +33 -9
- package/client.js +191 -84
- package/index.js +61 -51
- package/lib/spec.js +285 -0
- package/package.json +14 -6
- package/test/behaviour.mjs +0 -549
- package/test/download.mjs +0 -221
- package/test/job-background.mjs +0 -205
- package/test/pnpm-command.mjs +0 -49
- package/test/self-check.mjs +0 -124
package/index.js
CHANGED
|
@@ -63,10 +63,14 @@ import { request as httpsRequest } from 'node:https'
|
|
|
63
63
|
import { connect as netConnect } from 'node:net'
|
|
64
64
|
import { connect as tlsConnect } from 'node:tls'
|
|
65
65
|
import { createRequire } from 'node:module'
|
|
66
|
+
import { EXACT_VERSION, compareVersions, isVersionSpec, parseSpec, satisfiesSpec } from './lib/spec.js'
|
|
66
67
|
|
|
67
68
|
const ROUTE_PREFIX = '/api/dsh-plugin-rack'
|
|
68
69
|
const require = createRequire(import.meta.url)
|
|
69
|
-
|
|
70
|
+
// Read from package.json instead of a literal: the panel reports this number to
|
|
71
|
+
// the user and to the update checker, and a hand-maintained copy drifted from
|
|
72
|
+
// the published version the first time it mattered.
|
|
73
|
+
const VERSION = require('./package.json').version
|
|
70
74
|
// The package was called dsh-plugin-dashboard before the public release. Keep
|
|
71
75
|
// the old name recognised so a half-migrated install still treats the panel as
|
|
72
76
|
// itself — otherwise "turn everything off" would switch off the very route
|
|
@@ -1501,8 +1505,6 @@ async function readJsonBody(req) {
|
|
|
1501
1505
|
|
|
1502
1506
|
/** Packages the runtime itself provides; those are never fetched from npm. */
|
|
1503
1507
|
const HOST_SCOPE = /^@deepseek-ai\//
|
|
1504
|
-
/** A version pinned exactly — the profile's own convention for dsh bundles. */
|
|
1505
|
-
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
1506
1508
|
const REGISTRY_TIMEOUT = 12000
|
|
1507
1509
|
const UPDATE_CONCURRENCY = 6
|
|
1508
1510
|
|
|
@@ -1522,44 +1524,6 @@ function profileFacts(ctx) {
|
|
|
1522
1524
|
}
|
|
1523
1525
|
}
|
|
1524
1526
|
|
|
1525
|
-
function versionParts(value) {
|
|
1526
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/.exec(String(value).trim())
|
|
1527
|
-
if (match === null) return null
|
|
1528
|
-
return {
|
|
1529
|
-
numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
1530
|
-
pre: match[4] === undefined ? null : match[4].split('.'),
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
/** Standard semver ordering; an unparsable side compares as equal. */
|
|
1535
|
-
function compareVersions(left, right) {
|
|
1536
|
-
const a = versionParts(left)
|
|
1537
|
-
const b = versionParts(right)
|
|
1538
|
-
if (a === null || b === null) return 0
|
|
1539
|
-
for (let index = 0; index < 3; index += 1) {
|
|
1540
|
-
if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] < b.numbers[index] ? -1 : 1
|
|
1541
|
-
}
|
|
1542
|
-
if (a.pre === null && b.pre === null) return 0
|
|
1543
|
-
if (a.pre === null) return 1
|
|
1544
|
-
if (b.pre === null) return -1
|
|
1545
|
-
const length = Math.max(a.pre.length, b.pre.length)
|
|
1546
|
-
for (let index = 0; index < length; index += 1) {
|
|
1547
|
-
const x = a.pre[index]
|
|
1548
|
-
const y = b.pre[index]
|
|
1549
|
-
if (x === undefined) return -1
|
|
1550
|
-
if (y === undefined) return 1
|
|
1551
|
-
const xNumeric = /^\d+$/.test(x)
|
|
1552
|
-
const yNumeric = /^\d+$/.test(y)
|
|
1553
|
-
if (xNumeric && yNumeric) {
|
|
1554
|
-
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
|
|
1555
|
-
continue
|
|
1556
|
-
}
|
|
1557
|
-
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1
|
|
1558
|
-
if (x !== y) return x < y ? -1 : 1
|
|
1559
|
-
}
|
|
1560
|
-
return 0
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
1527
|
/**
|
|
1564
1528
|
* The registry answer for a package's `latest` dist-tag. The profile's own
|
|
1565
1529
|
* registry is asked first — a user behind a mirror should not be forced through
|
|
@@ -1597,15 +1561,29 @@ async function npmLatest(name, base, signal) {
|
|
|
1597
1561
|
throw new Error(errors.join('; '))
|
|
1598
1562
|
}
|
|
1599
1563
|
|
|
1600
|
-
/**
|
|
1564
|
+
/**
|
|
1565
|
+
* Dependencies this panel is willing to touch: host-provided ones are skipped,
|
|
1566
|
+
* and every spec it understands becomes a target.
|
|
1567
|
+
*
|
|
1568
|
+
* This used to accept exact pins only, which quietly meant that a profile using
|
|
1569
|
+
* `^1.2.3` — the normal way to write a dependency — had those packages invisible
|
|
1570
|
+
* to "update everything". The range still matters though: it is kept on the
|
|
1571
|
+
* target so the checker can refuse a registry version the user's own range
|
|
1572
|
+
* excludes, instead of silently jumping a major.
|
|
1573
|
+
*/
|
|
1601
1574
|
function eligibleDeps(facts) {
|
|
1602
1575
|
const out = []
|
|
1603
1576
|
for (const [name, spec] of Object.entries(facts.deps)) {
|
|
1604
1577
|
if (HOST_SCOPE.test(name)) continue
|
|
1605
1578
|
if (typeof spec !== 'string') continue
|
|
1606
|
-
const
|
|
1607
|
-
if (
|
|
1608
|
-
|
|
1579
|
+
const parsed = parseSpec(spec)
|
|
1580
|
+
if (parsed.kind === 'unsupported') continue
|
|
1581
|
+
// What is actually installed beats the range floor: with `^1.2.3` installed
|
|
1582
|
+
// at 1.9.0, comparing against the floor would report an update to 1.9.0.
|
|
1583
|
+
const installed = installedVersion(facts.dir, name)
|
|
1584
|
+
const current = installed !== null ? installed : parsed.floor
|
|
1585
|
+
if (current === null) continue
|
|
1586
|
+
out.push({ name, current, spec: spec.trim(), kind: parsed.kind, floor: parsed.floor })
|
|
1609
1587
|
}
|
|
1610
1588
|
return out
|
|
1611
1589
|
}
|
|
@@ -1625,11 +1603,18 @@ async function checkUpdates(ctx) {
|
|
|
1625
1603
|
const target = targets[index]
|
|
1626
1604
|
try {
|
|
1627
1605
|
const latest = await npmLatest(target.name, base, activeJobAbort === null ? undefined : activeJobAbort.signal)
|
|
1606
|
+
const ahead = compareVersions(latest, target.current) > 0
|
|
1607
|
+
// An exact pin has no range to violate. Anything else does, and a
|
|
1608
|
+
// major bump the user's own `^`/`~` excludes is not this panel's call.
|
|
1609
|
+
const inRange = target.kind === 'exact' || satisfiesSpec(latest, target.spec)
|
|
1628
1610
|
rows[index] = {
|
|
1629
1611
|
name: target.name,
|
|
1630
1612
|
current: target.current,
|
|
1631
1613
|
latest,
|
|
1632
|
-
|
|
1614
|
+
spec: target.spec,
|
|
1615
|
+
kind: target.kind,
|
|
1616
|
+
hasUpdate: ahead && inRange,
|
|
1617
|
+
outOfRange: ahead && !inRange,
|
|
1633
1618
|
error: null,
|
|
1634
1619
|
}
|
|
1635
1620
|
} catch (error) {
|
|
@@ -1637,7 +1622,10 @@ async function checkUpdates(ctx) {
|
|
|
1637
1622
|
name: target.name,
|
|
1638
1623
|
current: target.current,
|
|
1639
1624
|
latest: null,
|
|
1625
|
+
spec: target.spec,
|
|
1626
|
+
kind: target.kind,
|
|
1640
1627
|
hasUpdate: false,
|
|
1628
|
+
outOfRange: false,
|
|
1641
1629
|
error: error instanceof Error ? error.message : String(error),
|
|
1642
1630
|
}
|
|
1643
1631
|
}
|
|
@@ -2131,6 +2119,7 @@ async function applyUpdates(ctx, names, pinned = null, job = null) {
|
|
|
2131
2119
|
if (targets.length === 0) return { ok: false, message: 'no eligible package to update' }
|
|
2132
2120
|
|
|
2133
2121
|
const planned = []
|
|
2122
|
+
const outOfRange = []
|
|
2134
2123
|
const registryBase = await configuredRegistry(ctx)
|
|
2135
2124
|
for (const target of targets) {
|
|
2136
2125
|
// 每查一个都先看有没有被取消:registry 查询 + 代理重试是弱网下最长的一段,
|
|
@@ -2145,10 +2134,29 @@ async function applyUpdates(ctx, names, pinned = null, job = null) {
|
|
|
2145
2134
|
}
|
|
2146
2135
|
}
|
|
2147
2136
|
if (compareVersions(latest, target.current) <= 0) continue
|
|
2137
|
+
// The user's own range is a decision, not an obstacle: `^1.2.3` says 2.x is
|
|
2138
|
+
// out of bounds, so installing it because the registry moved on would be
|
|
2139
|
+
// this panel overriding them. Report it instead of applying it silently.
|
|
2140
|
+
if (target.kind !== 'exact' && !satisfiesSpec(latest, target.spec)) {
|
|
2141
|
+
outOfRange.push({ name: target.name, spec: target.spec, latest })
|
|
2142
|
+
continue
|
|
2143
|
+
}
|
|
2148
2144
|
planned.push({ name: target.name, current: target.current, latest })
|
|
2149
2145
|
}
|
|
2150
2146
|
assertNotCancelled(job)
|
|
2151
|
-
if (planned.length === 0)
|
|
2147
|
+
if (planned.length === 0) {
|
|
2148
|
+
if (outOfRange.length > 0) {
|
|
2149
|
+
return {
|
|
2150
|
+
ok: true,
|
|
2151
|
+
updated: [],
|
|
2152
|
+
outOfRange,
|
|
2153
|
+
message: outOfRange
|
|
2154
|
+
.map(one => `${one.name} ${one.latest} 超出了 package.json 写明的 ${one.spec},没有安装`)
|
|
2155
|
+
.join(';'),
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return { ok: true, updated: [], message: 'already current' }
|
|
2159
|
+
}
|
|
2152
2160
|
|
|
2153
2161
|
// Everything pnpm may touch, kept for the rollback below.
|
|
2154
2162
|
const backup = new Map()
|
|
@@ -2319,6 +2327,9 @@ async function applyUpdates(ctx, names, pinned = null, job = null) {
|
|
|
2319
2327
|
return {
|
|
2320
2328
|
ok: true,
|
|
2321
2329
|
updated: planned.map(item => ({ name: item.name, from: item.current, to: item.latest })),
|
|
2330
|
+
// Reported rather than swallowed: the user asked to update these, and the
|
|
2331
|
+
// honest answer is "the range you wrote excludes the new version".
|
|
2332
|
+
...(outOfRange.length > 0 ? { outOfRange } : {}),
|
|
2322
2333
|
hotSwap: swap,
|
|
2323
2334
|
restart: swap.ok !== true,
|
|
2324
2335
|
// 没有维护门时如实上报:用户有权知道这次是在会话可能正跑着的时候改了 node_modules。
|
|
@@ -2893,9 +2904,6 @@ async function pipeTo(part, response, signal, offset, deadline) {
|
|
|
2893
2904
|
stream.on('error', error => {
|
|
2894
2905
|
if (failure === null) failure = error
|
|
2895
2906
|
})
|
|
2896
|
-
const fail = error => {
|
|
2897
|
-
if (failure === null) failure = error
|
|
2898
|
-
}
|
|
2899
2907
|
|
|
2900
2908
|
let idle = null
|
|
2901
2909
|
const arm = () => {
|
|
@@ -3112,7 +3120,9 @@ async function listExtras(ctx) {
|
|
|
3112
3120
|
for (const [name, raw] of Object.entries(facts.deps)) {
|
|
3113
3121
|
if (HOST_SCOPE.test(name)) continue
|
|
3114
3122
|
const spec = typeof raw === 'string' ? raw.trim() : ''
|
|
3115
|
-
|
|
3123
|
+
// Everything the version updater understands is not an "other install kind":
|
|
3124
|
+
// ranges used to land here as `other` rows that offered no action at all.
|
|
3125
|
+
if (isVersionSpec(spec)) continue
|
|
3116
3126
|
const current = installedVersion(facts.dir, name)
|
|
3117
3127
|
if (spec.startsWith('link:')) {
|
|
3118
3128
|
rows.push({ name, kind: 'link', spec, current, note: '本地软链' })
|
package/lib/spec.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version specs, parsed and compared — the whole of what this plugin needs to
|
|
3
|
+
* understand about semver.
|
|
4
|
+
*
|
|
5
|
+
* These functions are pure and have no imports, so they can be unit-tested
|
|
6
|
+
* directly and type-checked without pulling the host into scope. Everything
|
|
7
|
+
* that decides "is there an update" goes through here, which is why they are
|
|
8
|
+
* the only part of the plugin covered by `tsc --checkJs`.
|
|
9
|
+
*
|
|
10
|
+
* The supported shapes are the ones a `package.json` in the wild actually uses:
|
|
11
|
+
* an exact pin, `^`, `~`, `>=`, `>`, `<`, `<=`, a bare or wildcard prefix
|
|
12
|
+
* (`1`, `1.x`, `1.2`, `1.2.x`), a hyphen range (`1.2.3 - 2.0.0`), and unions of
|
|
13
|
+
* those with `||`. Anything else — `link:`, `file:`, `github:`, a tarball URL,
|
|
14
|
+
* an npm alias — is `unsupported`, which is the signal to route it through the
|
|
15
|
+
* "其他安装方式" path instead of the version updater.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A registry version: 1.2.3, optionally with a prerelease suffix. */
|
|
19
|
+
export const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {'exact' | 'caret' | 'tilde' | 'xrange' | 'hyphen' | 'gt' | 'gte' | 'lt' | 'lte' | 'any' | 'union' | 'unsupported'} SpecKind
|
|
23
|
+
*
|
|
24
|
+
* @typedef {object} ParsedSpec
|
|
25
|
+
* @property {SpecKind} kind how the range is written
|
|
26
|
+
* @property {string | null} floor the lowest version the spec allows, zeros filled in
|
|
27
|
+
* @property {number} parts how many components the user actually wrote (1, 2 or 3)
|
|
28
|
+
* @property {string | null} [ceiling] inclusive or exclusive upper version (hyphen, lt, lte)
|
|
29
|
+
* @property {ParsedSpec[]} [ranges] the alternatives of a union
|
|
30
|
+
*
|
|
31
|
+
* @typedef {object} VersionParts
|
|
32
|
+
* @property {number[]} numbers always three entries
|
|
33
|
+
* @property {string[] | null} pre prerelease identifiers, or null for a release
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Split a version into its numeric core and prerelease identifiers.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} value
|
|
40
|
+
* @returns {VersionParts | null}
|
|
41
|
+
*/
|
|
42
|
+
export function versionParts(value) {
|
|
43
|
+
if (typeof value !== 'string') return null
|
|
44
|
+
const trimmed = value.trim().replace(/^v/, '')
|
|
45
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(trimmed)
|
|
46
|
+
if (match === null) return null
|
|
47
|
+
const pre = match[4] === undefined ? null : match[4].split('.')
|
|
48
|
+
return { numbers: [Number(match[1]), Number(match[2]), Number(match[3])], pre }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Semver ordering, prerelease rules included: 1.0.0-rc.1 < 1.0.0, and a
|
|
53
|
+
* numeric prerelease identifier sorts below an alphanumeric one.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} left
|
|
56
|
+
* @param {string} right
|
|
57
|
+
* @returns {number} -1, 0 or 1
|
|
58
|
+
*/
|
|
59
|
+
export function compareVersions(left, right) {
|
|
60
|
+
const a = versionParts(left)
|
|
61
|
+
const b = versionParts(right)
|
|
62
|
+
if (a === null || b === null) return 0
|
|
63
|
+
for (let index = 0; index < 3; index += 1) {
|
|
64
|
+
if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] < b.numbers[index] ? -1 : 1
|
|
65
|
+
}
|
|
66
|
+
if (a.pre === null && b.pre === null) return 0
|
|
67
|
+
if (a.pre === null) return 1
|
|
68
|
+
if (b.pre === null) return -1
|
|
69
|
+
const length = Math.max(a.pre.length, b.pre.length)
|
|
70
|
+
for (let index = 0; index < length; index += 1) {
|
|
71
|
+
const x = a.pre[index]
|
|
72
|
+
const y = b.pre[index]
|
|
73
|
+
if (x === undefined) return -1
|
|
74
|
+
if (y === undefined) return 1
|
|
75
|
+
const xNumeric = /^\d+$/.test(x)
|
|
76
|
+
const yNumeric = /^\d+$/.test(y)
|
|
77
|
+
if (xNumeric && yNumeric) {
|
|
78
|
+
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1
|
|
82
|
+
if (x !== y) return x < y ? -1 : 1
|
|
83
|
+
}
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Expand a partially written version to three components.
|
|
89
|
+
*
|
|
90
|
+
* `1` counts as one component, `1.2` as two — the count is what tells a caret
|
|
91
|
+
* or tilde range how wide it may go.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} value `1`, `1.2` or `1.2.3`, with an optional prerelease
|
|
94
|
+
* @returns {{floor: string | null, parts: number}}
|
|
95
|
+
*/
|
|
96
|
+
function expand(value) {
|
|
97
|
+
const trimmed = value.trim().replace(/^v/, '')
|
|
98
|
+
const core = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(-[0-9A-Za-z.-]+)?$/.exec(trimmed)
|
|
99
|
+
if (core === null) return { floor: null, parts: 0 }
|
|
100
|
+
const parts = core[3] !== undefined ? 3 : core[2] !== undefined ? 2 : 1
|
|
101
|
+
const floor = `${Number(core[1])}.${core[2] === undefined ? 0 : Number(core[2])}.${core[3] === undefined ? 0 : Number(core[3])}${core[4] === undefined ? '' : core[4]}`
|
|
102
|
+
return { floor, parts }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const UNSUPPORTED = /** @type {const} */ ({ kind: 'unsupported', floor: null, parts: 0 })
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Classify a dependency spec.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} spec the raw value from package.json
|
|
111
|
+
* @returns {ParsedSpec}
|
|
112
|
+
*/
|
|
113
|
+
export function parseSpec(spec) {
|
|
114
|
+
const raw = typeof spec === 'string' ? spec.trim() : ''
|
|
115
|
+
if (raw === '') return { kind: 'any', floor: null, parts: 0 }
|
|
116
|
+
|
|
117
|
+
// A union is only a union at the top level: `^1 || ^2` is two full specs.
|
|
118
|
+
if (raw.includes('||')) {
|
|
119
|
+
const ranges = raw
|
|
120
|
+
.split('||')
|
|
121
|
+
.map((part) => parseSpec(part))
|
|
122
|
+
.filter((part) => part.kind !== 'unsupported' && part.kind !== 'any')
|
|
123
|
+
if (ranges.length === 0) return { kind: 'any', floor: null, parts: 0 }
|
|
124
|
+
if (ranges.length === 1) return ranges[0]
|
|
125
|
+
return { kind: 'union', floor: null, parts: 0, ranges }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// `1.2.3 - 2.0.0`: hyphen ranges need the spaces, otherwise the dash is a
|
|
129
|
+
// prerelease separator (`1.2.3-rc.1`).
|
|
130
|
+
const hyphen = /^(\d[\w.-]*)\s+-\s+(\d[\w.-]*)$/.exec(raw)
|
|
131
|
+
if (hyphen !== null) {
|
|
132
|
+
const from = expand(hyphen[1])
|
|
133
|
+
const to = expand(hyphen[2])
|
|
134
|
+
if (from.floor === null || to.floor === null) return UNSUPPORTED
|
|
135
|
+
return { kind: 'hyphen', floor: from.floor, parts: from.parts, ceiling: to.floor }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (raw === '*' || raw === 'x' || raw === 'X' || raw === 'latest') {
|
|
139
|
+
return { kind: 'any', floor: null, parts: 0 }
|
|
140
|
+
}
|
|
141
|
+
if (EXACT_VERSION.test(raw)) return { kind: 'exact', floor: raw, parts: 3 }
|
|
142
|
+
|
|
143
|
+
const operators = [
|
|
144
|
+
['^', 'caret'],
|
|
145
|
+
['~', 'tilde'],
|
|
146
|
+
['>=', 'gte'],
|
|
147
|
+
['>', 'gt'],
|
|
148
|
+
['<=', 'lte'],
|
|
149
|
+
['<', 'lt'],
|
|
150
|
+
]
|
|
151
|
+
for (const [prefix, kind] of operators) {
|
|
152
|
+
if (!raw.startsWith(prefix)) continue
|
|
153
|
+
const expanded = expand(raw.slice(prefix.length))
|
|
154
|
+
if (expanded.floor === null) return UNSUPPORTED
|
|
155
|
+
if (kind === 'lt' || kind === 'lte') {
|
|
156
|
+
// `<=1.2.3` and `<1.2.3` have no floor; the version itself is the bound.
|
|
157
|
+
return { kind: /** @type {SpecKind} */ (kind), floor: null, parts: expanded.parts, ceiling: expanded.floor }
|
|
158
|
+
}
|
|
159
|
+
return { kind: /** @type {SpecKind} */ (kind), floor: expanded.floor, parts: expanded.parts }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A bare or wildcard prefix: `1`, `1.x`, `1.2`, `1.2.x`, `1.2.*`.
|
|
163
|
+
const wildcard = /^(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/.exec(raw)
|
|
164
|
+
if (wildcard !== null) {
|
|
165
|
+
const major = Number(wildcard[1])
|
|
166
|
+
const minorToken = wildcard[2]
|
|
167
|
+
const patchToken = wildcard[3]
|
|
168
|
+
const minor = minorToken === undefined || !/^\d+$/.test(minorToken) ? null : Number(minorToken)
|
|
169
|
+
const patch = patchToken === undefined || !/^\d+$/.test(patchToken) ? null : Number(patchToken)
|
|
170
|
+
const parts = patch !== null ? 3 : minor !== null ? 2 : 1
|
|
171
|
+
const floor = `${major}.${minor === null ? 0 : minor}.${patch === null ? 0 : patch}`
|
|
172
|
+
return { kind: 'xrange', floor, parts }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return UNSUPPORTED
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The first version outside a caret, tilde or wildcard range.
|
|
180
|
+
*
|
|
181
|
+
* @param {ParsedSpec} parsed
|
|
182
|
+
* @returns {number[] | null}
|
|
183
|
+
*/
|
|
184
|
+
function ceilingOf(parsed) {
|
|
185
|
+
if (parsed.floor === null) return null
|
|
186
|
+
const parts = versionParts(parsed.floor)
|
|
187
|
+
if (parts === null) return null
|
|
188
|
+
const [major, minor, patch] = parts.numbers
|
|
189
|
+
if (parsed.kind === 'caret') {
|
|
190
|
+
// ^0.2.3 means "0.2.x and up", not "0.x" — the zero-major rule.
|
|
191
|
+
if (major > 0) return [major + 1, 0, 0]
|
|
192
|
+
if (minor > 0) return [0, minor + 1, 0]
|
|
193
|
+
return [0, 0, patch + 1]
|
|
194
|
+
}
|
|
195
|
+
if (parsed.kind === 'tilde' || parsed.kind === 'xrange') {
|
|
196
|
+
// ~1.2.3 and ~1.2 both stay inside 1.2; only a bare ~1 (or `1.x`) widens
|
|
197
|
+
// to 2.0.0. `1.2` behaves like `~1.2`, which is what npm does.
|
|
198
|
+
if (parsed.parts === 1) return [major + 1, 0, 0]
|
|
199
|
+
if (parsed.parts === 2) return [major, minor + 1, 0]
|
|
200
|
+
return [major, minor + 1, 0]
|
|
201
|
+
}
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Is `version` below `ceiling` (exclusive)?
|
|
207
|
+
*
|
|
208
|
+
* @param {VersionParts} target
|
|
209
|
+
* @param {number[]} ceiling
|
|
210
|
+
* @returns {boolean}
|
|
211
|
+
*/
|
|
212
|
+
function below(target, ceiling) {
|
|
213
|
+
for (let index = 0; index < 3; index += 1) {
|
|
214
|
+
if (target.numbers[index] !== ceiling[index]) return target.numbers[index] < ceiling[index]
|
|
215
|
+
}
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The shared decision, on an already-parsed spec.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} version
|
|
223
|
+
* @param {ParsedSpec} parsed
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
function satisfiesParsed(version, parsed) {
|
|
227
|
+
if (parsed.kind === 'any') return true
|
|
228
|
+
if (parsed.kind === 'unsupported') return false
|
|
229
|
+
if (parsed.kind === 'union') {
|
|
230
|
+
return (parsed.ranges ?? []).some((one) => satisfiesParsed(version, one))
|
|
231
|
+
}
|
|
232
|
+
if (parsed.kind === 'exact') return parsed.floor !== null && compareVersions(version, parsed.floor) === 0
|
|
233
|
+
|
|
234
|
+
const target = versionParts(version)
|
|
235
|
+
if (target === null) return false
|
|
236
|
+
|
|
237
|
+
// A prerelease only passes when the spec itself asked for one. Without this,
|
|
238
|
+
// `^1.2.3` would quietly accept `2.0.0-rc.1` just because it sorts above the
|
|
239
|
+
// floor of a different range.
|
|
240
|
+
const floorParts = parsed.floor === null ? null : versionParts(parsed.floor)
|
|
241
|
+
const ceilingParts = parsed.ceiling === undefined || parsed.ceiling === null ? null : versionParts(parsed.ceiling)
|
|
242
|
+
const specHasPrerelease = (floorParts !== null && floorParts.pre !== null) || (ceilingParts !== null && ceilingParts.pre !== null)
|
|
243
|
+
if (target.pre !== null && !specHasPrerelease) return false
|
|
244
|
+
|
|
245
|
+
if (parsed.kind === 'lt' || parsed.kind === 'lte') {
|
|
246
|
+
if (ceilingParts === null) return false
|
|
247
|
+
const order = compareVersions(version, parsed.ceiling ?? '')
|
|
248
|
+
return parsed.kind === 'lte' ? order <= 0 : order < 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (parsed.kind === 'hyphen') {
|
|
252
|
+
if (floorParts === null || ceilingParts === null) return false
|
|
253
|
+
return compareVersions(version, parsed.floor ?? '') >= 0 && compareVersions(version, parsed.ceiling ?? '') <= 0
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (floorParts === null) return false
|
|
257
|
+
const order = compareVersions(version, parsed.floor ?? '')
|
|
258
|
+
if (parsed.kind === 'gte') return order >= 0
|
|
259
|
+
if (parsed.kind === 'gt') return order > 0
|
|
260
|
+
if (order < 0) return false
|
|
261
|
+
const ceiling = ceilingOf(parsed)
|
|
262
|
+
if (ceiling === null) return false
|
|
263
|
+
return below(target, ceiling)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Is `version` allowed by `spec`?
|
|
268
|
+
*
|
|
269
|
+
* @param {string} version
|
|
270
|
+
* @param {string} spec the raw spec, parsed here
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
export function satisfiesSpec(version, spec) {
|
|
274
|
+
return satisfiesParsed(version, parseSpec(spec))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A spec this plugin's version updater may rewrite.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} spec
|
|
281
|
+
* @returns {boolean}
|
|
282
|
+
*/
|
|
283
|
+
export function isVersionSpec(spec) {
|
|
284
|
+
return parseSpec(spec).kind !== 'unsupported'
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-rack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH 插件管理:所有插件一屏看完,一个开关直接开关,更新完自动热替换不用重启。/ Plugin rack for DSH: every plugin in one list with a power switch, an auto/manual flag that scopes the one-click bulk actions, and hot-swapped updates that need no restart.",
|
|
6
6
|
"author": "Jaside (LZM-P)",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/LZM-P/dsh-plugin-rack"
|
|
10
|
+
"url": "git+https://github.com/LZM-P/dsh-plugin-rack.git"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://github.com/LZM-P/dsh-plugin-rack#readme",
|
|
13
13
|
"bugs": "https://github.com/LZM-P/dsh-plugin-rack/issues",
|
|
@@ -35,16 +35,20 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"index.js",
|
|
37
37
|
"client.js",
|
|
38
|
+
"lib/*.js",
|
|
38
39
|
"cordis.patch.yml",
|
|
39
40
|
"locale/*.json",
|
|
40
41
|
"icon.svg",
|
|
41
42
|
"LICENSE",
|
|
42
|
-
"README.md"
|
|
43
|
-
"test/*.mjs"
|
|
43
|
+
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"check": "node test/self-check.mjs && node test/behaviour.mjs",
|
|
47
|
-
"test": "node test/self-check.mjs && node test/behaviour.mjs && node test/job-background.mjs && node test/download.mjs && node test/pnpm-command.mjs"
|
|
46
|
+
"check": "node scripts/build-client.mjs --check && node test/self-check.mjs && node test/behaviour.mjs && node test/spec.mjs",
|
|
47
|
+
"test": "node scripts/build-client.mjs --check && node test/self-check.mjs && node test/behaviour.mjs && node test/spec.mjs && node test/client.mjs && node test/job-background.mjs && node test/download.mjs && node test/pnpm-command.mjs",
|
|
48
|
+
"lint": "eslint .",
|
|
49
|
+
"types": "tsc -p tsconfig.json",
|
|
50
|
+
"publish": "node scripts/publish.mjs",
|
|
51
|
+
"build": "node scripts/build-client.mjs"
|
|
48
52
|
},
|
|
49
53
|
"dsh": {
|
|
50
54
|
"manifestVersion": 1,
|
|
@@ -72,5 +76,9 @@
|
|
|
72
76
|
"0.2.2-rc.3": "compatible"
|
|
73
77
|
}
|
|
74
78
|
}
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"eslint": "^9.15.0",
|
|
82
|
+
"typescript": "^5.6.3"
|
|
75
83
|
}
|
|
76
84
|
}
|