fg-devkit 1.28.0 → 1.28.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/configs/dependency-policy.vue-workspace.json +7 -7
- package/package.json +4 -4
- package/scaffold/vue/package.json +4 -4
- package/scripts/audit-override-commit.mjs +12 -3
- package/scripts/lib/audit-fix-plan.mjs +132 -12
- package/scripts/lib/audit-fix-plan.test.mjs +109 -4
- package/scripts/lib/override-changes.mjs +17 -1
- package/scripts/lib/override-changes.test.mjs +27 -0
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@lexical/rich-text": "0.53.0",
|
|
19
19
|
"@piwikpro/vue-piwik-pro": "2.0.0",
|
|
20
20
|
"@sentry/vue": "11.10.0",
|
|
21
|
-
"@tanstack/vue-query": "5.105.
|
|
21
|
+
"@tanstack/vue-query": "5.105.1",
|
|
22
22
|
"@unhead/vue": "3.4.3",
|
|
23
23
|
"@vuelidate/core": "2.0.3",
|
|
24
24
|
"@vuelidate/validators": "2.0.4",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"vega-embed": "7.3.0",
|
|
45
45
|
"vega-lite": "6.5.0",
|
|
46
46
|
"vue-chartjs": "5.3.4",
|
|
47
|
-
"vue-quilly": "1.
|
|
47
|
+
"vue-quilly": "1.2.0",
|
|
48
48
|
"vue3-touch-keyboard": "1.3.9",
|
|
49
49
|
"vuex": "4.1.0"
|
|
50
50
|
},
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"@storybook/addon-vitest": "10.6.5",
|
|
58
58
|
"@storybook/vue3": "10.6.5",
|
|
59
59
|
"@storybook/vue3-vite": "10.6.5",
|
|
60
|
-
"@tanstack/vue-query-devtools": "6.3.
|
|
60
|
+
"@tanstack/vue-query-devtools": "6.3.7",
|
|
61
61
|
"@types/lodash-es": "4.17.12",
|
|
62
62
|
"@types/node": "26.6.6",
|
|
63
63
|
"@vitejs/plugin-vue": "6.0.10",
|
|
64
64
|
"@vitest/browser": "5.0.5",
|
|
65
65
|
"@vitest/browser-playwright": "5.0.5",
|
|
66
66
|
"@vue/compiler-dom": "3.5.44",
|
|
67
|
-
"@vue/language-core": "3.3.
|
|
67
|
+
"@vue/language-core": "3.3.14",
|
|
68
68
|
"@vue/test-utils": "2.5.1",
|
|
69
|
-
"fg-devkit": "1.
|
|
69
|
+
"fg-devkit": "1.28.1",
|
|
70
70
|
"happy-dom": "21.0.4",
|
|
71
71
|
"jsdom": "30.1.3",
|
|
72
72
|
"randexp": "0.6.1",
|
|
73
|
-
"sass": "1.107.
|
|
73
|
+
"sass": "1.107.1",
|
|
74
74
|
"storybook": "10.6.5",
|
|
75
75
|
"vega": "6.4.0",
|
|
76
76
|
"vite": "8.3.5",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"playwright": "1.64.1",
|
|
88
88
|
"tailwindcss": "4.3.3",
|
|
89
89
|
"vue": "3.5.44",
|
|
90
|
-
"vue-i18n": "11.4.
|
|
90
|
+
"vue-i18n": "11.4.17",
|
|
91
91
|
"vue-router": "5.4.0"
|
|
92
92
|
},
|
|
93
93
|
"overrides": {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fg-devkit",
|
|
3
|
-
"version": "1.28.
|
|
3
|
+
"version": "1.28.2",
|
|
4
4
|
"description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"@typescript-eslint/parser": "8.72.1",
|
|
57
57
|
"@vitest/eslint-plugin": "1.6.28",
|
|
58
58
|
"@vue/eslint-config-prettier": "10.2.0",
|
|
59
|
-
"eslint": "10.
|
|
59
|
+
"eslint": "10.14.0",
|
|
60
60
|
"eslint-plugin-prettier": "5.5.7",
|
|
61
61
|
"eslint-plugin-vue": "10.12.0",
|
|
62
62
|
"eslint-plugin-vuejs-accessibility": "2.6.3",
|
|
63
|
-
"fallow": "3.
|
|
63
|
+
"fallow": "3.38.1",
|
|
64
64
|
"git-cliff": "2.14.2",
|
|
65
65
|
"lefthook": "2.2.2",
|
|
66
66
|
"npm-check-updates": "23.1.1",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"tsx": "4.23.15",
|
|
69
69
|
"typescript": "5.9.3",
|
|
70
70
|
"vue-eslint-parser": "10.4.1",
|
|
71
|
-
"vue-tsc": "3.3.
|
|
71
|
+
"vue-tsc": "3.3.14"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"fgd-ui": "pnpm --dir electron/maintainer-ui start",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@tailwindcss/vite": "4.3.3",
|
|
19
|
-
"@tanstack/vue-query": "5.105.
|
|
19
|
+
"@tanstack/vue-query": "5.105.1",
|
|
20
20
|
"axios": "1.21.0",
|
|
21
21
|
"content-disposition-header": "0.6.0",
|
|
22
22
|
"date-fns": "4.4.0",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"pinia": "4.1.0",
|
|
26
26
|
"qrcode.vue": "3.11.0",
|
|
27
27
|
"vue": "3.5.44",
|
|
28
|
-
"vue-i18n": "11.4.
|
|
28
|
+
"vue-i18n": "11.4.17",
|
|
29
29
|
"vue-router": "5.4.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@intlify/unplugin-vue-i18n": "11.2.8",
|
|
33
|
-
"@tanstack/vue-query-devtools": "6.3.
|
|
33
|
+
"@tanstack/vue-query-devtools": "6.3.7",
|
|
34
34
|
"@types/node": "26.6.6",
|
|
35
|
-
"fg-devkit": "1.28.
|
|
35
|
+
"fg-devkit": "1.28.2",
|
|
36
36
|
"@vitejs/plugin-vue": "6.0.10",
|
|
37
37
|
"tailwindcss": "4.3.3",
|
|
38
38
|
"tsx": "4.23.15",
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
isReleaseAgeExcluded,
|
|
45
45
|
isSameReleaseLine,
|
|
46
46
|
normalizeAdvisories,
|
|
47
|
+
overrideKey,
|
|
47
48
|
parseAuditJson,
|
|
48
49
|
parseReleaseAgeExcludeEntry,
|
|
49
50
|
planOverrides,
|
|
@@ -294,7 +295,7 @@ async function chooseForcedFixes(plan) {
|
|
|
294
295
|
for (const item of plan.crossMajor) {
|
|
295
296
|
const message = `${item.name} ${item.from} → ${item.to} (new major, ${ghsaList(item)}). Override?`
|
|
296
297
|
if (await decide(message, 'allow-major')) {
|
|
297
|
-
overrides.push({ ...item, key:
|
|
298
|
+
overrides.push({ ...item, key: overrideKey(item.name, item.from, item.to), value: item.to, forced: true })
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
|
|
@@ -449,7 +450,7 @@ function verify() {
|
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
|
|
452
|
-
function printSummary({ fixedByRefresh, plan, forced, heldBack, applied, remaining, suppressions, excludeChanges }) {
|
|
453
|
+
function printSummary({ fixedByRefresh, plan, forced, heldBack, applied, pruned, remaining, suppressions, excludeChanges }) {
|
|
453
454
|
const suppressed = new Set(suppressions.covered.map((advisory) => advisory.ghsa))
|
|
454
455
|
const unique = (items) => [...new Set(items)]
|
|
455
456
|
const lines = []
|
|
@@ -471,6 +472,10 @@ function printSummary({ fixedByRefresh, plan, forced, heldBack, applied, remaini
|
|
|
471
472
|
.filter((item) => item.forced)
|
|
472
473
|
.map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')}${youngNote(item)})`),
|
|
473
474
|
)
|
|
475
|
+
section(
|
|
476
|
+
'Override had no effect and was pruned (no parent range matched it, check pnpm-workspace.yaml by hand):',
|
|
477
|
+
pruned.map((item) => `${item.key} -> ${item.value} (${item.ghsas.join(', ')})`),
|
|
478
|
+
)
|
|
474
479
|
section(
|
|
475
480
|
'Bumped in package.json:',
|
|
476
481
|
forced.bumps.map(
|
|
@@ -542,6 +547,7 @@ async function main() {
|
|
|
542
547
|
let forced = { overrides: [], bumps: [] }
|
|
543
548
|
let heldBack = []
|
|
544
549
|
let allowed = []
|
|
550
|
+
let pruned = []
|
|
545
551
|
let remaining = []
|
|
546
552
|
let afterBumps
|
|
547
553
|
|
|
@@ -581,6 +587,9 @@ async function main() {
|
|
|
581
587
|
|
|
582
588
|
// 4. Prune overrides.
|
|
583
589
|
pruneOverrides()
|
|
590
|
+
const overridesAfterPrune = collectOverrides(cwd)
|
|
591
|
+
pruned = allowed.filter((item) => !(item.key in overridesAfterPrune))
|
|
592
|
+
allowed = allowed.filter((item) => item.key in overridesAfterPrune)
|
|
584
593
|
remaining = allowed.length > 0 || forced.bumps.length > 0 ? audit() : afterRefresh
|
|
585
594
|
}
|
|
586
595
|
const afterOverrides = snapshot()
|
|
@@ -588,7 +597,7 @@ async function main() {
|
|
|
588
597
|
// 5. Release-age excludes, 6. suppressions, 7. verify.
|
|
589
598
|
const excludeChanges = pruneReleaseAgeExclude()
|
|
590
599
|
const suppressions = syncSuppressions(remaining)
|
|
591
|
-
printSummary({ fixedByRefresh, plan, forced, heldBack, applied: allowed, remaining, suppressions, excludeChanges })
|
|
600
|
+
printSummary({ fixedByRefresh, plan, forced, heldBack, applied: allowed, pruned, remaining, suppressions, excludeChanges })
|
|
592
601
|
|
|
593
602
|
const final = snapshot()
|
|
594
603
|
const nothingChanged = !suppressionsWritten && TRACKED_FILES.every((file) => final[file] === original[file])
|
|
@@ -71,6 +71,38 @@ export function isSameReleaseLine(from, to) {
|
|
|
71
71
|
return a.patch === b.patch
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** First version of the release line `version` belongs to: `1.0.0` for 1.4.2, `0.12.0` for 0.12.4. */
|
|
75
|
+
export function releaseLineFloor(version) {
|
|
76
|
+
const parsed = parseVersion(version)
|
|
77
|
+
if (!parsed) return null
|
|
78
|
+
if (parsed.major > 0) return `${parsed.major}.0.0`
|
|
79
|
+
if (parsed.minor > 0) return `0.${parsed.minor}.0`
|
|
80
|
+
return `0.0.${parsed.patch}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** First version after the release line `version` belongs to: `2.0.0` for 1.4.2, `0.13.0` for 0.12.4. */
|
|
84
|
+
export function releaseLineCeiling(version) {
|
|
85
|
+
const parsed = parseVersion(version)
|
|
86
|
+
if (!parsed) return null
|
|
87
|
+
if (parsed.major > 0) return `${parsed.major + 1}.0.0`
|
|
88
|
+
if (parsed.minor > 0) return `0.${parsed.minor + 1}.0`
|
|
89
|
+
return `0.0.${parsed.patch + 1}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Override key for moving `from` up to `to`. pnpm applies a key when its range overlaps the
|
|
94
|
+
* range a parent requests, not the installed version, so the lower bound is the release line
|
|
95
|
+
* floor of `from`: a version lifted by an earlier override can sit outside the requested range.
|
|
96
|
+
* A prerelease keeps its own version as the lower bound, since `>=1.0.0` does not match
|
|
97
|
+
* `1.0.0-beta.1`. Across a major the key stops at the end of `from`'s release line so it
|
|
98
|
+
* never catches parents that already ask for the new major.
|
|
99
|
+
*/
|
|
100
|
+
export function overrideKey(name, from, to) {
|
|
101
|
+
const low = parseVersion(from)?.prerelease.length ? from : releaseLineFloor(from)
|
|
102
|
+
const high = isSameReleaseLine(from, to) ? to : releaseLineCeiling(from)
|
|
103
|
+
return `${name}@>=${low} <${high}`
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
/**
|
|
75
107
|
* Lowest patched version strictly above `installed`, read from an advisory's
|
|
76
108
|
* `patched_versions` (e.g. `>=1.2.2`, `>=1.1.21 <2.0.0 || >=2.1.7`).
|
|
@@ -140,8 +172,8 @@ export function normalizeAdvisories(report) {
|
|
|
140
172
|
* Decide what to do with advisories that survived the lockfile refresh.
|
|
141
173
|
*
|
|
142
174
|
* For every installed vulnerable version the target is the lowest version that fixes all
|
|
143
|
-
* advisories affecting it.
|
|
144
|
-
* keyed
|
|
175
|
+
* advisories affecting it. Installed versions in the same release line become one override,
|
|
176
|
+
* keyed from the release line floor (`name@>=1.0.0 <target`) so it matches the parent's range.
|
|
145
177
|
*
|
|
146
178
|
* @param {ReturnType<typeof normalizeAdvisories>} advisories
|
|
147
179
|
* @param {{ directDependencies?: Set<string> }} [options]
|
|
@@ -205,24 +237,25 @@ export function planOverrides(advisories, { directDependencies = new Set() } = {
|
|
|
205
237
|
.map(([installed, { target, ghsas: spanGhsas }]) => ({ low: installed, high: target, ghsas: spanGhsas }))
|
|
206
238
|
.sort((a, b) => compareVersions(a.low, b.low))
|
|
207
239
|
|
|
208
|
-
const merged =
|
|
240
|
+
const merged = new Map()
|
|
209
241
|
for (const span of spans) {
|
|
210
|
-
const
|
|
211
|
-
|
|
242
|
+
const floor = releaseLineFloor(span.low)
|
|
243
|
+
const last = merged.get(floor)
|
|
244
|
+
if (last) {
|
|
212
245
|
if (compareVersions(span.high, last.high) > 0) last.high = span.high
|
|
213
246
|
for (const ghsa of span.ghsas) last.ghsas.add(ghsa)
|
|
214
247
|
} else {
|
|
215
|
-
merged.
|
|
248
|
+
merged.set(floor, { ...span, ghsas: new Set(span.ghsas) })
|
|
216
249
|
}
|
|
217
250
|
}
|
|
218
251
|
|
|
219
|
-
for (const span of merged) {
|
|
252
|
+
for (const span of merged.values()) {
|
|
220
253
|
const entry = { name, from: span.low, to: span.high, ghsas: [...span.ghsas] }
|
|
221
254
|
if (!isSameReleaseLine(span.low, span.high)) {
|
|
222
255
|
plan.crossMajor.push(entry)
|
|
223
256
|
continue
|
|
224
257
|
}
|
|
225
|
-
plan.overrides.push({ ...entry, key:
|
|
258
|
+
plan.overrides.push({ ...entry, key: overrideKey(name, span.low, span.high), value: span.high })
|
|
226
259
|
}
|
|
227
260
|
}
|
|
228
261
|
|
|
@@ -288,9 +321,73 @@ function unquoteYaml(value) {
|
|
|
288
321
|
return text
|
|
289
322
|
}
|
|
290
323
|
|
|
324
|
+
/**
|
|
325
|
+
* `{ name, low, lowInclusive, high, highInclusive }` for keys like `name@>=1.0.0 <1.6.11`,
|
|
326
|
+
* `name@<=8.5.17`, `name@^1.2.0`, `name@~1.2.0` or `name@1.2.3`, else null. A null bound is open.
|
|
327
|
+
*/
|
|
328
|
+
function overrideKeyRange(key) {
|
|
329
|
+
const at = key.lastIndexOf('@')
|
|
330
|
+
if (at <= 0) return null
|
|
331
|
+
const range = { name: key.slice(0, at), low: null, lowInclusive: true, high: null, highInclusive: false }
|
|
332
|
+
const parts = key.slice(at + 1).trim().split(/\s+/)
|
|
333
|
+
const shorthand = parts.length === 1 && parts[0].match(/^([~^])(.+)$/)
|
|
334
|
+
if (shorthand) {
|
|
335
|
+
const version = parseVersion(shorthand[2])
|
|
336
|
+
if (!version) return null
|
|
337
|
+
range.low = shorthand[2]
|
|
338
|
+
range.high =
|
|
339
|
+
shorthand[1] === '^' ? releaseLineCeiling(shorthand[2]) : `${version.major}.${version.minor + 1}.0`
|
|
340
|
+
return range
|
|
341
|
+
}
|
|
342
|
+
for (const part of parts) {
|
|
343
|
+
const match = part.match(/^(>=|>|<=|<|=)?(.+)$/)
|
|
344
|
+
if (!match || !parseVersion(match[2])) return null
|
|
345
|
+
const operator = match[1] ?? '='
|
|
346
|
+
if (operator.startsWith('>') || operator === '=') {
|
|
347
|
+
range.low = match[2]
|
|
348
|
+
range.lowInclusive = operator !== '>'
|
|
349
|
+
}
|
|
350
|
+
if (operator.startsWith('<') || operator === '=') {
|
|
351
|
+
range.high = match[2]
|
|
352
|
+
range.highInclusive = operator !== '<'
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return range
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function startsBeforeEnd(start, end) {
|
|
359
|
+
if (start.low === null || end.high === null) return true
|
|
360
|
+
const order = compareVersions(start.low, end.high)
|
|
361
|
+
return order < 0 || (order === 0 && start.lowInclusive && end.highInclusive)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function rangesOverlap(a, b) {
|
|
365
|
+
return startsBeforeEnd(a, b) && startsBeforeEnd(b, a)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function rangeContains(outer, inner) {
|
|
369
|
+
const lowOrder = outer.low === null ? -1 : inner.low === null ? 1 : compareVersions(outer.low, inner.low)
|
|
370
|
+
const highOrder = outer.high === null ? -1 : inner.high === null ? 1 : compareVersions(inner.high, outer.high)
|
|
371
|
+
const lowOk = lowOrder < 0 || (lowOrder === 0 && (outer.lowInclusive || !inner.lowInclusive))
|
|
372
|
+
const highOk = highOrder < 0 || (highOrder === 0 && (outer.highInclusive || !inner.highInclusive))
|
|
373
|
+
return lowOk && highOk
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** How an existing key relates to `entry`: 'replaced', 'overlaps' (same package) or null. */
|
|
377
|
+
function relationTo(key, value, entry) {
|
|
378
|
+
const existing = overrideKeyRange(key)
|
|
379
|
+
const next = overrideKeyRange(entry.key)
|
|
380
|
+
if (!existing || !next || existing.name !== next.name || key === entry.key) return null
|
|
381
|
+
if (!rangesOverlap(existing, next)) return null
|
|
382
|
+
const lowerTarget = parseVersion(value) && compareVersions(value, entry.value) <= 0
|
|
383
|
+
return lowerTarget && rangeContains(next, existing) ? 'replaced' : 'overlaps'
|
|
384
|
+
}
|
|
385
|
+
|
|
291
386
|
/**
|
|
292
387
|
* Add or replace entries in the top-level `overrides:` map of pnpm-workspace.yaml,
|
|
293
|
-
* keeping every other line untouched. Creates the section when missing.
|
|
388
|
+
* keeping every other line untouched. Creates the section when missing. pnpm uses the first
|
|
389
|
+
* matching key, so an older key for the same package that lies inside a new entry's range is
|
|
390
|
+
* removed, and a new entry that overlaps a key it cannot replace goes in before that key.
|
|
294
391
|
*
|
|
295
392
|
* @param {string} text
|
|
296
393
|
* @param {{ key: string, value: string }[]} entries
|
|
@@ -314,17 +411,40 @@ export function upsertWorkspaceOverrides(text, entries) {
|
|
|
314
411
|
while (lastContent > start && lines[lastContent] === '') lastContent -= 1
|
|
315
412
|
|
|
316
413
|
const pending = new Map(entries.map((entry) => [entry.key, entry]))
|
|
414
|
+
const removed = new Set()
|
|
415
|
+
const insertBefore = new Map()
|
|
317
416
|
for (let i = start + 1; i <= lastContent; i += 1) {
|
|
318
|
-
const match = lines[i].match(/^(\s+)((?:'(?:[^']|'')*'|"[^"]*"|[^:#]+))
|
|
417
|
+
const match = lines[i].match(/^(\s+)((?:'(?:[^']|'')*'|"[^"]*"|[^:#]+)):\s*(.*?)\s*(?:#.*)?$/)
|
|
319
418
|
if (!match) continue
|
|
320
419
|
const key = unquoteYaml(match[2])
|
|
321
420
|
if (pending.has(key)) {
|
|
322
421
|
lines[i] = rendered(pending.get(key))
|
|
323
422
|
pending.delete(key)
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
const value = unquoteYaml(match[3])
|
|
426
|
+
if (entries.some((entry) => relationTo(key, value, entry) === 'replaced')) {
|
|
427
|
+
removed.add(i)
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
if (pending.has(entry.key) && relationTo(key, value, entry) === 'overlaps' && !insertBefore.has(entry.key)) {
|
|
432
|
+
insertBefore.set(entry.key, i)
|
|
433
|
+
}
|
|
324
434
|
}
|
|
325
435
|
}
|
|
326
|
-
|
|
327
|
-
|
|
436
|
+
const result = []
|
|
437
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
438
|
+
for (const [key, index] of insertBefore) {
|
|
439
|
+
if (index === i && pending.has(key)) {
|
|
440
|
+
result.push(rendered(pending.get(key)))
|
|
441
|
+
pending.delete(key)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!removed.has(i)) result.push(lines[i])
|
|
445
|
+
if (i === lastContent) result.push(...[...pending.values()].map(rendered))
|
|
446
|
+
}
|
|
447
|
+
return result.join('\n')
|
|
328
448
|
}
|
|
329
449
|
|
|
330
450
|
// ---------------------------------------------------------------------------
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
isSameReleaseLine,
|
|
14
14
|
lowestPatchedVersion,
|
|
15
15
|
normalizeAdvisories,
|
|
16
|
+
overrideKey,
|
|
16
17
|
planOverrides,
|
|
17
18
|
planReleaseAgeExcludePrune,
|
|
19
|
+
releaseLineFloor,
|
|
18
20
|
readWorkspaceList,
|
|
19
21
|
rewriteWorkspaceList,
|
|
20
22
|
upsertWorkspaceOverrides,
|
|
@@ -108,23 +110,44 @@ test('planOverrides picks the lowest version that fixes every advisory, per rele
|
|
|
108
110
|
assert.deepEqual(
|
|
109
111
|
plan.overrides.map(({ key, value }) => ({ key, value })),
|
|
110
112
|
[
|
|
111
|
-
{ key: 'brace-expansion@>=1.
|
|
112
|
-
{ key: 'brace-expansion@>=2.
|
|
113
|
+
{ key: 'brace-expansion@>=1.0.0 <1.1.21', value: '1.1.21' },
|
|
114
|
+
{ key: 'brace-expansion@>=2.0.0 <2.1.7', value: '2.1.7' },
|
|
113
115
|
],
|
|
114
116
|
)
|
|
115
117
|
assert.deepEqual(plan.overrides[0].ghsas.sort(), ['GHSA-1', 'GHSA-2'])
|
|
116
118
|
})
|
|
117
119
|
|
|
118
|
-
test('planOverrides merges
|
|
120
|
+
test('planOverrides merges installed versions of the same release line', () => {
|
|
119
121
|
const plan = planOverrides([
|
|
120
122
|
advisory({ name: 'x', ghsa: 'GHSA-1', vulnerable: '<1.4.0', patched: '>=1.4.0', versions: ['1.2.0', '1.3.0'] }),
|
|
121
123
|
])
|
|
122
124
|
assert.deepEqual(
|
|
123
125
|
plan.overrides.map(({ key, value }) => ({ key, value })),
|
|
124
|
-
[{ key: 'x@>=1.
|
|
126
|
+
[{ key: 'x@>=1.0.0 <1.4.0', value: '1.4.0' }],
|
|
125
127
|
)
|
|
126
128
|
})
|
|
127
129
|
|
|
130
|
+
test('planOverrides keys from the release line floor so a version lifted by an earlier override still matches', () => {
|
|
131
|
+
const plan = planOverrides([
|
|
132
|
+
advisory({ name: 'seroval', ghsa: 'GHSA-1', vulnerable: '<1.6.11', patched: '>=1.6.11', versions: ['1.6.3'] }),
|
|
133
|
+
advisory({ name: 'old', ghsa: 'GHSA-2', vulnerable: '<0.12.5', patched: '>=0.12.5', versions: ['0.12.4'] }),
|
|
134
|
+
])
|
|
135
|
+
assert.deepEqual(
|
|
136
|
+
plan.overrides.map(({ key, value }) => ({ key, value })),
|
|
137
|
+
[
|
|
138
|
+
{ key: 'seroval@>=1.0.0 <1.6.11', value: '1.6.11' },
|
|
139
|
+
{ key: 'old@>=0.12.0 <0.12.5', value: '0.12.5' },
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('releaseLineFloor follows caret semantics', () => {
|
|
145
|
+
assert.equal(releaseLineFloor('1.6.3'), '1.0.0')
|
|
146
|
+
assert.equal(releaseLineFloor('0.12.4'), '0.12.0')
|
|
147
|
+
assert.equal(releaseLineFloor('0.0.3'), '0.0.3')
|
|
148
|
+
assert.equal(releaseLineFloor('nope'), null)
|
|
149
|
+
})
|
|
150
|
+
|
|
128
151
|
test('planOverrides reports instead of overriding when it would be unsafe or useless', () => {
|
|
129
152
|
const plan = planOverrides(
|
|
130
153
|
[
|
|
@@ -245,6 +268,88 @@ test('upsertWorkspaceOverrides replaces matching keys and appends new ones in pl
|
|
|
245
268
|
)
|
|
246
269
|
})
|
|
247
270
|
|
|
271
|
+
test('upsertWorkspaceOverrides removes older overlapping keys of the same package', () => {
|
|
272
|
+
const text = [
|
|
273
|
+
'overrides:',
|
|
274
|
+
" 'seroval@>=1.5.6 <1.6.3': '1.6.3'",
|
|
275
|
+
" 'seroval@>=2.0.0 <2.0.4': '2.0.4'",
|
|
276
|
+
" 'uuid@>=8.3.2 <11.1.1': '11.1.1'",
|
|
277
|
+
' lodash: 4.18.1',
|
|
278
|
+
'',
|
|
279
|
+
].join('\n')
|
|
280
|
+
const result = upsertWorkspaceOverrides(text, [{ key: 'seroval@>=1.0.0 <1.6.11', value: '1.6.11' }])
|
|
281
|
+
assert.equal(
|
|
282
|
+
result,
|
|
283
|
+
[
|
|
284
|
+
'overrides:',
|
|
285
|
+
" 'seroval@>=2.0.0 <2.0.4': '2.0.4'",
|
|
286
|
+
" 'uuid@>=8.3.2 <11.1.1': '11.1.1'",
|
|
287
|
+
' lodash: 4.18.1',
|
|
288
|
+
" 'seroval@>=1.0.0 <1.6.11': '1.6.11'",
|
|
289
|
+
'',
|
|
290
|
+
].join('\n'),
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('upsertWorkspaceOverrides puts a new key before an overlapping key it cannot replace', () => {
|
|
295
|
+
const text = "overrides:\n lodash: 4.18.1\n 'seroval@<1.6.3': '1.6.3'\n 'uuid@<11.1.1': '11.1.1'\n"
|
|
296
|
+
const result = upsertWorkspaceOverrides(text, [{ key: 'seroval@>=1.0.0 <1.6.11', value: '1.6.11' }])
|
|
297
|
+
assert.equal(
|
|
298
|
+
result,
|
|
299
|
+
"overrides:\n lodash: 4.18.1\n 'seroval@>=1.0.0 <1.6.11': '1.6.11'\n 'seroval@<1.6.3': '1.6.3'\n 'uuid@<11.1.1': '11.1.1'\n",
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('upsertWorkspaceOverrides keeps a forced cross-major key when a newer major gets its own key', () => {
|
|
304
|
+
const text = "overrides:\n 'x@>=1.0.0 <2.0.0': '2.0.3'\n"
|
|
305
|
+
const result = upsertWorkspaceOverrides(text, [{ key: 'x@>=2.0.0 <2.1.7', value: '2.1.7' }])
|
|
306
|
+
assert.equal(result, "overrides:\n 'x@>=1.0.0 <2.0.0': '2.0.3'\n 'x@>=2.0.0 <2.1.7': '2.1.7'\n")
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('upsertWorkspaceOverrides reads <=, >, caret, tilde and exact keys', () => {
|
|
310
|
+
const text = [
|
|
311
|
+
'overrides:',
|
|
312
|
+
" 'x@<=0.9.0': '0.9.1'",
|
|
313
|
+
" 'x@>1.0.0 <=1.6.2': '1.6.3'",
|
|
314
|
+
" 'x@~1.5.4': '1.5.9'",
|
|
315
|
+
" 'x@1.5.6': '1.6.3'",
|
|
316
|
+
" 'x@^1.5.0': '1.6.3'",
|
|
317
|
+
" 'x@>=1.6.11': '1.7.0'",
|
|
318
|
+
' postcss@<=8.5.17: ^8.5.18',
|
|
319
|
+
'',
|
|
320
|
+
].join('\n')
|
|
321
|
+
const result = upsertWorkspaceOverrides(text, [
|
|
322
|
+
{ key: 'x@>=1.0.0 <1.6.11', value: '1.6.11' },
|
|
323
|
+
{ key: 'postcss@>=8.0.0 <8.5.20', value: '8.5.20' },
|
|
324
|
+
])
|
|
325
|
+
assert.equal(
|
|
326
|
+
result,
|
|
327
|
+
[
|
|
328
|
+
'overrides:',
|
|
329
|
+
" 'x@<=0.9.0': '0.9.1'",
|
|
330
|
+
" 'x@>=1.0.0 <1.6.11': '1.6.11'",
|
|
331
|
+
" 'x@^1.5.0': '1.6.3'",
|
|
332
|
+
" 'x@>=1.6.11': '1.7.0'",
|
|
333
|
+
" 'postcss@>=8.0.0 <8.5.20': '8.5.20'",
|
|
334
|
+
' postcss@<=8.5.17: ^8.5.18',
|
|
335
|
+
'',
|
|
336
|
+
].join('\n'),
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('upsertWorkspaceOverrides treats touching inclusive bounds as overlapping', () => {
|
|
341
|
+
const text = "overrides:\n 'x@<=1.6.11': '1.6.11'\n 'x@<1.0.0': '0.9.9'\n"
|
|
342
|
+
const result = upsertWorkspaceOverrides(text, [{ key: 'x@>=1.6.11 <1.7.2', value: '1.7.2' }])
|
|
343
|
+
assert.equal(result, "overrides:\n 'x@>=1.6.11 <1.7.2': '1.7.2'\n 'x@<=1.6.11': '1.6.11'\n 'x@<1.0.0': '0.9.9'\n")
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('overrideKey keeps prereleases and stops forced keys at the end of the release line', () => {
|
|
347
|
+
assert.equal(overrideKey('x', '1.6.3', '1.6.11'), 'x@>=1.0.0 <1.6.11')
|
|
348
|
+
assert.equal(overrideKey('x', '0.4.0-alpha.5', '0.4.1'), 'x@>=0.4.0-alpha.5 <0.4.1')
|
|
349
|
+
assert.equal(overrideKey('x', '1.1.18', '2.0.3'), 'x@>=1.0.0 <2.0.0')
|
|
350
|
+
assert.equal(overrideKey('@scope/x', '0.12.4', '1.0.0'), '@scope/x@>=0.12.0 <0.13.0')
|
|
351
|
+
})
|
|
352
|
+
|
|
248
353
|
test('diffLockfileVersions reports changed resolutions across lockfile documents', () => {
|
|
249
354
|
const before = [
|
|
250
355
|
'---',
|
|
@@ -80,7 +80,23 @@ export function collectOverrideChanges(before, after) {
|
|
|
80
80
|
changed.push({ key, name: getOverridePackageName(key), from, to })
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
return changed
|
|
83
|
+
return mergeReplacedKeys(changed)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A key swapped for a new one of the same package (old range removed, new range added) is one change. */
|
|
87
|
+
function mergeReplacedKeys(changed) {
|
|
88
|
+
const merged = []
|
|
89
|
+
for (const item of changed) {
|
|
90
|
+
const sameName = changed.filter((other) => other.name === item.name)
|
|
91
|
+
const added = sameName.filter((other) => other.to !== undefined)
|
|
92
|
+
const removed = sameName.filter((other) => other.to === undefined)
|
|
93
|
+
if (added.length !== 1 || removed.length === 0) {
|
|
94
|
+
merged.push(item)
|
|
95
|
+
} else if (item === added[0]) {
|
|
96
|
+
merged.push({ ...item, from: item.from ?? removed[0].from })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return merged
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
export function formatOverrideValue(value) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildOverrideCommitMessage, collectOverrideChanges } from './override-changes.mjs'
|
|
5
|
+
|
|
6
|
+
test('a replaced key of the same package is one change', () => {
|
|
7
|
+
const changed = collectOverrideChanges(
|
|
8
|
+
{ 'seroval@>=1.5.6 <1.6.3': '1.6.3', 'uuid@<11.1.1': '11.1.1' },
|
|
9
|
+
{ 'seroval@>=1.0.0 <1.6.11': '1.6.11', 'uuid@<11.1.1': '11.1.1' },
|
|
10
|
+
)
|
|
11
|
+
assert.deepEqual(changed, [{ key: 'seroval@>=1.0.0 <1.6.11', name: 'seroval', from: '1.6.3', to: '1.6.11' }])
|
|
12
|
+
assert.deepEqual(buildOverrideCommitMessage(changed), { subject: 'vuln(seroval): override 1.6.11', body: '' })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('separate packages and pruned keys stay separate changes', () => {
|
|
16
|
+
const changed = collectOverrideChanges(
|
|
17
|
+
{ 'a@<1.0.1': '1.0.1', 'b@<2.0.1': '2.0.1' },
|
|
18
|
+
{ 'b@<2.0.1': '2.0.1', 'c@<3.0.1': '3.0.1' },
|
|
19
|
+
)
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
changed.map(({ name, from, to }) => ({ name, from, to })),
|
|
22
|
+
[
|
|
23
|
+
{ name: 'a', from: '1.0.1', to: undefined },
|
|
24
|
+
{ name: 'c', from: undefined, to: '3.0.1' },
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
})
|