fg-devkit 1.4.0 → 1.5.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/README.md CHANGED
@@ -78,6 +78,36 @@ Check outdated policy versions:
78
78
  pnpm exec fgd policy-outdated
79
79
  ```
80
80
 
81
+ Check vulnerabilities for all policy packages together (including transitive dependencies):
82
+
83
+ ```bash
84
+ pnpm exec fgd policy-vulns
85
+ ```
86
+
87
+ Check vulnerabilities for all dependencies in the current repo:
88
+
89
+ ```bash
90
+ pnpm exec fgd repo-vulns
91
+ ```
92
+
93
+ Check vulnerabilities across all Vue repositories under the projects root:
94
+
95
+ ```bash
96
+ pnpm exec fgd fleet-vulns
97
+ ```
98
+
99
+ For summary-only mode (no full per-repo pnpm audit output):
100
+
101
+ ```bash
102
+ pnpm exec fgd fleet-vulns --summary-only
103
+ ```
104
+
105
+ If npm audit is rate-limited (429), slow the scan down:
106
+
107
+ ```bash
108
+ pnpm exec fgd fleet-vulns --delay-ms=8000
109
+ ```
110
+
81
111
  Upgrade policy versions interactively:
82
112
 
83
113
  ```bash
@@ -10,7 +10,7 @@
10
10
  "@sentry/tracing": "7.37.2",
11
11
  "@sentry/vue": "7.37.2",
12
12
  "@sentry/wasm": "7.67.0",
13
- "@tailwindcss/postcss": "4.2.3",
13
+ "@tailwindcss/postcss": "4.2.4",
14
14
  "@tanstack/vue-query": "5.99.2",
15
15
  "@types/leaflet": "1.5.7",
16
16
  "@unhead/vue": "2.0.5",
@@ -23,7 +23,7 @@
23
23
  "content-disposition-header": "0.6.0",
24
24
  "date-fns": "4.1.0",
25
25
  "element-plus": "2.9.7",
26
- "fds-vue-core": "6.2.1",
26
+ "fds-vue-core": "6.2.2",
27
27
  "imask": "7.6.1",
28
28
  "leaflet": "1.8.0",
29
29
  "lexical": "0.30.0",
@@ -84,13 +84,19 @@
84
84
  }
85
85
  },
86
86
  "managedAnywhere": {
87
- "@tailwindcss/vite": "4.2.3",
87
+ "@tailwindcss/vite": "4.2.4",
88
88
  "@vitejs/plugin-vue": "6.0.6",
89
89
  "axios": "1.15.1",
90
90
  "dotenv": "16.4.5",
91
91
  "qrcode.vue": "3.8.1",
92
- "tailwindcss": "4.2.3",
92
+ "tailwindcss": "4.2.4",
93
93
  "vite": "7.3.2",
94
94
  "vue": "3.5.32"
95
+ },
96
+ "overrides": {
97
+ "lodash@<=4.17.23": ">=4.18.0",
98
+ "lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
99
+ "lodash@>=4.0.0 <=4.17.23": ">=4.18.0",
100
+ "tmp@<=0.2.3": ">=0.2.4"
95
101
  }
96
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -29,6 +29,9 @@
29
29
  "./scripts/sync-consumer-lefthook.mjs": "./scripts/sync-consumer-lefthook.mjs",
30
30
  "./scripts/prune-consumer-deps.mjs": "./scripts/prune-consumer-deps.mjs",
31
31
  "./scripts/policy-outdated.mjs": "./scripts/policy-outdated.mjs",
32
+ "./scripts/policy-vulns.mjs": "./scripts/policy-vulns.mjs",
33
+ "./scripts/repo-vulns.mjs": "./scripts/repo-vulns.mjs",
34
+ "./scripts/fleet-vulns.mjs": "./scripts/fleet-vulns.mjs",
32
35
  "./scripts/policy-upgrade.mjs": "./scripts/policy-upgrade.mjs",
33
36
  "./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
34
37
  "./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
@@ -21,6 +21,7 @@ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mj
21
21
 
22
22
  const cwd = process.cwd()
23
23
  const pkgPath = path.resolve(cwd, 'package.json')
24
+ const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
24
25
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
25
26
  const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
26
27
 
@@ -42,6 +43,30 @@ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
42
43
  const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
43
44
  const mismatches = []
44
45
 
46
+ function getOverridePackageName(overrideKey) {
47
+ const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
48
+ return match ? match[1] : overrideKey
49
+ }
50
+
51
+ function getRelevantPolicyOverrides(policyOverrides) {
52
+ const entries = Object.entries(policyOverrides ?? {})
53
+ if (entries.length === 0) {
54
+ return {}
55
+ }
56
+ if (!fs.existsSync(lockfilePath)) {
57
+ return Object.fromEntries(entries)
58
+ }
59
+ const lockfile = fs.readFileSync(lockfilePath, 'utf8')
60
+ const relevant = {}
61
+ for (const [name, version] of entries) {
62
+ const pkgName = getOverridePackageName(name)
63
+ if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
64
+ relevant[name] = version
65
+ }
66
+ }
67
+ return relevant
68
+ }
69
+
45
70
  /** True if this package+section is governed by the policy (so global pin checks skip it). */
46
71
  function isPolicyManagedInSection(section, name) {
47
72
  if (policy.managedAnywhere?.[name] !== undefined) {
@@ -56,7 +81,6 @@ for (const [section, dependencies] of Object.entries(policy.managedDependencies
56
81
  }
57
82
 
58
83
  mismatches.push(...collectPinViolations('[policy] managedAnywhere.', policy.managedAnywhere ?? {}))
59
- mismatches.push(...collectPinViolations('[policy] overrides.', policy.overrides ?? {}))
60
84
 
61
85
  // Intersection: only packages that exist in both policy and consumer are compared.
62
86
  for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
@@ -98,12 +122,10 @@ for (const [name, expected] of Object.entries(policy.managedAnywhere ?? {})) {
98
122
 
99
123
  // Optional: policy may define pnpm.overrides entries the consumer must mirror.
100
124
  const overrides = pkg.pnpm?.overrides ?? {}
101
- for (const [name, expected] of Object.entries(policy.overrides ?? {})) {
125
+ for (const [name, expected] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
102
126
  const actual = overrides[name]
103
127
  if (actual !== expected) {
104
128
  mismatches.push(`pnpm.overrides.${name}: expected ${expected}, got ${actual ?? '(missing)'}`)
105
- } else if (actual !== undefined && !isPinnedDependencyVersion(actual)) {
106
- mismatches.push(`pnpm.overrides.${name}: must be pinned, got ${JSON.stringify(actual)}`)
107
129
  }
108
130
  }
109
131
 
@@ -121,9 +143,6 @@ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies'
121
143
  }
122
144
  }
123
145
 
124
- // Consumer pnpm.overrides must be pinned (ranges reported even if not in policy).
125
- mismatches.push(...collectPinViolations('[pins] pnpm.overrides.', pkg.pnpm?.overrides ?? {}))
126
-
127
146
  if (mismatches.length > 0) {
128
147
  console.error('[check-deps] Dependency policy mismatches found:')
129
148
  for (const mismatch of mismatches) {
@@ -7,6 +7,8 @@
7
7
  * pnpm exec fg-devkit sync-deps [policy-file]
8
8
  * pnpm exec fg-devkit policy-outdated [policy-file]
9
9
  * pnpm exec fg-devkit policy-vulns [policy-file]
10
+ * pnpm exec fg-devkit repo-vulns
11
+ * pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
10
12
  * pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
11
13
  * pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
12
14
  * pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
@@ -98,24 +100,6 @@ function runPnpmInstall() {
98
100
  return r.status ?? 1
99
101
  }
100
102
 
101
- /** Run pnpm audit in current repo and return normalized status info. */
102
- function runPnpmAuditCurrentRepo() {
103
- const r = spawnSync('pnpm', ['audit'], {
104
- stdio: 'inherit',
105
- cwd: process.cwd(),
106
- env: process.env,
107
- })
108
- if (r.error) {
109
- console.error('[fg-devkit]', r.error.message)
110
- return { ok: false, hasVulns: false, exitCode: 1 }
111
- }
112
- const code = r.status ?? 1
113
- if (code === 1) {
114
- return { ok: true, hasVulns: true, exitCode: 0 }
115
- }
116
- return { ok: code === 0, hasVulns: false, exitCode: code }
117
- }
118
-
119
103
  /** Run Lefthook from fg-devkit bundled dependency set. */
120
104
  function runBundledLefthook(args = []) {
121
105
  let lefthookBin
@@ -275,22 +259,9 @@ function runMaintainerPolicyUpgradeAndCommit() {
275
259
  return commitStaged(commitMessage)
276
260
  }
277
261
 
278
- /** Maintainer flow: run both policy vuln check and fg-devkit root audit. */
262
+ /** Maintainer flow: run full repo vulnerability audit in fg-devkit root. */
279
263
  function runMaintainerAuditAll() {
280
- console.log('[fg-devkit] Running policy vulnerability scan...')
281
- const policyCode = runNode('policy-vulns.mjs')
282
- if (policyCode !== 0) return policyCode
283
-
284
- console.log('[fg-devkit] Running fg-devkit repository audit...')
285
- const repoAudit = runPnpmAuditCurrentRepo()
286
- if (!repoAudit.ok) return repoAudit.exitCode
287
-
288
- if (repoAudit.hasVulns) {
289
- console.log('[fg-devkit] Completed: vulnerabilities were reported in fg-devkit repository audit.')
290
- } else {
291
- console.log('[fg-devkit] Completed: no vulnerabilities reported in fg-devkit repository audit.')
292
- }
293
- return 0
264
+ return runNode('repo-vulns.mjs')
294
265
  }
295
266
 
296
267
  /** Prompt release type in CLI before invoking release.sh to avoid a "stuck" terminal feeling. */
@@ -435,6 +406,8 @@ const DIRECT_COMMANDS = {
435
406
  'sync-deps': 'sync-dependency-policy.mjs',
436
407
  'policy-outdated': 'policy-outdated.mjs',
437
408
  'policy-vulns': 'policy-vulns.mjs',
409
+ 'repo-vulns': 'repo-vulns.mjs',
410
+ 'fleet-vulns': 'fleet-vulns.mjs',
438
411
  'policy-upgrade': 'policy-upgrade.mjs',
439
412
  'sync-project-configs': 'sync-project-configs.mjs',
440
413
  'sync-tooling-configs': 'sync-tooling-configs.mjs',
@@ -452,7 +425,10 @@ Commands:
452
425
  check-deps [policy-file] Validate package.json + pins against policy
453
426
  sync-deps [policy-file] Align declared versions to policy (existing keys only)
454
427
  policy-outdated [policy-file] Show outdated policy packages via pnpm outdated
455
- policy-vulns [policy-file] Check policy packages for vulnerabilities via pnpm audit
428
+ policy-vulns [policy-file] Check all policy packages together (including transitives) via pnpm audit
429
+ repo-vulns Check all repo dependencies for vulnerabilities via pnpm audit
430
+ fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
431
+ Run pnpm audit across Vue repos (full output + combined summary)
456
432
  policy-upgrade [policy-file] [ncu-args...]
457
433
  Upgrade policy packages via npm-check-updates (default interactive)
458
434
  sync-project-configs VS Code + tooling sync (overwrites config files)
@@ -556,9 +532,14 @@ const MAINTAINER_MENU = [
556
532
  },
557
533
  {
558
534
  key: 'm6',
559
- label: 'Maintainer: Check vulnerabilities (policy + fg-devkit repo)',
535
+ label: 'Maintainer: Check fg-devkit repo vulnerabilities',
560
536
  run: () => runMaintainerAuditAll(),
561
537
  },
538
+ {
539
+ key: 'm7',
540
+ label: 'Maintainer: Check vulnerabilities across all Vue repos',
541
+ run: () => runNode('fleet-vulns.mjs'),
542
+ },
562
543
  ]
563
544
 
564
545
  function isFgDevkitMaintainerContext() {
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runs vulnerability audits across Vue repositories and prints both per-repo
4
+ * pnpm audit output and a combined summary.
5
+ *
6
+ * Usage:
7
+ * pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import { spawnSync } from 'node:child_process'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
16
+ const cliArgs = process.argv.slice(2)
17
+ const summaryOnly = cliArgs.includes('--summary-only')
18
+ const projectsRootArg = cliArgs.find((a) => !a.startsWith('-'))
19
+ const projectsRoot = projectsRootArg ? path.resolve(process.cwd(), projectsRootArg) : path.resolve(scriptDir, '../..')
20
+ const delayArg = cliArgs.find((a) => a.startsWith('--delay-ms='))
21
+ const delayMs = Number(delayArg?.split('=')[1] ?? 2500)
22
+
23
+ function sleep(ms) {
24
+ return new Promise((resolve) => setTimeout(resolve, ms))
25
+ }
26
+
27
+ function walk(dir, acc = []) {
28
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
29
+ if (item.name === 'node_modules' || item.name === '.git') continue
30
+ const p = path.join(dir, item.name)
31
+ if (item.isDirectory()) walk(p, acc)
32
+ else if (item.name === 'package.json') acc.push(p)
33
+ }
34
+ return acc
35
+ }
36
+
37
+ function readJson(p) {
38
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
39
+ }
40
+
41
+ function isVueProject(pkg) {
42
+ return Boolean(pkg.dependencies?.vue || pkg.devDependencies?.vue)
43
+ }
44
+
45
+ function countTotal(severityMap = {}) {
46
+ return Object.values(severityMap).reduce((sum, n) => sum + Number(n || 0), 0)
47
+ }
48
+
49
+ const packageJsonFiles = walk(projectsRoot)
50
+ const repos = []
51
+ for (const packageJsonPath of packageJsonFiles) {
52
+ try {
53
+ const pkg = readJson(packageJsonPath)
54
+ const repoDir = path.dirname(packageJsonPath)
55
+ if (!isVueProject(pkg)) continue
56
+ if (pkg.name === 'fg-devkit') continue
57
+ repos.push(repoDir)
58
+ } catch {
59
+ // Ignore invalid package.json files.
60
+ }
61
+ }
62
+
63
+ const uniqueRepos = [...new Set(repos)].sort((a, b) => a.localeCompare(b))
64
+ if (uniqueRepos.length === 0) {
65
+ console.log(`[fleet-vulns] No Vue repositories found under ${projectsRoot}`)
66
+ process.exit(0)
67
+ }
68
+
69
+ console.log(`[fleet-vulns] projectsRoot=${projectsRoot}`)
70
+ console.log(`[fleet-vulns] repositories=${uniqueRepos.length}`)
71
+
72
+ const results = []
73
+ let hasFailures = false
74
+ let hasVulnerabilities = false
75
+
76
+ for (const repoDir of uniqueRepos) {
77
+ const repoName = path.basename(repoDir)
78
+ const lockfilePath = path.join(repoDir, 'pnpm-lock.yaml')
79
+ if (!fs.existsSync(lockfilePath)) {
80
+ console.log(`[fleet-vulns] SKIP ${repoName}: missing pnpm-lock.yaml`)
81
+ continue
82
+ }
83
+
84
+ console.log(`\n[fleet-vulns] Auditing ${repoName}`)
85
+ if (!summaryOnly) {
86
+ const prettyAudit = spawnSync('pnpm', ['audit'], {
87
+ cwd: repoDir,
88
+ env: process.env,
89
+ stdio: 'inherit',
90
+ })
91
+
92
+ if (prettyAudit.error) {
93
+ hasFailures = true
94
+ console.error(`[fleet-vulns] FAIL ${repoName}: ${prettyAudit.error.message}`)
95
+ continue
96
+ }
97
+ const prettyCode = prettyAudit.status ?? 1
98
+ if (prettyCode !== 0 && prettyCode !== 1) {
99
+ hasFailures = true
100
+ console.error(`[fleet-vulns] FAIL ${repoName}: pnpm audit exited with code ${prettyCode}`)
101
+ continue
102
+ }
103
+ }
104
+
105
+ const audit = spawnSync('pnpm', ['audit', '--json'], {
106
+ cwd: repoDir,
107
+ env: process.env,
108
+ encoding: 'utf8',
109
+ })
110
+
111
+ if (audit.error) {
112
+ hasFailures = true
113
+ console.error(`[fleet-vulns] FAIL ${repoName}: ${audit.error.message}`)
114
+ continue
115
+ }
116
+
117
+ let report = null
118
+ try {
119
+ report = JSON.parse(audit.stdout || '{}')
120
+ } catch {
121
+ report = null
122
+ }
123
+
124
+ if (!report || typeof report !== 'object') {
125
+ hasFailures = true
126
+ console.error(`[fleet-vulns] FAIL ${repoName}: could not parse pnpm audit --json output`)
127
+ continue
128
+ }
129
+
130
+ const vulnerabilities = report.metadata?.vulnerabilities ?? {}
131
+ const total = countTotal(vulnerabilities)
132
+ if (total > 0) hasVulnerabilities = true
133
+
134
+ results.push({
135
+ repoName,
136
+ vulnerabilities,
137
+ total,
138
+ dependencies: report.metadata?.totalDependencies ?? 0,
139
+ })
140
+
141
+ if (delayMs > 0) {
142
+ await sleep(delayMs)
143
+ }
144
+ }
145
+
146
+ console.log('\n[fleet-vulns] Summary')
147
+ for (const r of results) {
148
+ const v = r.vulnerabilities
149
+ console.log(
150
+ `- ${r.repoName}: total=${r.total} (critical=${v.critical ?? 0}, high=${v.high ?? 0}, moderate=${v.moderate ?? 0}, low=${v.low ?? 0}, info=${v.info ?? 0}) deps=${r.dependencies}`,
151
+ )
152
+ }
153
+
154
+ if (results.length === 0) {
155
+ console.log('[fleet-vulns] No repositories were audited.')
156
+ }
157
+
158
+ if (hasFailures) {
159
+ process.exit(1)
160
+ }
161
+ if (hasVulnerabilities) {
162
+ process.exit(1)
163
+ }
164
+ process.exit(0)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Checks vulnerabilities for dependency policy entries by projecting policy data
4
- * into a temporary package.json and running `pnpm audit` there.
3
+ * Checks vulnerabilities for all dependency policy entries together by projecting
4
+ * policy data into one temporary package.json and running `pnpm audit` there.
5
5
  *
6
6
  * Usage:
7
7
  * pnpm exec fg-devkit policy-vulns [policy-file]
@@ -22,6 +22,7 @@ function readJson(p) {
22
22
  }
23
23
 
24
24
  const policy = readJson(policyPath)
25
+ console.log(`[policy-vulns] policy=${policyPath}`)
25
26
  const deps = {
26
27
  ...(policy.managedDependencies?.dependencies ?? {}),
27
28
  ...(policy.managedAnywhere ?? {}),
@@ -48,7 +49,6 @@ fs.writeFileSync(
48
49
  'utf8',
49
50
  )
50
51
 
51
- console.log(`[policy-vulns] policy=${policyPath}`)
52
52
  const install = spawnSync('pnpm', ['install', '--ignore-scripts'], {
53
53
  cwd: tmpDir,
54
54
  stdio: 'inherit',
@@ -71,8 +71,4 @@ if (audit.error) {
71
71
  console.error('[policy-vulns]', audit.error.message)
72
72
  process.exit(1)
73
73
  }
74
- // pnpm audit returns exit code 1 when vulnerabilities are found.
75
- if ((audit.status ?? 1) === 1) {
76
- process.exit(0)
77
- }
78
74
  process.exit(audit.status ?? 1)
@@ -39,6 +39,15 @@ const policyPath = process.argv[3]
39
39
  ? path.resolve(process.cwd(), process.argv[3])
40
40
  : path.join(fgDevkitRoot, 'configs/dependency-policy.vue-workspace.json')
41
41
 
42
+ let existingPolicy = {}
43
+ try {
44
+ if (fs.existsSync(policyPath)) {
45
+ existingPolicy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
46
+ }
47
+ } catch {
48
+ existingPolicy = {}
49
+ }
50
+
42
51
  /** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
43
52
  /** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
44
53
  const SKIP_DIR_PREFIXES = []
@@ -180,6 +189,7 @@ const policy = {
180
189
  devDependencies: sortRecord(outDev),
181
190
  },
182
191
  managedAnywhere: sortRecord(outAny),
192
+ overrides: sortRecord(existingPolicy.overrides ?? {}),
183
193
  }
184
194
 
185
195
  const policyErrors = [
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Checks vulnerabilities for the full dependency graph in the current repository
4
+ * by running `pnpm audit` in the working directory.
5
+ *
6
+ * Usage:
7
+ * pnpm exec fg-devkit repo-vulns
8
+ */
9
+
10
+ import { spawnSync } from 'node:child_process'
11
+
12
+ console.log(`[repo-vulns] cwd=${process.cwd()}`)
13
+ const audit = spawnSync('pnpm', ['audit'], {
14
+ cwd: process.cwd(),
15
+ stdio: 'inherit',
16
+ env: process.env,
17
+ })
18
+ if (audit.error) {
19
+ console.error('[repo-vulns]', audit.error.message)
20
+ process.exit(1)
21
+ }
22
+ // pnpm audit returns exit code 1 when vulnerabilities are found.
23
+ if ((audit.status ?? 1) === 1) {
24
+ process.exit(0)
25
+ }
26
+ process.exit(audit.status ?? 1)
@@ -16,8 +16,6 @@ const REQUIRED_SCRIPTS = {
16
16
  'lint:ts': 'pnpm exec fgd typecheck',
17
17
  lint: 'pnpm exec fgd lint --fix .',
18
18
  'lint-check': 'pnpm exec fgd lint .',
19
- 'sync:vscode:settings': 'pnpm exec fgd sync-vscode-settings',
20
- 'create:release': 'pnpm exec fgd create-release',
21
19
  }
22
20
 
23
21
  function main() {
@@ -20,6 +20,7 @@ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mj
20
20
 
21
21
  const cwd = process.cwd()
22
22
  const pkgPath = path.resolve(cwd, 'package.json')
23
+ const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
23
24
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
24
25
  const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
25
26
 
@@ -43,7 +44,6 @@ const policyErrors = [
43
44
  collectPinViolations(`managedDependencies.${section}.`, deps),
44
45
  ),
45
46
  ...collectPinViolations('managedAnywhere.', policy.managedAnywhere ?? {}),
46
- ...collectPinViolations('overrides.', policy.overrides ?? {}),
47
47
  ]
48
48
 
49
49
  if (policyErrors.length > 0) {
@@ -57,6 +57,31 @@ if (policyErrors.length > 0) {
57
57
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
58
58
  const changes = []
59
59
 
60
+ function getOverridePackageName(overrideKey) {
61
+ const match = overrideKey.match(/^(@[^/]+\/[^@]+|[^@]+)@/)
62
+ return match ? match[1] : overrideKey
63
+ }
64
+
65
+ function getRelevantPolicyOverrides(policyOverrides) {
66
+ const entries = Object.entries(policyOverrides ?? {})
67
+ if (entries.length === 0) {
68
+ return {}
69
+ }
70
+ if (!fs.existsSync(lockfilePath)) {
71
+ return Object.fromEntries(entries)
72
+ }
73
+ const lockfile = fs.readFileSync(lockfilePath, 'utf8')
74
+ const relevant = {}
75
+ for (const [name, version] of entries) {
76
+ const pkgName = getOverridePackageName(name)
77
+ // Conservative: only apply overrides for packages present in lockfile.
78
+ if (lockfile.includes(`${pkgName}@`) || lockfile.includes(`/${pkgName}/`)) {
79
+ relevant[name] = version
80
+ }
81
+ }
82
+ return relevant
83
+ }
84
+
60
85
  // Bump versions for managed dependencies / devDependencies where the key already exists.
61
86
  for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
62
87
  pkg[section] ||= {}
@@ -104,7 +129,7 @@ pkg.pnpm ||= {}
104
129
  pkg.pnpm.overrides ||= {}
105
130
 
106
131
  // Apply policy-defined override versions when present in the policy.
107
- for (const [name, version] of Object.entries(policy.overrides ?? {})) {
132
+ for (const [name, version] of Object.entries(getRelevantPolicyOverrides(policy.overrides ?? {}))) {
108
133
  if (pkg.pnpm.overrides[name] !== version) {
109
134
  changes.push(`pnpm.overrides.${name}: ${pkg.pnpm.overrides[name] ?? '(missing)'} -> ${version}`)
110
135
  pkg.pnpm.overrides[name] = version