uniweb 0.65.0 → 0.66.0
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/package.json +6 -6
- package/partials/agents.md +3 -3
- package/src/backend/foundation-bring-along.js +154 -97
- package/src/backend/site-sync.js +220 -14
- package/src/commands/clone.js +1 -1
- package/src/commands/deploy.js +9 -7
- package/src/commands/publish.js +15 -4
- package/src/commands/pull.js +219 -67
- package/src/commands/push.js +15 -5
- package/src/commands/status.js +17 -3
- package/src/commands/validate.js +16 -12
- package/src/framework-index.json +4 -4
- package/src/index.js +20 -13
- package/src/utils/conformance.js +68 -10
- package/src/utils/flag-guard.js +5 -4
- package/src/utils/git.js +48 -1
- package/src/utils/pull-written.js +5 -0
- package/src/utils/semver-precedence.js +23 -0
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-24T15:29:04.711Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.6.1",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.60.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"deps": []
|
|
97
97
|
},
|
|
98
98
|
"@uniweb/schemas": {
|
|
99
|
-
"version": "0.
|
|
99
|
+
"version": "0.5.0",
|
|
100
100
|
"path": "framework/schemas",
|
|
101
101
|
"deps": []
|
|
102
102
|
},
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"deps": []
|
|
127
127
|
},
|
|
128
128
|
"@uniweb/unipress": {
|
|
129
|
-
"version": "0.10.
|
|
129
|
+
"version": "0.10.3",
|
|
130
130
|
"path": "framework/unipress",
|
|
131
131
|
"deps": [
|
|
132
132
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -1418,7 +1418,8 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1418
1418
|
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
1419
1419
|
--dry-run Resolve the target + adapter; print summary; upload nothing
|
|
1420
1420
|
--no-save Do not record this deploy in deploy.yml
|
|
1421
|
-
--no-validate Skip the content-conformance check (
|
|
1421
|
+
--no-validate Skip the content-conformance check (a static host warns;
|
|
1422
|
+
Uniweb Cloud stops on content that does not conform)
|
|
1422
1423
|
--non-interactive Fail with usage info instead of prompting
|
|
1423
1424
|
|
|
1424
1425
|
${colors.bright}Examples:${colors.reset}
|
|
@@ -1439,9 +1440,10 @@ ${colors.bright}Usage:${colors.reset}
|
|
|
1439
1440
|
The most ergonomic command in the tool: \`uniweb login && uniweb publish\` reads
|
|
1440
1441
|
your project and makes the site live on Uniweb hosting. It resolves which site,
|
|
1441
1442
|
BRINGS THE FOUNDATION ALONG (releases the local foundation when its code
|
|
1442
|
-
changed
|
|
1443
|
-
|
|
1444
|
-
|
|
1443
|
+
changed — under the next version when its own is registered; commit the
|
|
1444
|
+
package.json it writes), syncs content, settles payment when go-live needs it
|
|
1445
|
+
(a browser handoff), and goes live. A published-registry foundation needs no
|
|
1446
|
+
release; an already-paid site opens no browser.
|
|
1445
1447
|
|
|
1446
1448
|
For a third-party host use \`uniweb deploy --host=<name>\`; to register a
|
|
1447
1449
|
FOUNDATION on its own use \`uniweb register\` (alias \`uniweb release\`).
|
|
@@ -1454,8 +1456,11 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1454
1456
|
when only the content should go live. Refused if the
|
|
1455
1457
|
foundation has never been released — there is nothing to
|
|
1456
1458
|
bind to, and the app cannot open such a site.
|
|
1459
|
+
--bump When the registry holds a newer version of the foundation
|
|
1460
|
+
than yours, release yours above it instead of stopping
|
|
1457
1461
|
--no-save Do not record this deploy in deploy.yml
|
|
1458
|
-
--no-validate Skip the content-conformance check
|
|
1462
|
+
--no-validate Skip the content-conformance check, which stops a publish
|
|
1463
|
+
whose content does not conform to its data schemas
|
|
1459
1464
|
--org @org Work in @org for this publish, instead of the workspace
|
|
1460
1465
|
--personal chosen at \`uniweb login\` (or your personal workspace). A
|
|
1461
1466
|
site this publish creates is created — owned — there, and a
|
|
@@ -1690,17 +1695,18 @@ foundation declared for that input (meta.js \`data:\`). Answers "does my
|
|
|
1690
1695
|
data match what I promised?" — distinct from \`doctor\`, which checks your
|
|
1691
1696
|
project against framework conventions.
|
|
1692
1697
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1698
|
+
Every record file is checked against the schema its folder names, whether or
|
|
1699
|
+
not a section reads it. A violation fails the run; \`push\` and \`publish\` refuse
|
|
1700
|
+
the same findings. The live render path stays tolerant. Dynamic (\`url:\`)
|
|
1701
|
+
inputs and entity references can't be resolved without a running backend, so
|
|
1702
|
+
they're reported as deferred, never silently skipped.
|
|
1697
1703
|
|
|
1698
1704
|
${colors.bright}Options:${colors.reset}
|
|
1699
|
-
--
|
|
1705
|
+
--lax Report violations without failing (exit 0)
|
|
1700
1706
|
--json Machine-readable output (for CI annotations)
|
|
1701
1707
|
--site <name> Check one site in a multi-site workspace
|
|
1702
1708
|
|
|
1703
|
-
Exit codes: 0 clean (or
|
|
1709
|
+
Exit codes: 0 clean (or --lax), 1 violations, 2 setup error.
|
|
1704
1710
|
`,
|
|
1705
1711
|
register: `
|
|
1706
1712
|
${colors.cyan}${colors.bright}uniweb register${colors.reset} ${colors.dim}— Register (release) a foundation + its data schemas with the backend registry${colors.reset}
|
|
@@ -2087,10 +2093,11 @@ ${colors.bright}Publish Options:${colors.reset}
|
|
|
2087
2093
|
--dry-run Resolve everything; release/sync/POST nothing
|
|
2088
2094
|
--yes Skip confirmations (CI); never block on a prompt
|
|
2089
2095
|
--no-release Ship content against the already-released code; release nothing
|
|
2096
|
+
--bump Release above a newer registered foundation version
|
|
2090
2097
|
--org @org Work in @org for this publish, not your login's workspace
|
|
2091
2098
|
--personal Work in your personal workspace for this publish
|
|
2092
2099
|
--no-save Do not record this deploy in deploy.yml
|
|
2093
|
-
--no-validate Skip the content-conformance check (it
|
|
2100
|
+
--no-validate Skip the content-conformance check (it stops a publish)
|
|
2094
2101
|
|
|
2095
2102
|
uniweb publish is the smart Uniweb-hosting path: it brings the site's
|
|
2096
2103
|
foundation along, syncs, and goes live. To register a foundation on its own
|
|
@@ -2104,7 +2111,7 @@ ${colors.bright}Deploy Options:${colors.reset}
|
|
|
2104
2111
|
--target <name> Pick a target from deploy.yml (default: deploy.yml's \`default:\`)
|
|
2105
2112
|
--dry-run Resolve the target + adapter; print summary; upload nothing
|
|
2106
2113
|
--no-save Do not record this deploy in deploy.yml
|
|
2107
|
-
--no-validate Skip the content-conformance check (
|
|
2114
|
+
--no-validate Skip the content-conformance check (a static host warns)
|
|
2108
2115
|
To deploy on every push instead, see \`uniweb add ci --help\`.
|
|
2109
2116
|
|
|
2110
2117
|
${colors.bright}Dev Options:${colors.reset}
|
package/src/utils/conformance.js
CHANGED
|
@@ -8,17 +8,26 @@
|
|
|
8
8
|
* it. The first symptom was a section rendering nothing on a live site, with no
|
|
9
9
|
* error anywhere in the chain.
|
|
10
10
|
*
|
|
11
|
-
* ##
|
|
11
|
+
* ## On a STATIC host it warns and never blocks — and on a backend it is a gate.
|
|
12
|
+
*
|
|
13
|
+
* ⭐ Two answers, one per destination [Diego, 2026-09-24: "The old rationale was in
|
|
14
|
+
* the context of static sites. It was never meant for pushed sites since that didn't
|
|
15
|
+
* even exist at the time."]. `push` and `publish` register the foundation first, so
|
|
16
|
+
* the backend checks the records against the very schemas checked here: a finding
|
|
17
|
+
* there is a refusal on arrival, or a value lost on the way — never a disagreement to
|
|
18
|
+
* ship through. `refuseIfContentDoesNotConform` is that gate; `deploy` to a static
|
|
19
|
+
* host keeps the warning below, whose reasoning is unchanged for it:
|
|
12
20
|
*
|
|
13
21
|
* A schema can be newer than the content that was valid when it was authored —
|
|
14
22
|
* a foundation upgrade, a `@std/*` revision — so a finding does not mean the
|
|
15
23
|
* content is wrong, it means the two disagree. A framework that refused to
|
|
16
24
|
* publish over that would make an author's site hostage to a schema release,
|
|
17
|
-
* for a defect whose real-world cost is one section rendering wrong.
|
|
18
|
-
*
|
|
25
|
+
* for a defect whose real-world cost is one section rendering wrong. `uniweb
|
|
26
|
+
* validate` is where the gate lives for a static site, and CI is where it belongs.
|
|
19
27
|
*
|
|
20
|
-
* So: silent when everything conforms, silent when it cannot
|
|
21
|
-
* block when it finds something, and **the ship proceeds
|
|
28
|
+
* So, on a static host: silent when everything conforms, silent when it cannot
|
|
29
|
+
* check, one compact block when it finds something, and **the ship proceeds
|
|
30
|
+
* either way**.
|
|
22
31
|
*
|
|
23
32
|
* ## Why it is quiet about being unable to check
|
|
24
33
|
*
|
|
@@ -41,7 +50,8 @@ const c = {
|
|
|
41
50
|
reset: '\x1b[0m',
|
|
42
51
|
bold: '\x1b[1m',
|
|
43
52
|
dim: '\x1b[2m',
|
|
44
|
-
yellow: '\x1b[33m'
|
|
53
|
+
yellow: '\x1b[33m',
|
|
54
|
+
red: '\x1b[31m'
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
/** How many findings to print before summarizing the rest. */
|
|
@@ -144,6 +154,47 @@ export async function warnIfContentDoesNotConform(siteDir, options = {}) {
|
|
|
144
154
|
return formatted.total
|
|
145
155
|
}
|
|
146
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The same check as a GATE, for `push` and `publish` — see the header for why a
|
|
159
|
+
* backend gets a gate where a static host gets a warning.
|
|
160
|
+
*
|
|
161
|
+
* Only VIOLATIONS stop it: a record the backend would refuse, or whose value it would
|
|
162
|
+
* lose. A setup error is about how data reaches a section — a rendering question, not
|
|
163
|
+
* a record — so it is still printed as a warning and the ship goes on. A checker that
|
|
164
|
+
* cannot run says nothing and stops nothing, as above. `--no-validate` skips the check
|
|
165
|
+
* and leaves the records to the backend.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} siteDir
|
|
168
|
+
* @param {object} [options] - `args`, and the caller's `error` / `warn` / `dim` printers
|
|
169
|
+
* @returns {Promise<number>} the violations that stop the ship (0 to go on)
|
|
170
|
+
*/
|
|
171
|
+
export async function refuseIfContentDoesNotConform(siteDir, options = {}) {
|
|
172
|
+
const {
|
|
173
|
+
args = [],
|
|
174
|
+
error = (m) => console.log(`${c.red}✗${c.reset} ${m}`),
|
|
175
|
+
warn = (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
176
|
+
dim = (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
177
|
+
} = options
|
|
178
|
+
|
|
179
|
+
if (args.includes('--no-validate')) return 0
|
|
180
|
+
|
|
181
|
+
let result
|
|
182
|
+
try {
|
|
183
|
+
result = await checkSiteConformance(siteDir)
|
|
184
|
+
} catch {
|
|
185
|
+
return 0
|
|
186
|
+
}
|
|
187
|
+
if (result.status !== 'checked') return 0
|
|
188
|
+
|
|
189
|
+
const violations = result.report?.violations?.length || 0
|
|
190
|
+
const formatted = formatConformanceWarning(result, siteDir, { stops: violations > 0 })
|
|
191
|
+
if (!formatted) return 0
|
|
192
|
+
|
|
193
|
+
;(violations > 0 ? error : warn)(formatted.headline)
|
|
194
|
+
for (const line of formatted.details) dim(line)
|
|
195
|
+
return violations
|
|
196
|
+
}
|
|
197
|
+
|
|
147
198
|
/**
|
|
148
199
|
* Turn a report into the lines to print, or `null` when there is nothing to say.
|
|
149
200
|
*
|
|
@@ -154,9 +205,11 @@ export async function warnIfContentDoesNotConform(siteDir, options = {}) {
|
|
|
154
205
|
*
|
|
155
206
|
* @param {{report?: object, foundation?: string}} result
|
|
156
207
|
* @param {string} siteDir
|
|
208
|
+
* @param {{ stops?: boolean }} [options] - `stops`: the ship stops on this report
|
|
209
|
+
* (`refuseIfContentDoesNotConform`), so the last line says nothing was sent
|
|
157
210
|
* @returns {{headline: string, details: string[], total: number}|null}
|
|
158
211
|
*/
|
|
159
|
-
export function formatConformanceWarning(result, siteDir = '') {
|
|
212
|
+
export function formatConformanceWarning(result, siteDir = '', { stops = false } = {}) {
|
|
160
213
|
const violations = result?.report?.violations || []
|
|
161
214
|
const setupErrors = result?.report?.setupErrors || []
|
|
162
215
|
const total = violations.length + setupErrors.length
|
|
@@ -201,9 +254,14 @@ export function formatConformanceWarning(result, siteDir = '') {
|
|
|
201
254
|
const details = lines.slice(0, MAX_SHOWN).map((l) => `• ${l}`)
|
|
202
255
|
if (lines.length > MAX_SHOWN) details.push(`…and ${lines.length - MAX_SHOWN} more`)
|
|
203
256
|
|
|
204
|
-
// Name the command that explains it, and say plainly
|
|
205
|
-
//
|
|
206
|
-
|
|
257
|
+
// Name the command that explains it, and say plainly which this is — a warning
|
|
258
|
+
// during a ship reads as a failure unless it says so, and a refusal must say what
|
|
259
|
+
// did not happen.
|
|
260
|
+
details.push(
|
|
261
|
+
stops
|
|
262
|
+
? 'Nothing was sent. Run `uniweb validate` for the full report, or pass `--no-validate` to send it anyway and let the backend decide.'
|
|
263
|
+
: 'Shipping anyway. Run `uniweb validate` for the full report.'
|
|
264
|
+
)
|
|
207
265
|
|
|
208
266
|
return { headline, details, total }
|
|
209
267
|
}
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -64,15 +64,16 @@ const VERBS = {
|
|
|
64
64
|
// flag-guard-coverage.test.js the moment push gained the import, which is
|
|
65
65
|
// exactly the hand-enumeration failure that test exists to catch.
|
|
66
66
|
'--no-verify',
|
|
67
|
-
// ship content against the already-released code, releasing nothing
|
|
68
|
-
|
|
67
|
+
// ship content against the already-released code, releasing nothing — or
|
|
68
|
+
// release above a newer registered version instead of stopping (both bring-along)
|
|
69
|
+
'--no-release', '--bump', ...VIA_DEPLOY
|
|
69
70
|
],
|
|
70
71
|
publish: [
|
|
71
72
|
'--org', '--dry-run', '--force', '--foundation',
|
|
72
73
|
'--personal',
|
|
73
74
|
// read in utils/conformance.js, backend/site-sync.js, and
|
|
74
75
|
// backend/foundation-bring-along.js — none appear in publish.js
|
|
75
|
-
'--no-validate', '--yes', '--no-verify', '--no-release', ...VIA_DEPLOY
|
|
76
|
+
'--no-validate', '--yes', '--no-verify', '--no-release', '--bump', ...VIA_DEPLOY
|
|
76
77
|
],
|
|
77
78
|
pull: [
|
|
78
79
|
'--content-only', '--dry-run', '--force', '--merge',
|
|
@@ -103,7 +104,7 @@ const VERBS = {
|
|
|
103
104
|
'--force', '--no-verify', '--no-validate', '--yes', '--org', '--personal',
|
|
104
105
|
// inert here, reachable through the bring-along module status imports for
|
|
105
106
|
// `resolveLocalFoundation` — listed per the over-approximation note above
|
|
106
|
-
'--no-release', ...VIA_DEPLOY
|
|
107
|
+
'--no-release', '--bump', ...VIA_DEPLOY
|
|
107
108
|
],
|
|
108
109
|
/**
|
|
109
110
|
* `refresh` = `git pull`, then a DELEGATED `pull --merge`.
|
package/src/utils/git.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { execFileSync } from 'node:child_process'
|
|
21
|
+
import { createHash } from 'node:crypto'
|
|
21
22
|
import { readFileSync, realpathSync } from 'node:fs'
|
|
22
23
|
import { join, relative } from 'node:path'
|
|
23
24
|
import yaml from 'js-yaml'
|
|
@@ -173,8 +174,25 @@ export function uncommittedUnder(dir, relPaths) {
|
|
|
173
174
|
* never committed has no ancestor, so there is nothing to merge against).
|
|
174
175
|
*/
|
|
175
176
|
export function showAtHead(dir, relPath) {
|
|
177
|
+
return showAt(dir, 'HEAD', relPath)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A file's content at a revision, by a path relative to `dir`.
|
|
182
|
+
*
|
|
183
|
+
* ⛔ THE `./` IS WHAT MAKES THE PATH RELATIVE TO `dir`. In `<rev>:<path>` a bare path
|
|
184
|
+
* is read from the REPOSITORY ROOT, whatever the cwd. With the repo at the project
|
|
185
|
+
* root and the site in `site/` — the default layout — `HEAD:pages/home/hero.md`
|
|
186
|
+
* named nothing, every lookup failed, and `pull --merge` "kept yours" for every file:
|
|
187
|
+
* the backend's changes never merged in, while the pull recorded them as taken
|
|
188
|
+
* (2026-09-23). Every merge test ran with the repo AT the site directory, the one
|
|
189
|
+
* layout where a bare path happens to work.
|
|
190
|
+
*
|
|
191
|
+
* @returns {Buffer|null} null when the path isn't in that revision
|
|
192
|
+
*/
|
|
193
|
+
export function showAt(dir, rev, relPath) {
|
|
176
194
|
try {
|
|
177
|
-
return execFileSync('git', ['show',
|
|
195
|
+
return execFileSync('git', ['show', `${rev}:./${relPath}`], {
|
|
178
196
|
cwd: dir,
|
|
179
197
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
180
198
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -184,6 +202,35 @@ export function showAtHead(dir, relPath) {
|
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The most recent committed version of a file whose content hashes to `sha256` —
|
|
207
|
+
* how the version a pull wrote is found again, as the ancestor of a merge.
|
|
208
|
+
*
|
|
209
|
+
* HEAD is that version only until the user commits an edit on top of it. After that
|
|
210
|
+
* HEAD is THEIR edit, and a merge against it reads their change as the starting
|
|
211
|
+
* point: the backend's version wins whole and the committed edit is reverted in the
|
|
212
|
+
* working tree. The pull recorded the hash of what it wrote (`pull-written.json`),
|
|
213
|
+
* so the version is recoverable from history by content, wherever it was committed.
|
|
214
|
+
*
|
|
215
|
+
* @param {number} [limit] - how many commits touching the file to search
|
|
216
|
+
* @returns {Buffer|null} null when no committed version matches
|
|
217
|
+
*/
|
|
218
|
+
export function findCommitted(dir, relPath, sha256, { limit = 50 } = {}) {
|
|
219
|
+
let revs
|
|
220
|
+
try {
|
|
221
|
+
revs = git(['log', `-n${limit}`, '--format=%H', '--', `./${relPath}`], dir)
|
|
222
|
+
.split('\n')
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
} catch {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
for (const rev of revs) {
|
|
228
|
+
const buf = showAt(dir, rev, relPath)
|
|
229
|
+
if (buf && createHash('sha256').update(buf).digest('hex') === sha256) return buf
|
|
230
|
+
}
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
187
234
|
/**
|
|
188
235
|
* Three-way merge, in place: `minePath` is rewritten with the merged result.
|
|
189
236
|
*
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The record of what a machine wrote into a site project — pull's own output.
|
|
3
3
|
*
|
|
4
|
+
* ⭐ And, since 2026-09-23, what a push delivered: the files of the units the backend
|
|
5
|
+
* now holds as they were sent (`recordPushedUnits`, site-sync.js). Both are "the last
|
|
6
|
+
* version this file synced at", which is what `pull --merge` needs as its ancestor —
|
|
7
|
+
* found again in the repo's history by this hash (`findCommitted`, utils/git.js).
|
|
8
|
+
*
|
|
4
9
|
* ## What it is for
|
|
5
10
|
*
|
|
6
11
|
* `uniweb pull` refuses to run when there are uncommitted changes under the files
|
|
@@ -24,6 +24,29 @@ export function parseSemver(version) {
|
|
|
24
24
|
return { major: +m[1], minor: +m[2], patch: +m[3], pre: m[4] ? m[4].split('.') : [] }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The next version above `version` on its own line — what `--bump` releases under
|
|
29
|
+
* when the registry already holds the local version with other code.
|
|
30
|
+
*
|
|
31
|
+
* A release gets its next patch (`1.4.2` → `1.4.3`); a pre-release its next
|
|
32
|
+
* pre-release (`2.0.0-beta.3` → `2.0.0-beta.4`, `1.0.0-rc` → `1.0.0-rc.0`), so a bump
|
|
33
|
+
* nobody chose never promotes a beta line to a release. Build metadata is dropped: it
|
|
34
|
+
* carries no order, so it could not make a version newer.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} version
|
|
37
|
+
* @returns {string|null} null when `version` is not SemVer
|
|
38
|
+
*/
|
|
39
|
+
export function nextVersionAbove(version) {
|
|
40
|
+
const v = parseSemver(version)
|
|
41
|
+
if (!v) return null
|
|
42
|
+
if (!v.pre.length) return `${v.major}.${v.minor}.${v.patch + 1}`
|
|
43
|
+
const pre = [...v.pre]
|
|
44
|
+
const last = pre.length - 1
|
|
45
|
+
if (/^\d+$/.test(pre[last])) pre[last] = String(BigInt(pre[last]) + 1n)
|
|
46
|
+
else pre.push('0')
|
|
47
|
+
return `${v.major}.${v.minor}.${v.patch}-${pre.join('.')}`
|
|
48
|
+
}
|
|
49
|
+
|
|
27
50
|
/**
|
|
28
51
|
* @param {string} a
|
|
29
52
|
* @param {string} b
|