fg-devkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/core-coding-standards.mdc +203 -0
- package/LICENSE +19 -0
- package/README.md +13 -0
- package/configs/dependency-policy.vue-workspace.json +116 -0
- package/configs/eslint.config.base.js +262 -0
- package/configs/eslint.sync-stub.js +19 -0
- package/configs/prettier.config.js +16 -0
- package/configs/tsconfig.app.sync.json +38 -0
- package/configs/tsconfig.base.json +18 -0
- package/configs/vscode-settings.json +74 -0
- package/package.json +67 -0
- package/scripts/check-dependency-policy.mjs +135 -0
- package/scripts/cz/.cz-config.cjs +26 -0
- package/scripts/cz/cz-adapter.cjs +116 -0
- package/scripts/dependency-policy-pins.mjs +35 -0
- package/scripts/fg-devkit.mjs +247 -0
- package/scripts/refresh-dependency-policy-from-repos.mjs +191 -0
- package/scripts/release.sh +72 -0
- package/scripts/resolve-dependency-policy-path.mjs +32 -0
- package/scripts/sync-dependency-policy.mjs +123 -0
- package/scripts/sync-peers-from-dev.mjs +31 -0
- package/scripts/sync-project-configs.mjs +32 -0
- package/scripts/sync-tooling-configs.mjs +62 -0
- package/scripts/sync-vscode-settings.mjs +39 -0
- package/scripts/vuln-commit.mjs +142 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* fg-devkit CLI: dependency policy, editor/tooling config sync, and VS Code settings.
|
|
4
|
+
*
|
|
5
|
+
* Non-interactive (for scripts and automation):
|
|
6
|
+
* pnpm exec fg-devkit check-deps [policy-file]
|
|
7
|
+
* pnpm exec fg-devkit sync-deps [policy-file]
|
|
8
|
+
* pnpm exec fg-devkit sync-project-configs (VS Code + Prettier + ESLint stub + tsconfig app template)
|
|
9
|
+
* pnpm exec fg-devkit sync-tooling-configs (Prettier + ESLint stub + tsconfig app template only)
|
|
10
|
+
* pnpm exec fg-devkit sync-vscode-settings
|
|
11
|
+
* pnpm exec fg-devkit create-release [patch|minor|major]
|
|
12
|
+
* pnpm exec fg-devkit commit [cz-args...] (Commitizen; uses package.json config.commitizen.path)
|
|
13
|
+
* pnpm exec fg-devkit commit-vuln (staged package.json version bumps → vuln() commit)
|
|
14
|
+
* Interactive menu (no subcommand):
|
|
15
|
+
* pnpm exec fg-devkit
|
|
16
|
+
*
|
|
17
|
+
* Maintainer-only policy regeneration is not exposed here; run
|
|
18
|
+
* node node_modules/fg-devkit/scripts/refresh-dependency-policy-from-repos.mjs from a checkout when needed.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawnSync } from 'node:child_process'
|
|
22
|
+
import fs from 'node:fs'
|
|
23
|
+
import readline from 'node:readline/promises'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
27
|
+
|
|
28
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
|
|
30
|
+
/** Run another script in this package via Node; inherits stdio and returns the child exit code. */
|
|
31
|
+
function runNode(scriptRelative, args = [], options = {}) {
|
|
32
|
+
const scriptPath = path.join(scriptDir, scriptRelative)
|
|
33
|
+
if (!fs.existsSync(scriptPath)) {
|
|
34
|
+
console.error(`[fg-devkit] Missing script: ${scriptPath}`)
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
const r = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
cwd: options.cwd ?? process.cwd(),
|
|
40
|
+
env: process.env,
|
|
41
|
+
})
|
|
42
|
+
if (r.error) {
|
|
43
|
+
console.error('[fg-devkit]', r.error.message)
|
|
44
|
+
return 1
|
|
45
|
+
}
|
|
46
|
+
return r.status ?? 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run release.sh (pnpm version bump, git push, gh release). Uses consumer cwd.
|
|
51
|
+
* Optional first arg: patch | minor | major (otherwise the script prompts interactively).
|
|
52
|
+
*/
|
|
53
|
+
function runReleaseScript(args = []) {
|
|
54
|
+
const shPath = path.join(scriptDir, 'release.sh')
|
|
55
|
+
if (!fs.existsSync(shPath)) {
|
|
56
|
+
console.error(`[fg-devkit] Missing release.sh: ${shPath}`)
|
|
57
|
+
return 1
|
|
58
|
+
}
|
|
59
|
+
const r = spawnSync('bash', [shPath, ...args], {
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
cwd: process.cwd(),
|
|
62
|
+
env: process.env,
|
|
63
|
+
})
|
|
64
|
+
if (r.error) {
|
|
65
|
+
console.error('[fg-devkit]', r.error.message)
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
return r.status ?? 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run Commitizen in the consumer repo (reads config.commitizen.path from package.json).
|
|
73
|
+
* Tries pnpm exec cz, then pnpm exec git-cz, then npx commitizen git-cz.
|
|
74
|
+
*/
|
|
75
|
+
function runCommitizen(args = []) {
|
|
76
|
+
const cwd = process.cwd()
|
|
77
|
+
const attempts = [
|
|
78
|
+
() => spawnSync('pnpm', ['exec', 'cz', ...args], { stdio: 'inherit', cwd, env: process.env }),
|
|
79
|
+
() => spawnSync('pnpm', ['exec', 'git-cz', ...args], { stdio: 'inherit', cwd, env: process.env }),
|
|
80
|
+
() =>
|
|
81
|
+
spawnSync('npx', ['--yes', 'commitizen', 'git-cz', ...args], {
|
|
82
|
+
stdio: 'inherit',
|
|
83
|
+
cwd,
|
|
84
|
+
env: process.env,
|
|
85
|
+
}),
|
|
86
|
+
]
|
|
87
|
+
for (const run of attempts) {
|
|
88
|
+
const r = run()
|
|
89
|
+
if (r.error) {
|
|
90
|
+
if (r.error.code === 'ENOENT') {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
console.error('[fg-devkit]', r.error.message)
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
return r.status ?? 1
|
|
97
|
+
}
|
|
98
|
+
console.error(
|
|
99
|
+
'[fg-devkit] Commitizen not found. Add devDependency "commitizen", set config.commitizen.path, then retry.',
|
|
100
|
+
)
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Map subcommand name → script file (extra argv after subcommand is forwarded). */
|
|
105
|
+
const DIRECT_COMMANDS = {
|
|
106
|
+
'check-deps': 'check-dependency-policy.mjs',
|
|
107
|
+
'sync-deps': 'sync-dependency-policy.mjs',
|
|
108
|
+
'sync-project-configs': 'sync-project-configs.mjs',
|
|
109
|
+
'sync-tooling-configs': 'sync-tooling-configs.mjs',
|
|
110
|
+
'sync-vscode-settings': 'sync-vscode-settings.mjs',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printHelp() {
|
|
114
|
+
console.log(`Usage:
|
|
115
|
+
pnpm exec fg-devkit [command] [args...]
|
|
116
|
+
|
|
117
|
+
Commands:
|
|
118
|
+
check-deps [policy-file] Validate package.json + pins against policy
|
|
119
|
+
sync-deps [policy-file] Align declared versions to policy (existing keys only)
|
|
120
|
+
sync-project-configs VS Code settings + Prettier + ESLint + tsconfig (app template)
|
|
121
|
+
sync-tooling-configs Prettier + ESLint + tsconfig (app template; no VS Code)
|
|
122
|
+
sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
|
|
123
|
+
create-release [bump] Version bump, git push, GitHub release (patch|minor|major; omit = prompt)
|
|
124
|
+
(alias: createRelease)
|
|
125
|
+
commit [args...] Commitizen (git-cz); uses config.commitizen.path in package.json
|
|
126
|
+
commit-vuln Commit staged dependency bumps as vuln() (alias: vuln-commit)
|
|
127
|
+
|
|
128
|
+
With no command, opens an interactive menu.
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
-h, --help Show this help`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Numbered actions shown in the interactive menu (keys must match user input). */
|
|
135
|
+
const MENU = [
|
|
136
|
+
{
|
|
137
|
+
key: '1',
|
|
138
|
+
label: 'Check dependency policy against package.json (check-deps)',
|
|
139
|
+
run: () => runNode('check-dependency-policy.mjs'),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: '2',
|
|
143
|
+
label: 'Sync dependency versions from policy (sync-deps)',
|
|
144
|
+
run: () => runNode('sync-dependency-policy.mjs'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: '3',
|
|
148
|
+
label: 'Sync project configs (VS Code + Prettier + ESLint + tsconfig app template)',
|
|
149
|
+
run: () => runNode('sync-project-configs.mjs'),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: '4',
|
|
153
|
+
label: 'Create release (version bump, push, gh release — run from package repo root)',
|
|
154
|
+
run: () => runReleaseScript(),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: '5',
|
|
158
|
+
label: 'Commit (Commitizen — interactive conventional commit)',
|
|
159
|
+
run: () => runCommitizen(),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: '6',
|
|
163
|
+
label: 'Commit vulnerability bumps (staged package.json → vuln() message)',
|
|
164
|
+
run: () => runNode('vuln-commit.mjs'),
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
async function runInteractiveMenu() {
|
|
169
|
+
const rl = readline.createInterface({ input, output })
|
|
170
|
+
|
|
171
|
+
console.log(`\nfg-devkit — choose a task (cwd: ${process.cwd()})\n`)
|
|
172
|
+
|
|
173
|
+
for (const item of MENU) {
|
|
174
|
+
console.log(` ${item.key}) ${item.label}`)
|
|
175
|
+
}
|
|
176
|
+
console.log(' 0) Exit\n')
|
|
177
|
+
|
|
178
|
+
const choice = (await rl.question('Number: ')).trim()
|
|
179
|
+
|
|
180
|
+
if (choice === '0' || choice === '') {
|
|
181
|
+
rl.close()
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const item = MENU.find((m) => m.key === choice)
|
|
186
|
+
if (!item) {
|
|
187
|
+
console.error('[fg-devkit] Unknown option. Run again and choose a number from the list.')
|
|
188
|
+
rl.close()
|
|
189
|
+
return 1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let exitCode = 0
|
|
193
|
+
try {
|
|
194
|
+
const out = item.run()
|
|
195
|
+
if (typeof out === 'object' && out !== null && typeof out.then === 'function') {
|
|
196
|
+
exitCode = await out
|
|
197
|
+
} else if (typeof out === 'number') {
|
|
198
|
+
exitCode = out
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
rl.close()
|
|
202
|
+
}
|
|
203
|
+
return exitCode
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function main() {
|
|
207
|
+
const [, , sub, ...rest] = process.argv
|
|
208
|
+
|
|
209
|
+
if (sub === '--help' || sub === '-h') {
|
|
210
|
+
printHelp()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (sub) {
|
|
215
|
+
if (sub === 'create-release' || sub === 'createRelease') {
|
|
216
|
+
process.exitCode = runReleaseScript(rest)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
if (sub === 'commit') {
|
|
220
|
+
process.exitCode = runCommitizen(rest)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (sub === 'commit-vuln' || sub === 'vuln-commit' || sub === 'commit:vuln') {
|
|
224
|
+
process.exitCode = runNode('vuln-commit.mjs', rest)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
const script = DIRECT_COMMANDS[sub]
|
|
228
|
+
if (!script) {
|
|
229
|
+
console.error(`[fg-devkit] Unknown command: ${sub}`)
|
|
230
|
+
console.error('[fg-devkit] Run with --help for usage.')
|
|
231
|
+
process.exitCode = 1
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
process.exitCode = runNode(script, rest)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const code = await runInteractiveMenu()
|
|
239
|
+
if (code) {
|
|
240
|
+
process.exitCode = code
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main().catch((err) => {
|
|
245
|
+
console.error('[fg-devkit]', err)
|
|
246
|
+
process.exitCode = 1
|
|
247
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Rebuilds dependency-policy.vue-workspace.json from the union of all npm packages
|
|
4
|
+
* declared in Vue projects under a given projects root (not limited to keys that
|
|
5
|
+
* already exist in the policy).
|
|
6
|
+
*
|
|
7
|
+
* Classification:
|
|
8
|
+
* - Package appears only as dependencies everywhere → managedDependencies.dependencies
|
|
9
|
+
* - Package appears only as devDependencies → managedDependencies.devDependencies
|
|
10
|
+
* - Package appears as both (across or within repos) → managedAnywhere
|
|
11
|
+
*
|
|
12
|
+
* Version per package: highest semver among all parseable version strings (ignores
|
|
13
|
+
* file:, workspace:, link:). Packages with no parseable semver are skipped (e.g. only file:).
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - A package.json is considered a Vue project only if it lists "vue" in dependencies or devDependencies.
|
|
17
|
+
*
|
|
18
|
+
* Runs entirely on the local filesystem (no registry or network).
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node scripts/refresh-dependency-policy-from-repos.mjs [projectsRoot] [policyFile]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs'
|
|
25
|
+
import path from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
|
|
28
|
+
import { collectPinViolations } from './dependency-policy-pins.mjs'
|
|
29
|
+
|
|
30
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
const fgDevkitRoot = path.resolve(scriptDir, '..')
|
|
32
|
+
|
|
33
|
+
const projectsRoot = process.argv[2]
|
|
34
|
+
? path.resolve(process.cwd(), process.argv[2])
|
|
35
|
+
: path.resolve(scriptDir, '../..')
|
|
36
|
+
|
|
37
|
+
const policyPath = process.argv[3]
|
|
38
|
+
? path.resolve(process.cwd(), process.argv[3])
|
|
39
|
+
: path.join(fgDevkitRoot, 'configs/dependency-policy.vue-workspace.json')
|
|
40
|
+
|
|
41
|
+
/** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
|
|
42
|
+
/** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
|
|
43
|
+
const SKIP_DIR_PREFIXES = []
|
|
44
|
+
|
|
45
|
+
/** Recursively collect every package.json path under dir, skipping node_modules and .git. */
|
|
46
|
+
function walk(dir, acc = []) {
|
|
47
|
+
for (const name of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
if (name.name === 'node_modules' || name.name === '.git') continue
|
|
49
|
+
const p = path.join(dir, name.name)
|
|
50
|
+
if (name.isDirectory()) walk(p, acc)
|
|
51
|
+
else if (name.name === 'package.json') acc.push(p)
|
|
52
|
+
}
|
|
53
|
+
return acc
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shouldSkipPackageJson(relDir) {
|
|
57
|
+
for (const prefix of SKIP_DIR_PREFIXES) {
|
|
58
|
+
if (relDir === prefix || relDir.startsWith(`${prefix}${path.sep}`)) return true
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isVueProject(j) {
|
|
64
|
+
const d = { ...j.dependencies, ...j.devDependencies }
|
|
65
|
+
return Boolean(d.vue)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a version range string into [major, minor, patch] for comparison, or null if not usable.
|
|
70
|
+
* Strips common range prefixes; returns null for file/workspace/link/local paths.
|
|
71
|
+
*/
|
|
72
|
+
function coerceParts(spec) {
|
|
73
|
+
if (typeof spec !== 'string') return null
|
|
74
|
+
const s = spec.trim()
|
|
75
|
+
if (/^(file:|workspace:|link:|\.\/|\.\.\/)/.test(s)) return null
|
|
76
|
+
let t = s.replace(/^>=|^<=|^>|^</, '').trim()
|
|
77
|
+
t = t.replace(/^[\^~]/, '')
|
|
78
|
+
const base = t.split(/[-+]/)[0]
|
|
79
|
+
let m = base.match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
80
|
+
if (m) return [+m[1], +m[2], +m[3]]
|
|
81
|
+
m = base.match(/^(\d+)\.(\d+)$/)
|
|
82
|
+
if (m) return [+m[1], +m[2], 0]
|
|
83
|
+
m = base.match(/^(\d+)$/)
|
|
84
|
+
if (m) return [+m[0], 0, 0]
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Lexicographic semver compare on numeric [major, minor, patch] tuples. */
|
|
89
|
+
function compareParts(a, b) {
|
|
90
|
+
for (let i = 0; i < 3; i++) {
|
|
91
|
+
if (a[i] !== b[i]) return a[i] - b[i]
|
|
92
|
+
}
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Stable sort keys alphabetically for readable JSON diffs. */
|
|
97
|
+
function sortRecord(obj) {
|
|
98
|
+
return Object.keys(obj)
|
|
99
|
+
.sort((a, b) => a.localeCompare(b))
|
|
100
|
+
.reduce((o, k) => {
|
|
101
|
+
o[k] = obj[k]
|
|
102
|
+
return o
|
|
103
|
+
}, {})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pkgPaths = walk(projectsRoot).filter((pkgPath) => {
|
|
107
|
+
const rel = path.relative(projectsRoot, path.dirname(pkgPath))
|
|
108
|
+
return !shouldSkipPackageJson(rel || '.')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Per package name: whether it appears in dependencies and/or devDependencies, and all parseable semver tuples.
|
|
113
|
+
* @type {Map<string, { hasDep: boolean, hasDev: boolean, partsList: number[][] }>}
|
|
114
|
+
*/
|
|
115
|
+
const usage = new Map()
|
|
116
|
+
|
|
117
|
+
// Aggregate usage across all Vue package.json files under projectsRoot.
|
|
118
|
+
for (const pkgPath of pkgPaths) {
|
|
119
|
+
let j
|
|
120
|
+
try {
|
|
121
|
+
j = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
122
|
+
} catch {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
if (!isVueProject(j)) continue
|
|
126
|
+
|
|
127
|
+
const deps = j.dependencies ?? {}
|
|
128
|
+
const devs = j.devDependencies ?? {}
|
|
129
|
+
const names = new Set([...Object.keys(deps), ...Object.keys(devs)])
|
|
130
|
+
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
if (!usage.has(name)) {
|
|
133
|
+
usage.set(name, { hasDep: false, hasDev: false, partsList: [] })
|
|
134
|
+
}
|
|
135
|
+
const u = usage.get(name)
|
|
136
|
+
if (deps[name] !== undefined) u.hasDep = true
|
|
137
|
+
if (devs[name] !== undefined) u.hasDev = true
|
|
138
|
+
const v = deps[name] ?? devs[name]
|
|
139
|
+
const parts = coerceParts(v)
|
|
140
|
+
if (parts) u.partsList.push(parts)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const outDeps = {}
|
|
145
|
+
const outDev = {}
|
|
146
|
+
const outAny = {}
|
|
147
|
+
|
|
148
|
+
// Pick highest semver per package and assign to dependencies, devDependencies, or managedAnywhere.
|
|
149
|
+
for (const [name, u] of usage) {
|
|
150
|
+
if (u.partsList.length === 0) {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
let best = u.partsList[0]
|
|
154
|
+
for (const p of u.partsList) {
|
|
155
|
+
if (compareParts(p, best) > 0) best = p
|
|
156
|
+
}
|
|
157
|
+
const pinned = `${best[0]}.${best[1]}.${best[2]}`
|
|
158
|
+
const mixed = u.hasDep && u.hasDev
|
|
159
|
+
if (mixed) outAny[name] = pinned
|
|
160
|
+
else if (u.hasDep) outDeps[name] = pinned
|
|
161
|
+
else outDev[name] = pinned
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const policy = {
|
|
165
|
+
managedDependencies: {
|
|
166
|
+
dependencies: sortRecord(outDeps),
|
|
167
|
+
devDependencies: sortRecord(outDev),
|
|
168
|
+
},
|
|
169
|
+
managedAnywhere: sortRecord(outAny),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const policyErrors = [
|
|
173
|
+
...Object.entries(policy.managedDependencies).flatMap(([section, deps]) =>
|
|
174
|
+
collectPinViolations(`managedDependencies.${section}.`, deps),
|
|
175
|
+
),
|
|
176
|
+
...collectPinViolations('managedAnywhere.', policy.managedAnywhere),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
if (policyErrors.length > 0) {
|
|
180
|
+
console.error('[refresh-policy] Generated policy has invalid pins:')
|
|
181
|
+
for (const e of policyErrors) console.error(`- ${e}`)
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fs.writeFileSync(policyPath, `${JSON.stringify(policy, null, 2)}\n`)
|
|
186
|
+
|
|
187
|
+
console.log(`[refresh-policy] projectsRoot=${projectsRoot}`)
|
|
188
|
+
console.log(`[refresh-policy] policy=${policyPath}`)
|
|
189
|
+
console.log(
|
|
190
|
+
`[refresh-policy] wrote union: ${Object.keys(outDeps).length} dependencies, ${Object.keys(outDev).length} devDependencies, ${Object.keys(outAny).length} managedAnywhere (${usage.size} package names seen)`,
|
|
191
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Interactive or scripted release for this package: bumps version with pnpm, pushes commits/tags,
|
|
4
|
+
# and creates a GitHub Release with generated notes (requires gh CLI, authenticated).
|
|
5
|
+
#
|
|
6
|
+
# Usage: ./scripts/release.sh [major|minor|patch]
|
|
7
|
+
# With no argument, prompts for patch/minor/major.
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
bump_type="${1:-}"
|
|
12
|
+
|
|
13
|
+
if [[ -z "$bump_type" ]]; then
|
|
14
|
+
echo "Select release type:"
|
|
15
|
+
echo " 1) patch"
|
|
16
|
+
echo " 2) minor"
|
|
17
|
+
echo " 3) major"
|
|
18
|
+
read -r -p "Enter choice [1-3] (default: 1): " release_choice
|
|
19
|
+
release_choice="${release_choice:-1}"
|
|
20
|
+
case "$release_choice" in
|
|
21
|
+
1) bump_type="patch" ;;
|
|
22
|
+
2) bump_type="minor" ;;
|
|
23
|
+
3) bump_type="major" ;;
|
|
24
|
+
*)
|
|
25
|
+
echo "Invalid choice: $release_choice"
|
|
26
|
+
exit 1
|
|
27
|
+
;;
|
|
28
|
+
esac
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [[ "$bump_type" != "major" && "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then
|
|
32
|
+
echo "Usage: ./scripts/release.sh [major|minor|patch]"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
37
|
+
echo "GitHub CLI (gh) is required but not installed."
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
42
|
+
echo "GitHub CLI is not authenticated. Run: gh auth login"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
47
|
+
echo "Git working tree is not clean. Commit or stash changes first."
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
previous_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
|
52
|
+
|
|
53
|
+
pnpm version "$bump_type"
|
|
54
|
+
|
|
55
|
+
new_version="$(node -p "require('./package.json').version")"
|
|
56
|
+
new_tag="v${new_version}"
|
|
57
|
+
|
|
58
|
+
git push
|
|
59
|
+
git push --tags
|
|
60
|
+
|
|
61
|
+
if [[ -n "$previous_tag" ]]; then
|
|
62
|
+
gh release create "$new_tag" \
|
|
63
|
+
--latest \
|
|
64
|
+
--generate-notes \
|
|
65
|
+
--notes-start-tag "$previous_tag"
|
|
66
|
+
else
|
|
67
|
+
gh release create "$new_tag" \
|
|
68
|
+
--latest \
|
|
69
|
+
--generate-notes
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
echo "Release created: ${new_tag}"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves which dependency policy JSON file check/sync scripts should load.
|
|
3
|
+
*
|
|
4
|
+
* Used by check-dependency-policy.mjs and sync-dependency-policy.mjs so both tools share one rule:
|
|
5
|
+
* optional argv[2] override, otherwise the packaged default next to this package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_POLICY = 'dependency-policy.vue-workspace.json'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* First CLI argument selects the policy file (optional).
|
|
15
|
+
* - Omitted: fg-devkit/configs/dependency-policy.vue-workspace.json
|
|
16
|
+
* - Absolute path: used as-is
|
|
17
|
+
* - Relative path: if it exists under cwd, use it; else fg-devkit/configs/<arg>
|
|
18
|
+
*/
|
|
19
|
+
export function resolveDependencyPolicyPath(scriptDir, cwd, argv) {
|
|
20
|
+
const arg = argv[2]
|
|
21
|
+
if (!arg) {
|
|
22
|
+
return path.resolve(scriptDir, '../configs', DEFAULT_POLICY)
|
|
23
|
+
}
|
|
24
|
+
if (path.isAbsolute(arg)) {
|
|
25
|
+
return arg
|
|
26
|
+
}
|
|
27
|
+
const fromCwd = path.resolve(cwd, arg)
|
|
28
|
+
if (fs.existsSync(fromCwd)) {
|
|
29
|
+
return fromCwd
|
|
30
|
+
}
|
|
31
|
+
return path.resolve(scriptDir, '../configs', arg)
|
|
32
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Rewrites version fields in the current package.json so they match the shared dependency policy.
|
|
4
|
+
*
|
|
5
|
+
* Only updates keys that already exist in package.json (does not add new dependencies from the policy).
|
|
6
|
+
* Aligns managedDependencies, managedAnywhere (only where the package is already declared), optional
|
|
7
|
+
* policy overrides into pnpm.overrides, and logs each change.
|
|
8
|
+
*
|
|
9
|
+
* Refuses to run if the policy file contains any non-pinned versions.
|
|
10
|
+
*
|
|
11
|
+
* Policy path: first CLI argument, or default under fg-devkit/configs/.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
import { collectPinViolations } from './dependency-policy-pins.mjs'
|
|
19
|
+
import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
|
|
20
|
+
|
|
21
|
+
const cwd = process.cwd()
|
|
22
|
+
const pkgPath = path.resolve(cwd, 'package.json')
|
|
23
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(pkgPath)) {
|
|
27
|
+
console.error('[sync-deps] Could not find package.json in current working directory')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(policyPath)) {
|
|
32
|
+
console.error(`[sync-deps] Could not find dependency policy: ${policyPath}`)
|
|
33
|
+
console.error(
|
|
34
|
+
'[sync-deps] Usage: node …/sync-dependency-policy.mjs [policy-file]\n' +
|
|
35
|
+
' policy-file: absolute path, path relative to cwd, or filename under fg-devkit/configs/ (default: dependency-policy.vue-workspace.json)',
|
|
36
|
+
)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
|
|
41
|
+
const policyErrors = [
|
|
42
|
+
...Object.entries(policy.managedDependencies ?? {}).flatMap(([section, deps]) =>
|
|
43
|
+
collectPinViolations(`managedDependencies.${section}.`, deps),
|
|
44
|
+
),
|
|
45
|
+
...collectPinViolations('managedAnywhere.', policy.managedAnywhere ?? {}),
|
|
46
|
+
...collectPinViolations('overrides.', policy.overrides ?? {}),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
if (policyErrors.length > 0) {
|
|
50
|
+
console.error('[sync-deps] Policy file contains non-pinned versions:')
|
|
51
|
+
for (const err of policyErrors) {
|
|
52
|
+
console.error(`- ${err}`)
|
|
53
|
+
}
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
58
|
+
const changes = []
|
|
59
|
+
|
|
60
|
+
// Bump versions for managed dependencies / devDependencies where the key already exists.
|
|
61
|
+
for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
|
|
62
|
+
pkg[section] ||= {}
|
|
63
|
+
|
|
64
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
65
|
+
if (pkg[section][name] === undefined) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (pkg[section][name] !== version) {
|
|
69
|
+
changes.push(`${section}.${name}: ${pkg[section][name]} -> ${version}`)
|
|
70
|
+
pkg[section][name] = version
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Same for packages that may live in either dependencies or devDependencies.
|
|
76
|
+
for (const [name, version] of Object.entries(policy.managedAnywhere ?? {})) {
|
|
77
|
+
pkg.dependencies ||= {}
|
|
78
|
+
pkg.devDependencies ||= {}
|
|
79
|
+
const inDep = pkg.dependencies[name] !== undefined
|
|
80
|
+
const inDev = pkg.devDependencies[name] !== undefined
|
|
81
|
+
if (inDep && inDev) {
|
|
82
|
+
if (pkg.dependencies[name] !== version) {
|
|
83
|
+
changes.push(`dependencies.${name}: ${pkg.dependencies[name]} -> ${version}`)
|
|
84
|
+
pkg.dependencies[name] = version
|
|
85
|
+
}
|
|
86
|
+
if (pkg.devDependencies[name] !== version) {
|
|
87
|
+
changes.push(`devDependencies.${name}: ${pkg.devDependencies[name]} -> ${version}`)
|
|
88
|
+
pkg.devDependencies[name] = version
|
|
89
|
+
}
|
|
90
|
+
} else if (inDep) {
|
|
91
|
+
if (pkg.dependencies[name] !== version) {
|
|
92
|
+
changes.push(`dependencies.${name}: ${pkg.dependencies[name] ?? '(missing)'} -> ${version}`)
|
|
93
|
+
pkg.dependencies[name] = version
|
|
94
|
+
}
|
|
95
|
+
} else if (inDev) {
|
|
96
|
+
if (pkg.devDependencies[name] !== version) {
|
|
97
|
+
changes.push(`devDependencies.${name}: ${pkg.devDependencies[name] ?? '(missing)'} -> ${version}`)
|
|
98
|
+
pkg.devDependencies[name] = version
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pkg.pnpm ||= {}
|
|
104
|
+
pkg.pnpm.overrides ||= {}
|
|
105
|
+
|
|
106
|
+
// Apply policy-defined override versions when present in the policy.
|
|
107
|
+
for (const [name, version] of Object.entries(policy.overrides ?? {})) {
|
|
108
|
+
if (pkg.pnpm.overrides[name] !== version) {
|
|
109
|
+
changes.push(`pnpm.overrides.${name}: ${pkg.pnpm.overrides[name] ?? '(missing)'} -> ${version}`)
|
|
110
|
+
pkg.pnpm.overrides[name] = version
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (changes.length === 0) {
|
|
115
|
+
console.log('[sync-deps] package.json already matches dependency policy')
|
|
116
|
+
process.exit(0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
120
|
+
console.log('[sync-deps] Updated package.json to match dependency policy')
|
|
121
|
+
for (const change of changes) {
|
|
122
|
+
console.log(`- ${change}`)
|
|
123
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* For library packages: copies versions from devDependencies/dependencies into peerDependencies
|
|
4
|
+
* for every key already listed in peerDependencies, so declared peers stay aligned with local installs.
|
|
5
|
+
*
|
|
6
|
+
* Does not add new peer keys; warns and skips peers that have no matching dev/dependency entry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
|
|
12
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json')
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
14
|
+
|
|
15
|
+
const sources = { ...pkg.devDependencies, ...pkg.dependencies }
|
|
16
|
+
|
|
17
|
+
pkg.peerDependencies = pkg.peerDependencies || {}
|
|
18
|
+
const peerKeys = Object.keys(pkg.peerDependencies)
|
|
19
|
+
|
|
20
|
+
for (const name of peerKeys) {
|
|
21
|
+
const version = sources[name]
|
|
22
|
+
if (version) {
|
|
23
|
+
pkg.peerDependencies[name] = version
|
|
24
|
+
} else {
|
|
25
|
+
// If missing locally, leave existing peer version untouched
|
|
26
|
+
console.warn(`[sync-peers] Skipping ${name} (not found in devDependencies/dependencies)`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
31
|
+
console.warn('[sync-peers] peerDependencies updated from devDependencies')
|