fg-devkit 1.16.2 → 1.17.1
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/.cursor/rules/core-coding-standards.mdc +2 -0
- package/README.md +16 -2
- package/configs/dependency-policy.vue-workspace.json +4 -4
- package/package.json +9 -7
- package/scaffold/vue/package.json +2 -2
- package/scripts/audit-override-commit.mjs +38 -92
- package/scripts/fg-devkit.mjs +21 -5
- package/scripts/override-changes.mjs +83 -0
- package/scripts/policy-upgrade-commit.mjs +118 -31
- package/scripts/prune-overrides.mjs +340 -0
|
@@ -67,6 +67,8 @@ alwaysApply: true
|
|
|
67
67
|
- Set `staleTime` intentionally per resource; keep `refetchOnWindowFocus` off unless fresh-on-focus behavior is required.
|
|
68
68
|
- On mutation success, update cache intentionally (`setQueryData`) and/or invalidate the narrowest matching keys.
|
|
69
69
|
- Respect shared query client retry behavior and only override retries when endpoint-specific behavior is needed.
|
|
70
|
+
- TanStack Query boolean flags (`isPending`, `isSuccess`, `isError`) are typed `Ref<false> | Ref<true>`. `vue-tsc` unwraps that union in templates to `boolean`; the Vue language plugin may not.
|
|
71
|
+
- Narrow in script (`const isPending = mutation.isPending as Ref<boolean>`), then bind without `.value` (`:loading="isPending"`). Do not use `.value` in templates (fails `vue-tsc`) and do not add a wrapping `computed` only to satisfy the editor.
|
|
70
72
|
|
|
71
73
|
## Mutation cache behavior
|
|
72
74
|
|
package/README.md
CHANGED
|
@@ -139,13 +139,27 @@ Upgrade one or more package.json dependencies and auto-commit
|
|
|
139
139
|
pnpm exec fgd policy-upgrade-commit
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
Same upgrade, then prune unused workspace overrides in a second commit if any can be dropped:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pnpm exec fgd policy-upgrade-commit --prune-overrides
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Fix one or more vulnerability overrides interactively, prune unused overrides, and auto-commit
|
|
149
|
+
(`vuln(package): override version`, `vuln(package): prune override`, or `vuln(): override N packages` with a body listing each change):
|
|
144
150
|
|
|
145
151
|
```bash
|
|
146
152
|
pnpm exec fgd audit-override-commit
|
|
147
153
|
```
|
|
148
154
|
|
|
155
|
+
Report, remove, or remove and auto-commit workspace overrides that no longer change `pnpm audit`:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pnpm exec fgd prune-overrides
|
|
159
|
+
pnpm exec fgd prune-overrides --fix
|
|
160
|
+
pnpm exec fgd prune-overrides --commit
|
|
161
|
+
```
|
|
162
|
+
|
|
149
163
|
## tsconfig in consuming repos
|
|
150
164
|
|
|
151
165
|
Use `fgd sync-project-configs` (or `fgd sync-tooling-configs`) to write the local `tsconfig.json` template from `fg-devkit`.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@lexical/rich-text": "0.49.0",
|
|
18
18
|
"@lexical/link": "0.49.0",
|
|
19
19
|
"@piwikpro/vue-piwik-pro": "1.7.1",
|
|
20
|
-
"@sentry/vue": "10.
|
|
20
|
+
"@sentry/vue": "10.74.0",
|
|
21
21
|
"@tanstack/vue-query": "5.102.8",
|
|
22
22
|
"@types/leaflet": "1.9.22",
|
|
23
23
|
"@unhead/vue": "2.0.5",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"eslint-plugin-storybook": "10.6.0",
|
|
78
78
|
"globals": "15.11.0",
|
|
79
79
|
"jsdom": "26.1.0",
|
|
80
|
-
"sass": "1.104.
|
|
80
|
+
"sass": "1.104.1",
|
|
81
81
|
"storybook": "10.6.0",
|
|
82
82
|
"tsx": "4.23.13",
|
|
83
83
|
"vega": "6.4.0",
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
"vite-plugin-vue-devtools": "8.2.1",
|
|
86
86
|
"vite-plugin-vue-inspector": "7.0.0",
|
|
87
87
|
"vitest": "4.1.10",
|
|
88
|
-
"yaml": "2.9.
|
|
88
|
+
"yaml": "2.9.1"
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"managedAnywhere": {
|
|
92
92
|
"@tailwindcss/vite": "4.3.3",
|
|
93
|
-
"@vitejs/plugin-vue": "6.0.
|
|
93
|
+
"@vitejs/plugin-vue": "6.0.9",
|
|
94
94
|
"axios": "1.20.0",
|
|
95
95
|
"dotenv": "16.4.5",
|
|
96
96
|
"qrcode.vue": "3.11.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fg-devkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
"./scripts/fleet-git-discard-scoped.mjs": "./scripts/fleet-git-discard-scoped.mjs",
|
|
54
54
|
"./scripts/policy-upgrade.mjs": "./scripts/policy-upgrade.mjs",
|
|
55
55
|
"./scripts/policy-upgrade-commit.mjs": "./scripts/policy-upgrade-commit.mjs",
|
|
56
|
+
"./scripts/override-changes.mjs": "./scripts/override-changes.mjs",
|
|
56
57
|
"./scripts/audit-override-commit.mjs": "./scripts/audit-override-commit.mjs",
|
|
58
|
+
"./scripts/prune-overrides.mjs": "./scripts/prune-overrides.mjs",
|
|
57
59
|
"./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
|
|
58
60
|
"./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
|
|
59
61
|
"./scripts/sync-dependency-policy-commit.mjs": "./scripts/sync-dependency-policy-commit.mjs",
|
|
@@ -103,12 +105,12 @@
|
|
|
103
105
|
},
|
|
104
106
|
"dependencies": {
|
|
105
107
|
"@inquirer/checkbox": "4.3.2",
|
|
106
|
-
"@inquirer/confirm": "6.3.
|
|
107
|
-
"@inquirer/input": "5.1.
|
|
108
|
-
"@inquirer/select": "5.2.
|
|
108
|
+
"@inquirer/confirm": "6.3.2",
|
|
109
|
+
"@inquirer/input": "5.1.6",
|
|
110
|
+
"@inquirer/select": "5.2.5",
|
|
109
111
|
"@intlify/eslint-plugin-vue-i18n": "4.5.1",
|
|
110
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
111
|
-
"@typescript-eslint/parser": "8.
|
|
112
|
+
"@typescript-eslint/eslint-plugin": "8.70.0",
|
|
113
|
+
"@typescript-eslint/parser": "8.70.0",
|
|
112
114
|
"@vitest/eslint-plugin": "1.6.27",
|
|
113
115
|
"@vue/eslint-config-prettier": "10.2.0",
|
|
114
116
|
"@vue/eslint-config-typescript": "14.9.0",
|
|
@@ -118,7 +120,7 @@
|
|
|
118
120
|
"eslint-plugin-vue": "10.11.0",
|
|
119
121
|
"fallow": "2.101.0",
|
|
120
122
|
"git-cliff": "2.13.1",
|
|
121
|
-
"lefthook": "2.1.
|
|
123
|
+
"lefthook": "2.1.14",
|
|
122
124
|
"npm-check-updates": "22.2.7",
|
|
123
125
|
"prettier": "3.9.6",
|
|
124
126
|
"tsx": "4.23.13",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@intlify/unplugin-vue-i18n": "11.2.5",
|
|
33
33
|
"@tanstack/vue-query-devtools": "6.1.48",
|
|
34
34
|
"@types/node": "22.16.5",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"fg-devkit": "1.17.1",
|
|
36
|
+
"@vitejs/plugin-vue": "6.0.9",
|
|
37
37
|
"tailwindcss": "4.3.3",
|
|
38
38
|
"tsx": "4.23.13",
|
|
39
39
|
"vite": "7.3.5",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Fix one or more vulnerabilities via `pnpm audit --fix override -i`,
|
|
3
|
+
* Fix one or more vulnerabilities via `pnpm audit --fix override -i`, prune unused
|
|
4
|
+
* overrides, install, then auto-commit.
|
|
4
5
|
*
|
|
5
6
|
* Usage:
|
|
6
7
|
* pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
|
|
@@ -9,79 +10,17 @@
|
|
|
9
10
|
import fs from 'node:fs'
|
|
10
11
|
import path from 'node:path'
|
|
11
12
|
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import {
|
|
15
|
+
buildOverrideCommitMessage,
|
|
16
|
+
collectOverrideChanges,
|
|
17
|
+
collectOverrides,
|
|
18
|
+
formatOverrideValue,
|
|
19
|
+
} from './override-changes.mjs'
|
|
12
20
|
|
|
13
21
|
const cwd = process.cwd()
|
|
22
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
14
23
|
const packageJsonPath = path.join(cwd, 'package.json')
|
|
15
|
-
const workspaceYamlPath = path.join(cwd, 'pnpm-workspace.yaml')
|
|
16
|
-
|
|
17
|
-
/** Strip surrounding single/double quotes from YAML scalar keys/values. */
|
|
18
|
-
function unquoteYamlScalar(value) {
|
|
19
|
-
if (
|
|
20
|
-
(value.startsWith("'") && value.endsWith("'")) ||
|
|
21
|
-
(value.startsWith('"') && value.endsWith('"'))
|
|
22
|
-
) {
|
|
23
|
-
return value.slice(1, -1)
|
|
24
|
-
}
|
|
25
|
-
return value
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getOverridePackageName(overrideKey) {
|
|
29
|
-
const key = unquoteYamlScalar(overrideKey.trim())
|
|
30
|
-
const match = key.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
|
|
31
|
-
return match ? match[1] : key
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Read flat `overrides:` entries from pnpm-workspace.yaml (no YAML dependency). */
|
|
35
|
-
function readWorkspaceOverrides(yamlPath) {
|
|
36
|
-
if (!fs.existsSync(yamlPath)) return {}
|
|
37
|
-
const overrides = {}
|
|
38
|
-
let inOverrides = false
|
|
39
|
-
for (const line of fs.readFileSync(yamlPath, 'utf8').split('\n')) {
|
|
40
|
-
if (/^overrides:\s*$/.test(line)) {
|
|
41
|
-
inOverrides = true
|
|
42
|
-
continue
|
|
43
|
-
}
|
|
44
|
-
if (!inOverrides) continue
|
|
45
|
-
if (/^\S/.test(line)) break
|
|
46
|
-
const match = line.match(/^ ([^:]+):\s*(.+?)\s*$/)
|
|
47
|
-
if (match) {
|
|
48
|
-
const key = unquoteYamlScalar(match[1].trim())
|
|
49
|
-
const value = unquoteYamlScalar(match[2].trim())
|
|
50
|
-
overrides[key] = value
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return overrides
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function readJson(jsonPath) {
|
|
57
|
-
return JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function collectOverrides() {
|
|
61
|
-
const overrides = {}
|
|
62
|
-
|
|
63
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
64
|
-
const pkg = readJson(packageJsonPath)
|
|
65
|
-
Object.assign(overrides, pkg.overrides ?? {})
|
|
66
|
-
Object.assign(overrides, pkg.pnpm?.overrides ?? {})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
Object.assign(overrides, readWorkspaceOverrides(workspaceYamlPath))
|
|
70
|
-
return overrides
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function collectOverrideChanges(before, after) {
|
|
74
|
-
const changed = []
|
|
75
|
-
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
76
|
-
for (const key of keys) {
|
|
77
|
-
const from = before[key]
|
|
78
|
-
const to = after[key]
|
|
79
|
-
if (to !== undefined && from !== to) {
|
|
80
|
-
changed.push({ key, name: getOverridePackageName(key), from, to })
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return changed
|
|
84
|
-
}
|
|
85
24
|
|
|
86
25
|
function runPnpm(args) {
|
|
87
26
|
const result = spawnSync('pnpm', args, {
|
|
@@ -109,6 +48,20 @@ function runGit(args) {
|
|
|
109
48
|
return result.status ?? 1
|
|
110
49
|
}
|
|
111
50
|
|
|
51
|
+
function runPruneOverridesFix() {
|
|
52
|
+
console.log('[audit-override-commit] Pruning overrides that no longer affect pnpm audit…')
|
|
53
|
+
const result = spawnSync(process.execPath, [path.join(scriptDir, 'prune-overrides.mjs'), '--fix'], {
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
cwd,
|
|
56
|
+
env: process.env,
|
|
57
|
+
})
|
|
58
|
+
if (result.error) {
|
|
59
|
+
console.error('[audit-override-commit]', result.error.message)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
return result.status ?? 1
|
|
63
|
+
}
|
|
64
|
+
|
|
112
65
|
function stageExistingFiles(files) {
|
|
113
66
|
const existingFiles = files.filter((filePath) => fs.existsSync(path.join(cwd, filePath)))
|
|
114
67
|
if (existingFiles.length === 0) return 0
|
|
@@ -123,7 +76,7 @@ function main() {
|
|
|
123
76
|
|
|
124
77
|
let beforeOverrides
|
|
125
78
|
try {
|
|
126
|
-
beforeOverrides = collectOverrides()
|
|
79
|
+
beforeOverrides = collectOverrides(cwd)
|
|
127
80
|
} catch (err) {
|
|
128
81
|
console.error(`[audit-override-commit] Failed to read overrides before audit fix: ${err.message}`)
|
|
129
82
|
process.exit(1)
|
|
@@ -137,9 +90,19 @@ function main() {
|
|
|
137
90
|
process.exit(auditExitCode)
|
|
138
91
|
}
|
|
139
92
|
|
|
93
|
+
const installExitCode = runPnpm(['install'])
|
|
94
|
+
if (installExitCode !== 0) {
|
|
95
|
+
process.exit(installExitCode)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pruneExitCode = runPruneOverridesFix()
|
|
99
|
+
if (pruneExitCode !== 0) {
|
|
100
|
+
process.exit(pruneExitCode)
|
|
101
|
+
}
|
|
102
|
+
|
|
140
103
|
let afterOverrides
|
|
141
104
|
try {
|
|
142
|
-
afterOverrides = collectOverrides()
|
|
105
|
+
afterOverrides = collectOverrides(cwd)
|
|
143
106
|
} catch (err) {
|
|
144
107
|
console.error(`[audit-override-commit] Failed to read overrides after audit fix: ${err.message}`)
|
|
145
108
|
process.exit(1)
|
|
@@ -154,12 +117,7 @@ function main() {
|
|
|
154
117
|
console.log('[audit-override-commit] Override changes detected:')
|
|
155
118
|
for (const item of changed) {
|
|
156
119
|
const fromLabel = item.from ?? '(missing)'
|
|
157
|
-
console.log(` - ${item.name}: ${fromLabel} -> ${item.to}`)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const installExitCode = runPnpm(['install'])
|
|
161
|
-
if (installExitCode !== 0) {
|
|
162
|
-
process.exit(installExitCode)
|
|
120
|
+
console.log(` - ${item.name}: ${fromLabel} -> ${formatOverrideValue(item.to)}`)
|
|
163
121
|
}
|
|
164
122
|
|
|
165
123
|
const stageExitCode = stageExistingFiles(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])
|
|
@@ -167,7 +125,7 @@ function main() {
|
|
|
167
125
|
process.exit(stageExitCode)
|
|
168
126
|
}
|
|
169
127
|
|
|
170
|
-
const { subject, body } =
|
|
128
|
+
const { subject, body } = buildOverrideCommitMessage(changed)
|
|
171
129
|
console.log(`[audit-override-commit] Auto-committing: ${subject}`)
|
|
172
130
|
if (body) {
|
|
173
131
|
console.log(body)
|
|
@@ -176,16 +134,4 @@ function main() {
|
|
|
176
134
|
process.exit(runGit(commitArgs))
|
|
177
135
|
}
|
|
178
136
|
|
|
179
|
-
function buildCommitMessage(changed) {
|
|
180
|
-
if (changed.length === 1) {
|
|
181
|
-
const only = changed[0]
|
|
182
|
-
return { subject: `vuln(${only.name}): override ${only.to}`, body: '' }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const body = changed
|
|
186
|
-
.map((item) => `- ${item.name}: ${item.from ?? '(missing)'} -> ${item.to}`)
|
|
187
|
-
.join('\n')
|
|
188
|
-
return { subject: `vuln(): override ${changed.length} packages`, body }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
137
|
main()
|
package/scripts/fg-devkit.mjs
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* pnpm exec fg-devkit sync-node-toolchain [policy-file] [--dry-run]
|
|
16
16
|
* pnpm exec fg-devkit bump-node-toolchain [nvmrc-version] [projects-root] [--dry-run] [--skip-fleet] [--no-engines]
|
|
17
17
|
* pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
|
|
18
|
-
* pnpm exec fg-devkit policy-upgrade-commit [ncu-args...]
|
|
18
|
+
* pnpm exec fg-devkit policy-upgrade-commit [--prune-overrides] [ncu-args...]
|
|
19
19
|
* pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
|
|
20
|
+
* pnpm exec fg-devkit prune-overrides [--fix] [--commit]
|
|
20
21
|
* pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
|
|
21
22
|
* pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
|
|
22
23
|
* pnpm exec fg-devkit sync-vscode-settings
|
|
@@ -474,6 +475,7 @@ const DIRECT_COMMANDS = {
|
|
|
474
475
|
'policy-upgrade': 'policy-upgrade.mjs',
|
|
475
476
|
'policy-upgrade-commit': 'policy-upgrade-commit.mjs',
|
|
476
477
|
'audit-override-commit': 'audit-override-commit.mjs',
|
|
478
|
+
'prune-overrides': 'prune-overrides.mjs',
|
|
477
479
|
'sync-project-configs': 'sync-project-configs.mjs',
|
|
478
480
|
'sync-tooling-configs': 'sync-tooling-configs.mjs',
|
|
479
481
|
'sync-vscode-settings': 'sync-vscode-settings.mjs',
|
|
@@ -532,10 +534,13 @@ Commands:
|
|
|
532
534
|
Options: --dry-run, --fg-devkit-only, --pick, --all, --only=a,b
|
|
533
535
|
policy-upgrade [policy-file] [ncu-args...]
|
|
534
536
|
Upgrade policy packages via ncu (interactive) and auto-commit
|
|
535
|
-
policy-upgrade-commit [ncu-args...]
|
|
536
|
-
Upgrade
|
|
537
|
+
policy-upgrade-commit [--prune-overrides] [ncu-args...]
|
|
538
|
+
Upgrade consumer package.json packages and auto-commit
|
|
539
|
+
--prune-overrides also drops unused workspace overrides in a second commit
|
|
537
540
|
audit-override-commit [pnpm-audit-args...]
|
|
538
|
-
Fix
|
|
541
|
+
Fix vulnerability overrides via pnpm audit, prune unused ones, auto-commit
|
|
542
|
+
prune-overrides [--fix] [--commit]
|
|
543
|
+
Report unused workspace overrides; --fix removes them; --commit also auto-commits
|
|
539
544
|
sync-project-configs VS Code + tooling sync (overwrites config files)
|
|
540
545
|
sync-tooling-configs Tooling sync only (overwrites config files)
|
|
541
546
|
sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
|
|
@@ -812,7 +817,9 @@ async function runInteractiveMenu() {
|
|
|
812
817
|
{ name: 'Check', value: 'check' },
|
|
813
818
|
{ name: 'Sync and auto-commit', value: 'sync-commit' },
|
|
814
819
|
{ name: 'Upgrade package.json packages and auto-commit', value: 'upgrade-commit' },
|
|
820
|
+
{ name: 'Upgrade package.json packages, prune overrides, and auto-commit', value: 'upgrade-commit-prune' },
|
|
815
821
|
{ name: 'Fix vulnerability overrides and auto-commit', value: 'audit-override-commit' },
|
|
822
|
+
{ name: 'Prune unused vulnerability overrides and auto-commit', value: 'prune-overrides' },
|
|
816
823
|
{ name: 'Cancel', value: 'cancel' },
|
|
817
824
|
],
|
|
818
825
|
default: 'check',
|
|
@@ -823,10 +830,19 @@ async function runInteractiveMenu() {
|
|
|
823
830
|
if (choice === 'sync-commit') {
|
|
824
831
|
return runSelectedAction(() => runNode('sync-dependency-policy-commit.mjs'))
|
|
825
832
|
}
|
|
833
|
+
if (choice === 'upgrade-commit') {
|
|
834
|
+
return runSelectedAction(() => runNode('policy-upgrade-commit.mjs'))
|
|
835
|
+
}
|
|
836
|
+
if (choice === 'upgrade-commit-prune') {
|
|
837
|
+
return runSelectedAction(() => runNode('policy-upgrade-commit.mjs', ['--prune-overrides']))
|
|
838
|
+
}
|
|
826
839
|
if (choice === 'audit-override-commit') {
|
|
827
840
|
return runSelectedAction(() => runNode('audit-override-commit.mjs'))
|
|
828
841
|
}
|
|
829
|
-
|
|
842
|
+
if (choice === 'prune-overrides') {
|
|
843
|
+
return runSelectedAction(() => runNode('prune-overrides.mjs', ['--commit']))
|
|
844
|
+
}
|
|
845
|
+
return 1
|
|
830
846
|
}
|
|
831
847
|
|
|
832
848
|
if (group === 'sync-configs') {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
function unquoteYamlScalar(value) {
|
|
5
|
+
if (
|
|
6
|
+
(value.startsWith("'") && value.endsWith("'")) ||
|
|
7
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
8
|
+
) {
|
|
9
|
+
return value.slice(1, -1)
|
|
10
|
+
}
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getOverridePackageName(overrideKey) {
|
|
15
|
+
const key = unquoteYamlScalar(overrideKey.trim())
|
|
16
|
+
const match = key.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
|
|
17
|
+
return match ? match[1] : key
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readWorkspaceOverrides(yamlPath) {
|
|
21
|
+
if (!fs.existsSync(yamlPath)) return {}
|
|
22
|
+
const overrides = {}
|
|
23
|
+
let inOverrides = false
|
|
24
|
+
for (const line of fs.readFileSync(yamlPath, 'utf8').split('\n')) {
|
|
25
|
+
if (/^overrides:\s*$/.test(line)) {
|
|
26
|
+
inOverrides = true
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (!inOverrides) continue
|
|
30
|
+
if (/^\S/.test(line)) break
|
|
31
|
+
const match = line.match(/^ ([^:]+):\s*(.+?)\s*$/)
|
|
32
|
+
if (match) {
|
|
33
|
+
const key = unquoteYamlScalar(match[1].trim())
|
|
34
|
+
const value = unquoteYamlScalar(match[2].trim())
|
|
35
|
+
overrides[key] = value
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return overrides
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function collectOverrides(cwd = process.cwd()) {
|
|
42
|
+
const overrides = {}
|
|
43
|
+
const packageJsonPath = path.join(cwd, 'package.json')
|
|
44
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
45
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
46
|
+
Object.assign(overrides, pkg.overrides ?? {})
|
|
47
|
+
Object.assign(overrides, pkg.pnpm?.overrides ?? {})
|
|
48
|
+
}
|
|
49
|
+
Object.assign(overrides, readWorkspaceOverrides(path.join(cwd, 'pnpm-workspace.yaml')))
|
|
50
|
+
return overrides
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function collectOverrideChanges(before, after) {
|
|
54
|
+
const changed = []
|
|
55
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const from = before[key]
|
|
58
|
+
const to = after[key]
|
|
59
|
+
if (from !== to) {
|
|
60
|
+
changed.push({ key, name: getOverridePackageName(key), from, to })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return changed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatOverrideValue(value) {
|
|
67
|
+
return value ?? '(removed)'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildOverrideCommitMessage(changed) {
|
|
71
|
+
if (changed.length === 1) {
|
|
72
|
+
const only = changed[0]
|
|
73
|
+
if (only.to === undefined) {
|
|
74
|
+
return { subject: `vuln(${only.name}): prune override`, body: '' }
|
|
75
|
+
}
|
|
76
|
+
return { subject: `vuln(${only.name}): override ${only.to}`, body: '' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const body = changed
|
|
80
|
+
.map((item) => `- ${item.name}: ${item.from ?? '(missing)'} -> ${formatOverrideValue(item.to)}`)
|
|
81
|
+
.join('\n')
|
|
82
|
+
return { subject: `vuln(): override ${changed.length} packages`, body }
|
|
83
|
+
}
|
|
@@ -4,15 +4,33 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* pnpm exec fg-devkit policy-upgrade-commit [ncu-args...]
|
|
7
|
+
* pnpm exec fg-devkit policy-upgrade-commit --prune-overrides [ncu-args...]
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import fs from 'node:fs'
|
|
10
11
|
import path from 'node:path'
|
|
11
12
|
import { spawnSync } from 'node:child_process'
|
|
12
13
|
import { createRequire } from 'node:module'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import {
|
|
16
|
+
buildOverrideCommitMessage,
|
|
17
|
+
collectOverrideChanges,
|
|
18
|
+
collectOverrides,
|
|
19
|
+
formatOverrideValue,
|
|
20
|
+
} from './override-changes.mjs'
|
|
13
21
|
|
|
14
22
|
const require = createRequire(import.meta.url)
|
|
15
|
-
const
|
|
23
|
+
const cwd = process.cwd()
|
|
24
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const packageJsonPath = path.join(cwd, 'package.json')
|
|
26
|
+
const PRUNE_FLAG = '--prune-overrides'
|
|
27
|
+
|
|
28
|
+
function splitArgs(argv) {
|
|
29
|
+
return {
|
|
30
|
+
pruneOverrides: argv.includes(PRUNE_FLAG),
|
|
31
|
+
ncuArgs: argv.filter((arg) => arg !== PRUNE_FLAG),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
|
|
17
35
|
function runNcu(args = []) {
|
|
18
36
|
let ncuBin
|
|
@@ -31,7 +49,7 @@ function runNcu(args = []) {
|
|
|
31
49
|
|
|
32
50
|
const result = spawnSync(process.execPath, [ncuBin, ...effectiveArgs], {
|
|
33
51
|
stdio: 'inherit',
|
|
34
|
-
cwd
|
|
52
|
+
cwd,
|
|
35
53
|
env: process.env,
|
|
36
54
|
})
|
|
37
55
|
if (result.error) {
|
|
@@ -44,7 +62,21 @@ function runNcu(args = []) {
|
|
|
44
62
|
function runGit(args) {
|
|
45
63
|
const result = spawnSync('git', args, {
|
|
46
64
|
stdio: 'inherit',
|
|
47
|
-
cwd
|
|
65
|
+
cwd,
|
|
66
|
+
env: process.env,
|
|
67
|
+
})
|
|
68
|
+
if (result.error) {
|
|
69
|
+
console.error('[policy-upgrade-commit]', result.error.message)
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
return result.status ?? 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runPruneOverridesFix() {
|
|
76
|
+
console.log('[policy-upgrade-commit] Pruning overrides that no longer affect pnpm audit…')
|
|
77
|
+
const result = spawnSync(process.execPath, [path.join(scriptDir, 'prune-overrides.mjs'), '--fix'], {
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
cwd,
|
|
48
80
|
env: process.env,
|
|
49
81
|
})
|
|
50
82
|
if (result.error) {
|
|
@@ -55,7 +87,7 @@ function runGit(args) {
|
|
|
55
87
|
}
|
|
56
88
|
|
|
57
89
|
function stageExistingFiles(files) {
|
|
58
|
-
const existingFiles = files.filter((filePath) => fs.existsSync(path.join(
|
|
90
|
+
const existingFiles = files.filter((filePath) => fs.existsSync(path.join(cwd, filePath)))
|
|
59
91
|
if (existingFiles.length === 0) return 0
|
|
60
92
|
return runGit(['add', '--', ...existingFiles])
|
|
61
93
|
}
|
|
@@ -87,7 +119,6 @@ function collectPackageVersionChanges(beforePackage, afterPackage) {
|
|
|
87
119
|
return changed
|
|
88
120
|
}
|
|
89
121
|
|
|
90
|
-
/** ncu can upgrade package.json `packageManager` (e.g. pnpm@x.y.z); treat as one bump. */
|
|
91
122
|
function collectPackageManagerChange(beforePackage, afterPackage) {
|
|
92
123
|
const before = beforePackage.packageManager
|
|
93
124
|
const after = afterPackage.packageManager
|
|
@@ -102,12 +133,80 @@ function collectPackageManagerChange(beforePackage, afterPackage) {
|
|
|
102
133
|
}
|
|
103
134
|
}
|
|
104
135
|
|
|
136
|
+
function buildCommitMessage(changed) {
|
|
137
|
+
if (changed.length === 1) {
|
|
138
|
+
const only = changed[0]
|
|
139
|
+
return { subject: `dep(${only.name}): bump to ${only.to}`, body: '' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const body = changed.map((item) => `- ${item.name}: ${item.from} -> ${item.to}`).join('\n')
|
|
143
|
+
return { subject: `dep(): bump ${changed.length} packages`, body }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function commitChanges(prefix, subject, body) {
|
|
147
|
+
const stageExitCode = stageExistingFiles(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])
|
|
148
|
+
if (stageExitCode !== 0) {
|
|
149
|
+
return stageExitCode
|
|
150
|
+
}
|
|
151
|
+
console.log(`[${prefix}] Auto-committing: ${subject}`)
|
|
152
|
+
if (body) {
|
|
153
|
+
console.log(body)
|
|
154
|
+
}
|
|
155
|
+
const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
|
|
156
|
+
return runGit(commitArgs)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function pruneAndCommitOverrides() {
|
|
160
|
+
let beforeOverrides
|
|
161
|
+
try {
|
|
162
|
+
beforeOverrides = collectOverrides(cwd)
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error(`[policy-upgrade-commit] Failed to read overrides before prune: ${err.message}`)
|
|
165
|
+
return 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (Object.keys(beforeOverrides).length === 0) {
|
|
169
|
+
console.log('[policy-upgrade-commit] No overrides to prune.')
|
|
170
|
+
return 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pruneExitCode = runPruneOverridesFix()
|
|
174
|
+
if (pruneExitCode !== 0) {
|
|
175
|
+
return pruneExitCode
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let afterOverrides
|
|
179
|
+
try {
|
|
180
|
+
afterOverrides = collectOverrides(cwd)
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(`[policy-upgrade-commit] Failed to read overrides after prune: ${err.message}`)
|
|
183
|
+
return 1
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const changed = collectOverrideChanges(beforeOverrides, afterOverrides)
|
|
187
|
+
if (changed.length === 0) {
|
|
188
|
+
console.log('[policy-upgrade-commit] No unused overrides to prune.')
|
|
189
|
+
return 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log('[policy-upgrade-commit] Override changes detected:')
|
|
193
|
+
for (const item of changed) {
|
|
194
|
+
const fromLabel = item.from ?? '(missing)'
|
|
195
|
+
console.log(` - ${item.name}: ${fromLabel} -> ${formatOverrideValue(item.to)}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { subject, body } = buildOverrideCommitMessage(changed)
|
|
199
|
+
return commitChanges('policy-upgrade-commit', subject, body)
|
|
200
|
+
}
|
|
201
|
+
|
|
105
202
|
function main() {
|
|
106
203
|
if (!fs.existsSync(packageJsonPath)) {
|
|
107
204
|
console.error(`[policy-upgrade-commit] package.json not found at ${packageJsonPath}`)
|
|
108
205
|
process.exit(1)
|
|
109
206
|
}
|
|
110
207
|
|
|
208
|
+
const { pruneOverrides, ncuArgs } = splitArgs(process.argv.slice(2))
|
|
209
|
+
|
|
111
210
|
let beforePackage
|
|
112
211
|
try {
|
|
113
212
|
beforePackage = readJson(packageJsonPath)
|
|
@@ -116,7 +215,7 @@ function main() {
|
|
|
116
215
|
process.exit(1)
|
|
117
216
|
}
|
|
118
217
|
|
|
119
|
-
const upgradeExitCode = runNcu(
|
|
218
|
+
const upgradeExitCode = runNcu(ncuArgs)
|
|
120
219
|
if (upgradeExitCode !== 0) {
|
|
121
220
|
process.exit(upgradeExitCode)
|
|
122
221
|
}
|
|
@@ -137,36 +236,24 @@ function main() {
|
|
|
137
236
|
|
|
138
237
|
if (changed.length === 0) {
|
|
139
238
|
console.log('[policy-upgrade-commit] No package version changes detected in package.json.')
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(` - ${item.name}: ${item.from} -> ${item.to}`)
|
|
146
|
-
}
|
|
239
|
+
} else {
|
|
240
|
+
console.log('[policy-upgrade-commit] Package version changes detected in package.json:')
|
|
241
|
+
for (const item of changed) {
|
|
242
|
+
console.log(` - ${item.name}: ${item.from} -> ${item.to}`)
|
|
243
|
+
}
|
|
147
244
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
245
|
+
const { subject, body } = buildCommitMessage(changed)
|
|
246
|
+
const commitExitCode = commitChanges('policy-upgrade-commit', subject, body)
|
|
247
|
+
if (commitExitCode !== 0) {
|
|
248
|
+
process.exit(commitExitCode)
|
|
249
|
+
}
|
|
151
250
|
}
|
|
152
251
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (body) {
|
|
156
|
-
console.log(body)
|
|
157
|
-
}
|
|
158
|
-
const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
|
|
159
|
-
process.exit(runGit(commitArgs))
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function buildCommitMessage(changed) {
|
|
163
|
-
if (changed.length === 1) {
|
|
164
|
-
const only = changed[0]
|
|
165
|
-
return { subject: `dep(${only.name}): bump to ${only.to}`, body: '' }
|
|
252
|
+
if (!pruneOverrides) {
|
|
253
|
+
process.exit(0)
|
|
166
254
|
}
|
|
167
255
|
|
|
168
|
-
|
|
169
|
-
return { subject: `dep(): bump ${changed.length} packages`, body }
|
|
256
|
+
process.exit(pruneAndCommitOverrides())
|
|
170
257
|
}
|
|
171
258
|
|
|
172
259
|
main()
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Find pnpm-workspace.yaml overrides that no longer change the audit report.
|
|
4
|
+
*
|
|
5
|
+
* Each remaining override is removed in isolation (in a temp copy), the lockfile
|
|
6
|
+
* is re-resolved, and `pnpm audit --json` is compared to the current remaining
|
|
7
|
+
* set. That way overlapping rules (e.g. three nodemailer selectors) are not all
|
|
8
|
+
* dropped just because each is redundant while the others exist.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm exec fg-devkit prune-overrides # report only
|
|
12
|
+
* pnpm exec fg-devkit prune-overrides --fix # drop the prunable entries
|
|
13
|
+
* pnpm exec fg-devkit prune-overrides --commit # drop them and auto-commit
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, rmSync, cpSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { spawnSync } from 'node:child_process'
|
|
20
|
+
import { parseArgs } from 'node:util'
|
|
21
|
+
import {
|
|
22
|
+
buildOverrideCommitMessage,
|
|
23
|
+
collectOverrideChanges,
|
|
24
|
+
collectOverrides,
|
|
25
|
+
formatOverrideValue,
|
|
26
|
+
} from './override-changes.mjs'
|
|
27
|
+
|
|
28
|
+
const ROOT = process.cwd()
|
|
29
|
+
const WORKSPACE_FILE = 'pnpm-workspace.yaml'
|
|
30
|
+
const COPIED_FILES = ['package.json', 'pnpm-lock.yaml', WORKSPACE_FILE, '.npmrc']
|
|
31
|
+
|
|
32
|
+
const { values: args } = parseArgs({
|
|
33
|
+
options: {
|
|
34
|
+
fix: { type: 'boolean', default: false },
|
|
35
|
+
commit: { type: 'boolean', default: false },
|
|
36
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
37
|
+
},
|
|
38
|
+
strict: true,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (args.help) {
|
|
42
|
+
console.log(`Usage: pnpm exec fg-devkit prune-overrides [--fix] [--commit]
|
|
43
|
+
|
|
44
|
+
Reports workspace overrides that can be removed without introducing new
|
|
45
|
+
pnpm audit findings. Pass --fix to delete those entries in place.
|
|
46
|
+
Pass --commit to apply the deletions and auto-commit.`)
|
|
47
|
+
process.exit(0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const shouldFix = args.fix || args.commit
|
|
51
|
+
|
|
52
|
+
function runGit(gitArgs) {
|
|
53
|
+
const result = spawnSync('git', gitArgs, {
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
cwd: ROOT,
|
|
56
|
+
env: process.env,
|
|
57
|
+
})
|
|
58
|
+
if (result.error) {
|
|
59
|
+
console.error('[prune-overrides]', result.error.message)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
return result.status ?? 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function commitOverrideChanges(beforeOverrides, afterOverrides) {
|
|
66
|
+
const changed = collectOverrideChanges(beforeOverrides, afterOverrides)
|
|
67
|
+
if (changed.length === 0) {
|
|
68
|
+
console.log('[prune-overrides] No override changes to commit.')
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('[prune-overrides] Override changes detected:')
|
|
73
|
+
for (const item of changed) {
|
|
74
|
+
const fromLabel = item.from ?? '(missing)'
|
|
75
|
+
console.log(` - ${item.name}: ${fromLabel} -> ${formatOverrideValue(item.to)}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const files = ['package.json', 'pnpm-lock.yaml', WORKSPACE_FILE].filter((file) =>
|
|
79
|
+
existsSync(join(ROOT, file)),
|
|
80
|
+
)
|
|
81
|
+
if (files.length === 0) {
|
|
82
|
+
console.error('[prune-overrides] No files to stage.')
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stageExitCode = runGit(['add', '--', ...files])
|
|
87
|
+
if (stageExitCode !== 0) {
|
|
88
|
+
return stageExitCode
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { subject, body } = buildOverrideCommitMessage(changed)
|
|
92
|
+
console.log(`[prune-overrides] Auto-committing: ${subject}`)
|
|
93
|
+
if (body) {
|
|
94
|
+
console.log(body)
|
|
95
|
+
}
|
|
96
|
+
const commitArgs = body ? ['commit', '-m', subject, '-m', body] : ['commit', '-m', subject]
|
|
97
|
+
return runGit(commitArgs)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runPnpm(cwd, pnpmArgs, { allowFailure = false } = {}) {
|
|
101
|
+
const result = spawnSync('pnpm', pnpmArgs, {
|
|
102
|
+
cwd,
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
env: { ...process.env, CI: 'true' },
|
|
105
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
106
|
+
})
|
|
107
|
+
if (result.error) {
|
|
108
|
+
throw result.error
|
|
109
|
+
}
|
|
110
|
+
if (!allowFailure && result.status !== 0) {
|
|
111
|
+
const detail = (result.stderr || result.stdout || '').trim()
|
|
112
|
+
throw new Error(`pnpm ${pnpmArgs.join(' ')} failed (exit ${result.status})\n${detail}`)
|
|
113
|
+
}
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function splitYamlSections(text) {
|
|
118
|
+
const lines = text.split('\n')
|
|
119
|
+
const preamble = []
|
|
120
|
+
const sections = []
|
|
121
|
+
let current = null
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const isTopLevel =
|
|
124
|
+
line !== '' &&
|
|
125
|
+
!line.startsWith(' ') &&
|
|
126
|
+
!line.startsWith('\t') &&
|
|
127
|
+
!line.startsWith('#') &&
|
|
128
|
+
/^[A-Za-z][\w.-]*:/.test(line)
|
|
129
|
+
if (isTopLevel) {
|
|
130
|
+
current = { header: line, body: [] }
|
|
131
|
+
sections.push(current)
|
|
132
|
+
} else if (current) {
|
|
133
|
+
current.body.push(line)
|
|
134
|
+
} else {
|
|
135
|
+
preamble.push(line)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { preamble, sections }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function joinYamlSections({ preamble, sections }) {
|
|
142
|
+
const chunks = []
|
|
143
|
+
if (preamble.some((line) => line !== '')) {
|
|
144
|
+
chunks.push(preamble.join('\n'))
|
|
145
|
+
}
|
|
146
|
+
for (const section of sections) {
|
|
147
|
+
const body = section.body
|
|
148
|
+
const trimmed = [...body]
|
|
149
|
+
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
|
|
150
|
+
trimmed.pop()
|
|
151
|
+
}
|
|
152
|
+
chunks.push([section.header, ...trimmed].join('\n'))
|
|
153
|
+
}
|
|
154
|
+
return `${chunks.join('\n')}\n`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseOverrideLine(line) {
|
|
158
|
+
const match = line.match(/^(\s+)(?:"([^"]+)"|'([^']+)'|([^:]+)):\s*(.*?)\s*$/)
|
|
159
|
+
if (!match) {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
return { key: match[2] ?? match[3] ?? match[4], value: match[5] }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readOverrides(workspaceText) {
|
|
166
|
+
const doc = splitYamlSections(workspaceText)
|
|
167
|
+
const section = doc.sections.find((item) => item.header.startsWith('overrides:'))
|
|
168
|
+
if (!section) {
|
|
169
|
+
return { doc, entries: [] }
|
|
170
|
+
}
|
|
171
|
+
const entries = []
|
|
172
|
+
for (const line of section.body) {
|
|
173
|
+
if (!line.trim() || line.trim().startsWith('#')) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
const parsed = parseOverrideLine(line)
|
|
177
|
+
if (!parsed) {
|
|
178
|
+
throw new Error(`cannot parse override line: ${line}`)
|
|
179
|
+
}
|
|
180
|
+
entries.push({ ...parsed, line })
|
|
181
|
+
}
|
|
182
|
+
return { doc, entries }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writeOverrides(workspaceText, keepKeys) {
|
|
186
|
+
const keep = new Set(keepKeys)
|
|
187
|
+
const { doc, entries } = readOverrides(workspaceText)
|
|
188
|
+
const sectionIndex = doc.sections.findIndex((item) => item.header.startsWith('overrides:'))
|
|
189
|
+
if (sectionIndex === -1) {
|
|
190
|
+
return workspaceText
|
|
191
|
+
}
|
|
192
|
+
const kept = entries.filter((entry) => keep.has(entry.key))
|
|
193
|
+
if (kept.length === 0) {
|
|
194
|
+
doc.sections.splice(sectionIndex, 1)
|
|
195
|
+
} else {
|
|
196
|
+
doc.sections[sectionIndex].body = kept.map((entry) => entry.line)
|
|
197
|
+
}
|
|
198
|
+
return joinYamlSections(doc)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function advisoryIds(report) {
|
|
202
|
+
const ids = new Set()
|
|
203
|
+
if (report?.advisories && typeof report.advisories === 'object') {
|
|
204
|
+
for (const [key, advisory] of Object.entries(report.advisories)) {
|
|
205
|
+
ids.add(advisory?.github_advisory_id || advisory?.id || key)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return ids
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseAuditJson(stdout) {
|
|
212
|
+
const text = stdout.trim()
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(text)
|
|
215
|
+
} catch {
|
|
216
|
+
const start = text.indexOf('{')
|
|
217
|
+
const end = text.lastIndexOf('}')
|
|
218
|
+
if (start >= 0 && end > start) {
|
|
219
|
+
return JSON.parse(text.slice(start, end + 1))
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`pnpm audit --json did not return JSON\n${text.slice(0, 500)}`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function auditProject(cwd) {
|
|
226
|
+
const result = runPnpm(cwd, ['audit', '--json'], { allowFailure: true })
|
|
227
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`pnpm audit failed (exit ${result.status})\n${(result.stderr || result.stdout || '').trim()}`,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
return advisoryIds(parseAuditJson(result.stdout))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function copyProject(dest) {
|
|
236
|
+
for (const file of COPIED_FILES) {
|
|
237
|
+
const from = join(ROOT, file)
|
|
238
|
+
if (existsSync(from)) {
|
|
239
|
+
cpSync(from, join(dest, file))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveAndAudit(tempDir, workspaceText) {
|
|
245
|
+
writeFileSync(join(tempDir, WORKSPACE_FILE), workspaceText)
|
|
246
|
+
cpSync(join(ROOT, 'pnpm-lock.yaml'), join(tempDir, 'pnpm-lock.yaml'))
|
|
247
|
+
runPnpm(tempDir, ['install', '--lockfile-only', '--ignore-scripts', '--no-frozen-lockfile'])
|
|
248
|
+
return auditProject(tempDir)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatIds(ids) {
|
|
252
|
+
return [...ids].sort().join(', ')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function main() {
|
|
256
|
+
const workspacePath = join(ROOT, WORKSPACE_FILE)
|
|
257
|
+
if (!existsSync(workspacePath)) {
|
|
258
|
+
console.log('No pnpm-workspace.yaml.')
|
|
259
|
+
return 0
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const original = readFileSync(workspacePath, 'utf8')
|
|
263
|
+
const { entries } = readOverrides(original)
|
|
264
|
+
if (entries.length === 0) {
|
|
265
|
+
console.log('No overrides in pnpm-workspace.yaml.')
|
|
266
|
+
return 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!existsSync(join(ROOT, 'pnpm-lock.yaml'))) {
|
|
270
|
+
throw new Error('pnpm-lock.yaml not found')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'prune-overrides-'))
|
|
274
|
+
try {
|
|
275
|
+
copyProject(tempDir)
|
|
276
|
+
console.log(`Checking ${entries.length} override(s) against pnpm audit…`)
|
|
277
|
+
const remaining = new Map(entries.map((entry) => [entry.key, entry]))
|
|
278
|
+
const prunable = []
|
|
279
|
+
let baseline = resolveAndAudit(tempDir, writeOverrides(original, remaining.keys()))
|
|
280
|
+
|
|
281
|
+
let index = 0
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
index += 1
|
|
284
|
+
const trialKeys = [...remaining.keys()].filter((key) => key !== entry.key)
|
|
285
|
+
process.stdout.write(`[${index}/${entries.length}] ${entry.key} … `)
|
|
286
|
+
try {
|
|
287
|
+
const trial = resolveAndAudit(tempDir, writeOverrides(original, trialKeys))
|
|
288
|
+
const introduced = [...trial].filter((id) => !baseline.has(id))
|
|
289
|
+
if (introduced.length > 0) {
|
|
290
|
+
console.log(`KEEP (new: ${formatIds(introduced)})`)
|
|
291
|
+
} else {
|
|
292
|
+
remaining.delete(entry.key)
|
|
293
|
+
prunable.push(entry.key)
|
|
294
|
+
baseline = trial
|
|
295
|
+
console.log('PRUNE')
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
const message = error instanceof Error ? error.message.split('\n')[0] : String(error)
|
|
299
|
+
console.log(`KEEP (${message})`)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log('')
|
|
304
|
+
if (prunable.length === 0) {
|
|
305
|
+
console.log('Every override still affects the audit report.')
|
|
306
|
+
return 0
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(`Prunable (${prunable.length}):`)
|
|
310
|
+
for (const key of prunable) {
|
|
311
|
+
console.log(` - ${key}`)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!shouldFix) {
|
|
315
|
+
console.log('\nRe-run with --fix to remove them, or --commit to remove and auto-commit.')
|
|
316
|
+
return 0
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const beforeOverrides = collectOverrides(ROOT)
|
|
320
|
+
writeFileSync(workspacePath, writeOverrides(original, remaining.keys()))
|
|
321
|
+
console.log('\nUpdated pnpm-workspace.yaml. Refreshing lockfile…')
|
|
322
|
+
runPnpm(ROOT, ['install', '--no-frozen-lockfile', '--ignore-scripts'])
|
|
323
|
+
|
|
324
|
+
if (!args.commit) {
|
|
325
|
+
console.log('Done.')
|
|
326
|
+
return 0
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return commitOverrideChanges(beforeOverrides, collectOverrides(ROOT))
|
|
330
|
+
} finally {
|
|
331
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
process.exit(main())
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error(error instanceof Error ? error.message : error)
|
|
339
|
+
process.exit(2)
|
|
340
|
+
}
|