fg-devkit 1.12.5 → 1.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/dependency-policy.vue-workspace.json +40 -34
- package/package.json +25 -9
- package/scaffold/vue/package.json +15 -12
- package/scripts/bump-node-toolchain.mjs +260 -0
- package/scripts/bump-pnpm-toolchain.mjs +65 -28
- package/scripts/check-dependency-policy.mjs +8 -0
- package/scripts/dependency-policy-toolchain.mjs +187 -7
- package/scripts/fg-devkit.mjs +214 -3
- package/scripts/fleet-git-commit-push-scoped.mjs +97 -0
- package/scripts/fleet-git-commit-push.mjs +75 -0
- package/scripts/fleet-git-discard-scoped.mjs +93 -0
- package/scripts/fleet-git-discard.mjs +66 -0
- package/scripts/fleet-git-pull-scoped.mjs +92 -0
- package/scripts/fleet-git-pull.mjs +65 -0
- package/scripts/fleet-project-repos.mjs +173 -0
- package/scripts/fleet-repo-picker.mjs +190 -0
- package/scripts/fleet-vue-repos.mjs +3 -1
- package/scripts/fleet-vulns.mjs +2 -1
- package/scripts/git-commit-push-fleet-common.mjs +147 -0
- package/scripts/git-discard-fleet-common.mjs +100 -0
- package/scripts/git-fleet-common.mjs +120 -0
- package/scripts/git-pull-fleet-common.mjs +90 -0
- package/scripts/node-toolchain-sync.mjs +347 -0
- package/scripts/pnpm-toolchain-sync.mjs +231 -0
- package/scripts/scaffold-vue.mjs +2 -2
- package/scripts/sync-dependency-policy.mjs +47 -15
- package/scripts/sync-node-toolchain.mjs +64 -0
- package/scripts/sync-pnpm-toolchain.mjs +59 -0
- package/scripts/sync-scaffold-vue-deps.mjs +12 -5
- package/scaffold/vue/Dockerfile.local +0 -16
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Node toolchain files in a single repository from policy.toolchain.node.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
deriveJenkinsImageTag,
|
|
10
|
+
normalizeNodeVersionLine,
|
|
11
|
+
resolveDockerNodeTag,
|
|
12
|
+
resolveExpectedNodePolicy,
|
|
13
|
+
} from './dependency-policy-toolchain.mjs'
|
|
14
|
+
|
|
15
|
+
const DOCKER_FROM_NODE_PATTERN = /FROM node:(v)?(\d+(?:\.\d+)?)(-[a-z0-9-]+)?/gi
|
|
16
|
+
const JENKINS_NODE_IMAGE_PATTERN = /(feelgoodsvenska\/jenkins-build:)node\d+/g
|
|
17
|
+
const GITHUB_NODE_VERSION_PATTERN = /node-version:\s*['"]?\d+(?:\.\d+)?['"]?/g
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} dir
|
|
21
|
+
* @returns {string[]}
|
|
22
|
+
*/
|
|
23
|
+
function findGithubWorkflows(dir) {
|
|
24
|
+
const workflowsDir = path.join(dir, '.github', 'workflows')
|
|
25
|
+
if (!fs.existsSync(workflowsDir)) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
return fs
|
|
29
|
+
.readdirSync(workflowsDir, { withFileTypes: true })
|
|
30
|
+
.filter((entry) => entry.isFile() && /\.(ya?ml)$/i.test(entry.name))
|
|
31
|
+
.map((entry) => path.join(workflowsDir, entry.name))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} dir
|
|
36
|
+
* @param {string[]} dockerfileNames
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
function findDockerfiles(dir, dockerfileNames = ['Dockerfile', 'Dockerfile.local']) {
|
|
40
|
+
const found = []
|
|
41
|
+
for (const name of dockerfileNames) {
|
|
42
|
+
const filePath = path.join(dir, name)
|
|
43
|
+
if (fs.existsSync(filePath)) {
|
|
44
|
+
found.push(filePath)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return found
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} dir
|
|
52
|
+
* @returns {string[]}
|
|
53
|
+
*/
|
|
54
|
+
function findPackageJsonFiles(dir) {
|
|
55
|
+
const found = []
|
|
56
|
+
const skipDirNames = new Set([
|
|
57
|
+
'node_modules',
|
|
58
|
+
'.git',
|
|
59
|
+
'.next',
|
|
60
|
+
'dist',
|
|
61
|
+
'build',
|
|
62
|
+
'coverage',
|
|
63
|
+
'storybook-static',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
function walk(currentDir) {
|
|
67
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
if (skipDirNames.has(entry.name) || entry.name.startsWith('.')) {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
walk(path.join(currentDir, entry.name))
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (entry.name === 'package.json') {
|
|
77
|
+
found.push(path.join(currentDir, entry.name))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
walk(dir)
|
|
83
|
+
return found.sort((a, b) => a.localeCompare(b))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} content
|
|
88
|
+
* @param {string} expectedNvmrc
|
|
89
|
+
* @param {string} [dockerTag] e.g. 22-alpine
|
|
90
|
+
* @returns {{ content: string, changed: boolean }}
|
|
91
|
+
*/
|
|
92
|
+
export function syncDockerfileNodeImages(content, expectedNvmrc, dockerTag) {
|
|
93
|
+
const imageTag = resolveDockerNodeTag(expectedNvmrc, dockerTag)
|
|
94
|
+
let changed = false
|
|
95
|
+
|
|
96
|
+
const updated = content.replace(DOCKER_FROM_NODE_PATTERN, (match) => {
|
|
97
|
+
const next = `FROM node:${imageTag}`
|
|
98
|
+
if (next !== match) {
|
|
99
|
+
changed = true
|
|
100
|
+
}
|
|
101
|
+
return next
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return { content: updated, changed }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} content
|
|
109
|
+
* @param {string} jenkinsImageTag e.g. node22
|
|
110
|
+
* @returns {{ content: string, changed: boolean }}
|
|
111
|
+
*/
|
|
112
|
+
export function syncJenkinsfileNodeImage(content, jenkinsImageTag) {
|
|
113
|
+
let changed = false
|
|
114
|
+
const updated = content.replace(JENKINS_NODE_IMAGE_PATTERN, (match, prefix) => {
|
|
115
|
+
const next = `${prefix}${jenkinsImageTag}`
|
|
116
|
+
if (next !== match) {
|
|
117
|
+
changed = true
|
|
118
|
+
}
|
|
119
|
+
return next
|
|
120
|
+
})
|
|
121
|
+
return { content: updated, changed }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} content
|
|
126
|
+
* @param {string} expectedNvmrc
|
|
127
|
+
* @returns {{ content: string, changed: boolean }}
|
|
128
|
+
*/
|
|
129
|
+
export function syncGithubWorkflowNodeVersion(content, expectedNvmrc) {
|
|
130
|
+
const major = expectedNvmrc.split('.')[0]
|
|
131
|
+
let changed = false
|
|
132
|
+
const updated = content.replace(GITHUB_NODE_VERSION_PATTERN, (match) => {
|
|
133
|
+
const next = `node-version: ${major}`
|
|
134
|
+
if (next !== match) {
|
|
135
|
+
changed = true
|
|
136
|
+
}
|
|
137
|
+
return next
|
|
138
|
+
})
|
|
139
|
+
return { content: updated, changed }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} filePath
|
|
144
|
+
* @param {string} expected
|
|
145
|
+
* @param {boolean} dryRun
|
|
146
|
+
* @returns {string | null} change description
|
|
147
|
+
*/
|
|
148
|
+
function writeTextFileIfNeeded(filePath, expected, dryRun) {
|
|
149
|
+
const normalizedExpected = expected.trim()
|
|
150
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : undefined
|
|
151
|
+
const normalizedCurrent = current === undefined ? undefined : normalizeNodeVersionLine(current)
|
|
152
|
+
|
|
153
|
+
if (normalizedCurrent === normalizedExpected) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const rel = path.basename(filePath)
|
|
158
|
+
const description =
|
|
159
|
+
current === undefined
|
|
160
|
+
? `${rel}: (missing) -> ${normalizedExpected}`
|
|
161
|
+
: `${rel}: ${normalizedCurrent} -> ${normalizedExpected}`
|
|
162
|
+
|
|
163
|
+
if (!dryRun) {
|
|
164
|
+
fs.writeFileSync(filePath, `${normalizedExpected}\n`)
|
|
165
|
+
}
|
|
166
|
+
return description
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {string} repoRoot
|
|
171
|
+
* @param {Record<string, unknown>} policy
|
|
172
|
+
* @param {{ dryRun?: boolean, fgDevkitRoot?: string }} [options]
|
|
173
|
+
* @returns {string[]} change descriptions
|
|
174
|
+
*/
|
|
175
|
+
export function syncNodeToolchainInRepo(repoRoot, policy, options = {}) {
|
|
176
|
+
const { dryRun = false, fgDevkitRoot } = options
|
|
177
|
+
const nodePolicy = resolveExpectedNodePolicy(policy, fgDevkitRoot)
|
|
178
|
+
if (!nodePolicy) {
|
|
179
|
+
return []
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const changes = []
|
|
183
|
+
const { nvmrc, engines, jenkinsImage, dockerTag } = nodePolicy
|
|
184
|
+
const rootPackageJsonPath = path.join(repoRoot, 'package.json')
|
|
185
|
+
const hasRootPackageJson = fs.existsSync(rootPackageJsonPath)
|
|
186
|
+
|
|
187
|
+
if (hasRootPackageJson) {
|
|
188
|
+
const nvmrcPath = path.join(repoRoot, '.nvmrc')
|
|
189
|
+
const nvmrcChange = writeTextFileIfNeeded(nvmrcPath, nvmrc, dryRun)
|
|
190
|
+
if (nvmrcChange) {
|
|
191
|
+
changes.push(nvmrcChange)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nodeVersionPath = path.join(repoRoot, '.node-version')
|
|
196
|
+
if (hasRootPackageJson && fs.existsSync(nodeVersionPath)) {
|
|
197
|
+
const nodeVersionChange = writeTextFileIfNeeded(nodeVersionPath, nvmrc, dryRun)
|
|
198
|
+
if (nodeVersionChange) {
|
|
199
|
+
changes.push(nodeVersionChange)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (engines) {
|
|
204
|
+
for (const pkgPath of findPackageJsonFiles(repoRoot)) {
|
|
205
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
206
|
+
const currentEngines = pkg.engines?.node
|
|
207
|
+
if (currentEngines === engines) {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const relPkgPath = path.relative(repoRoot, pkgPath)
|
|
212
|
+
changes.push(`${relPkgPath} engines.node: ${currentEngines ?? '(missing)'} -> ${engines}`)
|
|
213
|
+
if (!dryRun) {
|
|
214
|
+
pkg.engines = { ...(pkg.engines ?? {}), node: engines }
|
|
215
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const dockerfilePath of findDockerfiles(repoRoot)) {
|
|
221
|
+
const dockerfile = fs.readFileSync(dockerfilePath, 'utf8')
|
|
222
|
+
const { content: updatedDockerfile, changed } = syncDockerfileNodeImages(dockerfile, nvmrc, dockerTag)
|
|
223
|
+
if (changed) {
|
|
224
|
+
changes.push(`${path.basename(dockerfilePath)}: node base image -> node:${dockerTag ?? resolveDockerNodeTag(nvmrc)}`)
|
|
225
|
+
if (!dryRun) {
|
|
226
|
+
fs.writeFileSync(dockerfilePath, updatedDockerfile)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const jenkinsfilePath = path.join(repoRoot, 'Jenkinsfile')
|
|
232
|
+
if (fs.existsSync(jenkinsfilePath)) {
|
|
233
|
+
const jenkinsfile = fs.readFileSync(jenkinsfilePath, 'utf8')
|
|
234
|
+
const { content: updatedJenkinsfile, changed } = syncJenkinsfileNodeImage(
|
|
235
|
+
jenkinsfile,
|
|
236
|
+
jenkinsImage ?? deriveJenkinsImageTag(nvmrc),
|
|
237
|
+
)
|
|
238
|
+
if (changed) {
|
|
239
|
+
changes.push(`Jenkinsfile: jenkins-build image -> ${jenkinsImage ?? deriveJenkinsImageTag(nvmrc)}`)
|
|
240
|
+
if (!dryRun) {
|
|
241
|
+
fs.writeFileSync(jenkinsfilePath, updatedJenkinsfile)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const workflowPath of findGithubWorkflows(repoRoot)) {
|
|
247
|
+
const workflow = fs.readFileSync(workflowPath, 'utf8')
|
|
248
|
+
const { content: updatedWorkflow, changed } = syncGithubWorkflowNodeVersion(workflow, nvmrc)
|
|
249
|
+
if (changed) {
|
|
250
|
+
changes.push(
|
|
251
|
+
`${path.relative(repoRoot, workflowPath)}: node-version -> ${nvmrc.split('.')[0]}`,
|
|
252
|
+
)
|
|
253
|
+
if (!dryRun) {
|
|
254
|
+
fs.writeFileSync(workflowPath, updatedWorkflow)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return changes
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string} repoRoot
|
|
264
|
+
* @param {Record<string, unknown>} policy
|
|
265
|
+
* @param {string} [fgDevkitRoot]
|
|
266
|
+
* @returns {string[]}
|
|
267
|
+
*/
|
|
268
|
+
export function collectNodeToolchainViolations(repoRoot, policy, fgDevkitRoot) {
|
|
269
|
+
const nodePolicy = resolveExpectedNodePolicy(policy, fgDevkitRoot)
|
|
270
|
+
if (!nodePolicy) {
|
|
271
|
+
return []
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const violations = []
|
|
275
|
+
const { nvmrc, engines, jenkinsImage, dockerTag } = nodePolicy
|
|
276
|
+
const hasRootPackageJson = fs.existsSync(path.join(repoRoot, 'package.json'))
|
|
277
|
+
|
|
278
|
+
if (hasRootPackageJson) {
|
|
279
|
+
const nvmrcPath = path.join(repoRoot, '.nvmrc')
|
|
280
|
+
if (!fs.existsSync(nvmrcPath)) {
|
|
281
|
+
violations.push(`.nvmrc: expected ${nvmrc}, got (missing)`)
|
|
282
|
+
} else {
|
|
283
|
+
const actual = normalizeNodeVersionLine(fs.readFileSync(nvmrcPath, 'utf8'))
|
|
284
|
+
if (actual !== nvmrc) {
|
|
285
|
+
violations.push(`.nvmrc: expected ${nvmrc}, got ${actual}`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const nodeVersionPath = path.join(repoRoot, '.node-version')
|
|
290
|
+
if (fs.existsSync(nodeVersionPath)) {
|
|
291
|
+
const actual = normalizeNodeVersionLine(fs.readFileSync(nodeVersionPath, 'utf8'))
|
|
292
|
+
if (actual !== nvmrc) {
|
|
293
|
+
violations.push(`.node-version: expected ${nvmrc}, got ${actual}`)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (engines) {
|
|
299
|
+
for (const pkgPath of findPackageJsonFiles(repoRoot)) {
|
|
300
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
301
|
+
const actual = pkg.engines?.node
|
|
302
|
+
if (actual !== engines) {
|
|
303
|
+
violations.push(
|
|
304
|
+
`${path.relative(repoRoot, pkgPath)} engines.node: expected ${engines}, got ${actual ?? '(missing)'}`,
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const dockerfilePath of findDockerfiles(repoRoot)) {
|
|
311
|
+
const dockerfile = fs.readFileSync(dockerfilePath, 'utf8')
|
|
312
|
+
const expectedFrom = `FROM node:${dockerTag ?? resolveDockerNodeTag(nvmrc)}`
|
|
313
|
+
for (const match of dockerfile.matchAll(/FROM node:(v)?(\d+(?:\.\d+)?)(-[a-z0-9-]+)?/gi)) {
|
|
314
|
+
if (match[0] !== expectedFrom) {
|
|
315
|
+
violations.push(
|
|
316
|
+
`${path.basename(dockerfilePath)}: expected ${expectedFrom}, got ${match[0]}`,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const jenkinsfilePath = path.join(repoRoot, 'Jenkinsfile')
|
|
323
|
+
const expectedJenkins = jenkinsImage ?? deriveJenkinsImageTag(nvmrc)
|
|
324
|
+
if (fs.existsSync(jenkinsfilePath)) {
|
|
325
|
+
const jenkinsfile = fs.readFileSync(jenkinsfilePath, 'utf8')
|
|
326
|
+
const matches = [...jenkinsfile.matchAll(/feelgoodsvenska\/jenkins-build:(node\d+)/g)]
|
|
327
|
+
for (const match of matches) {
|
|
328
|
+
if (match[1] !== expectedJenkins) {
|
|
329
|
+
violations.push(`Jenkinsfile: expected jenkins-build:${expectedJenkins}, got jenkins-build:${match[1]}`)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const major = nvmrc.split('.')[0]
|
|
335
|
+
for (const workflowPath of findGithubWorkflows(repoRoot)) {
|
|
336
|
+
const workflow = fs.readFileSync(workflowPath, 'utf8')
|
|
337
|
+
for (const match of workflow.matchAll(/node-version:\s*['"]?(\d+(?:\.\d+)?)['"]?/g)) {
|
|
338
|
+
if (match[1].split('.')[0] !== major) {
|
|
339
|
+
violations.push(
|
|
340
|
+
`${path.relative(repoRoot, workflowPath)}: expected node-version ${major}, got ${match[1]}`,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return violations
|
|
347
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync pnpm packageManager and pnpm@ version pins in a repository.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { extractPnpmVersion } from './dependency-policy-toolchain.mjs'
|
|
9
|
+
|
|
10
|
+
const PNPM_AT_VERSION_PATTERN = /pnpm@\d+\.\d+\.\d+(-[\w.]+)?/g
|
|
11
|
+
const PNPM_AT_LATEST_PATTERN = /pnpm@latest/gi
|
|
12
|
+
|
|
13
|
+
const SKIP_DIR_NAMES = new Set([
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.git',
|
|
16
|
+
'dist',
|
|
17
|
+
'build',
|
|
18
|
+
'.next',
|
|
19
|
+
'coverage',
|
|
20
|
+
'storybook-static',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} content
|
|
25
|
+
* @param {string} pnpmVersion Bare semver (no pnpm@ prefix).
|
|
26
|
+
* @returns {{ content: string, changed: boolean }}
|
|
27
|
+
*/
|
|
28
|
+
export function syncPnpmVersionInContent(content, pnpmVersion) {
|
|
29
|
+
const target = `pnpm@${pnpmVersion}`
|
|
30
|
+
let changed = false
|
|
31
|
+
|
|
32
|
+
let updated = content.replace(PNPM_AT_VERSION_PATTERN, (match) => {
|
|
33
|
+
if (match !== target) {
|
|
34
|
+
changed = true
|
|
35
|
+
}
|
|
36
|
+
return target
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
updated = updated.replace(PNPM_AT_LATEST_PATTERN, () => {
|
|
40
|
+
changed = true
|
|
41
|
+
return target
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return { content: updated, changed }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} content
|
|
49
|
+
* @param {string} pnpmVersion
|
|
50
|
+
* @returns {{ content: string, changed: boolean }}
|
|
51
|
+
*/
|
|
52
|
+
export function syncDockerfilePnpmVersion(content, pnpmVersion) {
|
|
53
|
+
return syncPnpmVersionInContent(content, pnpmVersion)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} filePath
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function shouldScanFileForPnpmPins(filePath) {
|
|
61
|
+
const base = path.basename(filePath)
|
|
62
|
+
if (base === 'package.json' || base === 'pnpm-lock.yaml') {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
if (base === 'Dockerfile' || base === 'Dockerfile.local') {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
if (base === 'Jenkinsfile') {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
filePath.includes(`${path.sep}.github${path.sep}workflows${path.sep}`) &&
|
|
73
|
+
/\.(ya?ml)$/i.test(base)
|
|
74
|
+
) {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
return base.endsWith('.sh')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} dir
|
|
82
|
+
* @param {string[]} acc
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
function walkRepoFiles(dir, acc = []) {
|
|
86
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
87
|
+
if (SKIP_DIR_NAMES.has(item.name)) continue
|
|
88
|
+
const filePath = path.join(dir, item.name)
|
|
89
|
+
if (item.isDirectory()) {
|
|
90
|
+
walkRepoFiles(filePath, acc)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
acc.push(filePath)
|
|
94
|
+
}
|
|
95
|
+
return acc
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} repoDir
|
|
100
|
+
* @param {string} packageManager
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
export function findPnpmVersionMismatchesInRepo(repoDir, packageManager) {
|
|
104
|
+
const pnpmVersion = extractPnpmVersion(packageManager)
|
|
105
|
+
if (!pnpmVersion) {
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const target = `pnpm@${pnpmVersion}`
|
|
110
|
+
const violations = []
|
|
111
|
+
|
|
112
|
+
for (const filePath of walkRepoFiles(repoDir)) {
|
|
113
|
+
const rel = path.relative(repoDir, filePath).split(path.sep).join('/')
|
|
114
|
+
const base = path.basename(filePath)
|
|
115
|
+
|
|
116
|
+
if (base === 'package.json') {
|
|
117
|
+
try {
|
|
118
|
+
const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
119
|
+
if (pkg.packageManager !== undefined && pkg.packageManager !== packageManager) {
|
|
120
|
+
violations.push(`${rel} packageManager: expected ${packageManager}, got ${pkg.packageManager}`)
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore
|
|
124
|
+
}
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!shouldScanFileForPnpmPins(filePath)) {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
133
|
+
if (!/pnpm@(?:\d+\.\d+\.\d+(?:-[\w.]+)?|latest)/i.test(content)) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const match of content.matchAll(/pnpm@(?:\d+\.\d+\.\d+(?:-[\w.]+)?|latest)/gi)) {
|
|
138
|
+
if (match[0] !== target) {
|
|
139
|
+
violations.push(`${rel}: expected ${target}, got ${match[0]}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return violations
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} repoDir
|
|
149
|
+
* @param {string} packageManager e.g. pnpm@11.8.0
|
|
150
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
151
|
+
* @returns {string[]} change descriptions
|
|
152
|
+
*/
|
|
153
|
+
export function syncPnpmToolchainInRepo(repoDir, packageManager, options = {}) {
|
|
154
|
+
const { dryRun = false } = options
|
|
155
|
+
const pnpmVersion = extractPnpmVersion(packageManager)
|
|
156
|
+
if (!pnpmVersion) {
|
|
157
|
+
throw new Error(`Invalid packageManager: ${packageManager}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const changes = []
|
|
161
|
+
|
|
162
|
+
for (const filePath of walkRepoFiles(repoDir)) {
|
|
163
|
+
const rel = path.relative(repoDir, filePath).split(path.sep).join('/')
|
|
164
|
+
const base = path.basename(filePath)
|
|
165
|
+
|
|
166
|
+
if (base === 'package.json') {
|
|
167
|
+
let pkg
|
|
168
|
+
try {
|
|
169
|
+
pkg = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
170
|
+
} catch {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (pkg.packageManager !== undefined && pkg.packageManager !== packageManager) {
|
|
174
|
+
changes.push(`${rel} packageManager: ${pkg.packageManager} -> ${packageManager}`)
|
|
175
|
+
if (!dryRun) {
|
|
176
|
+
pkg.packageManager = packageManager
|
|
177
|
+
fs.writeFileSync(filePath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!shouldScanFileForPnpmPins(filePath)) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
188
|
+
if (!/pnpm@(?:\d+\.\d+\.\d+(?:-[\w.]+)?|latest)/i.test(content)) {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { content: updated, changed } = syncPnpmVersionInContent(content, pnpmVersion)
|
|
193
|
+
if (changed) {
|
|
194
|
+
changes.push(`${rel}: pnpm -> ${pnpmVersion}`)
|
|
195
|
+
if (!dryRun) {
|
|
196
|
+
fs.writeFileSync(filePath, updated)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return changes
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {Record<string, unknown>} pkg
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
export function isVueFgDevkitConsumerPackage(pkg) {
|
|
209
|
+
if (!pkg || pkg.name === 'fg-devkit') {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
const hasVue = Boolean(pkg.dependencies?.vue || pkg.devDependencies?.vue)
|
|
213
|
+
const hasFgDevkit = Boolean(pkg.dependencies?.['fg-devkit'] || pkg.devDependencies?.['fg-devkit'])
|
|
214
|
+
return hasVue && hasFgDevkit
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {string} repoDir
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
export function isVueFgDevkitConsumerRepo(repoDir) {
|
|
222
|
+
const pkgPath = path.join(repoDir, 'package.json')
|
|
223
|
+
if (!fs.existsSync(pkgPath)) {
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
return isVueFgDevkitConsumerPackage(JSON.parse(fs.readFileSync(pkgPath, 'utf8')))
|
|
228
|
+
} catch {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
}
|
package/scripts/scaffold-vue.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* --slug <string> package.json name / slug (default: my-app)
|
|
11
11
|
* --docker-image <string> Docker registry image without tag (default: myorg/my-app-ui)
|
|
12
12
|
* --jenkins-library <string> Jenkins shared library name (default: my-shared-library)
|
|
13
|
-
* --jenkins-node-image <string> CI Docker image for Node (default: node:22-
|
|
13
|
+
* --jenkins-node-image <string> CI Docker image for Node (default: node:22-alpine)
|
|
14
14
|
* --jenkins-docker-label <string> Jenkins label for docker-build agents (default: docker)
|
|
15
15
|
* --jenkins-registry-cred <string> Jenkins credentialsId for Docker registry (default: docker-registry)
|
|
16
16
|
* --jenkins-poeditor-cred <string> Jenkins credentialsId for POEditor API key (default: poeditor_api_key)
|
|
@@ -146,7 +146,7 @@ function main() {
|
|
|
146
146
|
const slug = flags.get('slug') ?? 'my-app'
|
|
147
147
|
const dockerImage = flags.get('docker-image') ?? 'myorg/my-app-ui'
|
|
148
148
|
const jenkinsLibrary = flags.get('jenkins-library') ?? 'my-shared-library'
|
|
149
|
-
const jenkinsNodeImage = flags.get('jenkins-node-image') ?? 'node:22-
|
|
149
|
+
const jenkinsNodeImage = flags.get('jenkins-node-image') ?? 'node:22-alpine'
|
|
150
150
|
const jenkinsDockerLabel = flags.get('jenkins-docker-label') ?? 'docker'
|
|
151
151
|
const jenkinsRegistryCred = flags.get('jenkins-registry-cred') ?? 'docker-registry'
|
|
152
152
|
const jenkinsPoeditorCred = flags.get('jenkins-poeditor-cred') ?? 'poeditor_api_key'
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Only updates keys that already exist in package.json (does not add new dependencies from the policy).
|
|
6
6
|
* Aligns managedDependencies, managedAnywhere (only where the package is already declared), optional
|
|
7
|
-
* policy overrides into pnpm.overrides, toolchain
|
|
7
|
+
* policy overrides into pnpm.overrides, toolchain fields (packageManager, Node), and logs each change.
|
|
8
8
|
*
|
|
9
9
|
* Refuses to run if the policy file contains any non-pinned versions.
|
|
10
10
|
*
|
|
@@ -17,15 +17,19 @@ import { fileURLToPath } from 'node:url'
|
|
|
17
17
|
|
|
18
18
|
import { collectPinViolations } from './dependency-policy-pins.mjs'
|
|
19
19
|
import {
|
|
20
|
+
collectNodeToolchainPolicyViolations,
|
|
20
21
|
collectToolchainPolicyViolations,
|
|
21
22
|
resolveExpectedPackageManager,
|
|
22
23
|
} from './dependency-policy-toolchain.mjs'
|
|
24
|
+
import { syncNodeToolchainInRepo } from './node-toolchain-sync.mjs'
|
|
25
|
+
import { syncPnpmToolchainInRepo } from './pnpm-toolchain-sync.mjs'
|
|
23
26
|
import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
|
|
24
27
|
|
|
25
28
|
const cwd = process.cwd()
|
|
26
29
|
const pkgPath = path.resolve(cwd, 'package.json')
|
|
27
30
|
const lockfilePath = path.resolve(cwd, 'pnpm-lock.yaml')
|
|
28
31
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
32
|
+
const fgDevkitRoot = path.resolve(scriptDir, '..')
|
|
29
33
|
const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
|
|
30
34
|
|
|
31
35
|
if (!fs.existsSync(pkgPath)) {
|
|
@@ -49,6 +53,7 @@ const policyErrors = [
|
|
|
49
53
|
),
|
|
50
54
|
...collectPinViolations('managedAnywhere.', policy.managedAnywhere ?? {}),
|
|
51
55
|
...collectToolchainPolicyViolations(policy),
|
|
56
|
+
...collectNodeToolchainPolicyViolations(policy),
|
|
52
57
|
]
|
|
53
58
|
|
|
54
59
|
if (policyErrors.length > 0) {
|
|
@@ -130,30 +135,57 @@ for (const [name, version] of Object.entries(policy.managedAnywhere ?? {})) {
|
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
pkg.pnpm ||= {}
|
|
134
|
-
pkg.pnpm.overrides ||= {}
|
|
135
|
-
|
|
136
138
|
// Apply policy-defined override versions when present in the policy.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const relevantOverrides = getRelevantPolicyOverrides(policy.overrides ?? {})
|
|
140
|
+
for (const [name, version] of Object.entries(relevantOverrides)) {
|
|
141
|
+
if (pkg.pnpm?.overrides?.[name] === version) {
|
|
142
|
+
continue
|
|
141
143
|
}
|
|
144
|
+
pkg.pnpm ??= {}
|
|
145
|
+
pkg.pnpm.overrides ??= {}
|
|
146
|
+
changes.push(`pnpm.overrides.${name}: ${pkg.pnpm.overrides[name] ?? '(missing)'} -> ${version}`)
|
|
147
|
+
pkg.pnpm.overrides[name] = version
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
const expectedPackageManager = resolveExpectedPackageManager(policy,
|
|
150
|
+
const expectedPackageManager = resolveExpectedPackageManager(policy, fgDevkitRoot)
|
|
145
151
|
if (expectedPackageManager !== undefined && pkg.packageManager !== expectedPackageManager) {
|
|
146
152
|
changes.push(`packageManager: ${pkg.packageManager ?? '(missing)'} -> ${expectedPackageManager}`)
|
|
147
153
|
pkg.packageManager = expectedPackageManager
|
|
148
154
|
}
|
|
149
155
|
|
|
150
|
-
if (changes.length
|
|
151
|
-
|
|
156
|
+
if (changes.length > 0) {
|
|
157
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const nodeToolchainChanges = syncNodeToolchainInRepo(cwd, policy, { fgDevkitRoot })
|
|
161
|
+
|
|
162
|
+
let pnpmToolchainChanges = []
|
|
163
|
+
if (expectedPackageManager !== undefined) {
|
|
164
|
+
pnpmToolchainChanges = syncPnpmToolchainInRepo(cwd, expectedPackageManager)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (changes.length === 0 && nodeToolchainChanges.length === 0 && pnpmToolchainChanges.length === 0) {
|
|
168
|
+
console.log('[sync-deps] package.json and toolchain already match dependency policy')
|
|
152
169
|
process.exit(0)
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
|
|
156
|
-
console.log('[sync-deps] Updated package.json to match dependency policy')
|
|
157
|
-
for (const change of changes) {
|
|
158
|
-
|
|
172
|
+
if (changes.length > 0) {
|
|
173
|
+
console.log('[sync-deps] Updated package.json to match dependency policy')
|
|
174
|
+
for (const change of changes) {
|
|
175
|
+
console.log(`- ${change}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (nodeToolchainChanges.length > 0) {
|
|
180
|
+
console.log('[sync-deps] Updated Node toolchain files')
|
|
181
|
+
for (const change of nodeToolchainChanges) {
|
|
182
|
+
console.log(`- ${change}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (pnpmToolchainChanges.length > 0) {
|
|
187
|
+
console.log('[sync-deps] Updated pnpm toolchain files')
|
|
188
|
+
for (const change of pnpmToolchainChanges) {
|
|
189
|
+
console.log(`- ${change}`)
|
|
190
|
+
}
|
|
159
191
|
}
|