@nitra/cursor 1.8.104 → 1.8.106
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/bin/auto-rules.md +45 -0
- package/bin/n-cursor.js +270 -149
- package/mdc/graphql.mdc +15 -1
- package/mdc/k8s.mdc +11 -10
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +558 -553
- package/scripts/check-bun.mjs +106 -82
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +267 -186
- package/scripts/check-k8s.mjs +1148 -673
- package/scripts/check-nginx-default-tpl.mjs +125 -100
- package/scripts/check-npm-module.mjs +165 -118
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +288 -210
- package/scripts/check-vue.mjs +110 -69
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +92 -72
- package/scripts/utils/workspaces.mjs +39 -16
package/scripts/check-vue.mjs
CHANGED
|
@@ -12,8 +12,6 @@ import { readFile } from 'node:fs/promises'
|
|
|
12
12
|
import { join, relative } from 'node:path'
|
|
13
13
|
|
|
14
14
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
15
|
-
|
|
16
|
-
const MAJOR_VERSION_RE = /(\d+)/
|
|
17
15
|
import {
|
|
18
16
|
findForbiddenVueImportsInSourceFile,
|
|
19
17
|
isVueImportScanSourceFile,
|
|
@@ -22,6 +20,8 @@ import {
|
|
|
22
20
|
import { walkDir } from './utils/walkDir.mjs'
|
|
23
21
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
24
22
|
|
|
23
|
+
const MAJOR_VERSION_RE = /(\d+)/
|
|
24
|
+
|
|
25
25
|
/**
|
|
26
26
|
* Формує зрозумілий для людини підпис пакета для повідомлень перевірки.
|
|
27
27
|
* @param {string} rootDir відносний шлях (`'.'` або `site` тощо)
|
|
@@ -52,99 +52,94 @@ function ukFilesCountPhrase(n) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Перевіряє залежності
|
|
56
|
-
* @param {string}
|
|
57
|
-
* @param {
|
|
58
|
-
* @param {
|
|
59
|
-
* @
|
|
55
|
+
* Перевіряє наявність залежності в об'єкті deps.
|
|
56
|
+
* @param {Record<string,string>} deps об'єкт залежностей
|
|
57
|
+
* @param {string} name ім'я пакета
|
|
58
|
+
* @param {string} prefix префікс повідомлення
|
|
59
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
60
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
61
|
+
* @param {string} hint підказка при відсутності
|
|
60
62
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const pkgPath = join(rootDir, 'package.json')
|
|
66
|
-
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
67
|
-
const deps = pkg.dependencies || {}
|
|
68
|
-
const devDeps = pkg.devDependencies || {}
|
|
69
|
-
const allDeps = { ...deps, ...devDeps }
|
|
70
|
-
|
|
71
|
-
if (deps.vue) {
|
|
72
|
-
passFn(`${prefix}vue в dependencies: ${deps.vue}`)
|
|
63
|
+
function checkRequiredDep(deps, name, prefix, passFn, fail, hint = `${name} відсутній`) {
|
|
64
|
+
if (deps[name]) {
|
|
65
|
+
passFn(`${prefix}${name}: ${deps[name]}`)
|
|
73
66
|
} else {
|
|
74
|
-
fail(`${prefix}
|
|
67
|
+
fail(`${prefix}${hint}`)
|
|
75
68
|
}
|
|
69
|
+
}
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Перевіряє версію vite у devDependencies.
|
|
73
|
+
* @param {Record<string,string>} devDeps devDependencies з package.json
|
|
74
|
+
* @param {string} prefix параметр prefix
|
|
75
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
76
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
77
|
+
*/
|
|
78
|
+
function checkViteVersion(devDeps, prefix, passFn, fail) {
|
|
79
|
+
const v = devDeps.vite
|
|
80
|
+
if (!v) {
|
|
85
81
|
fail(`${prefix}vite відсутній в devDependencies`)
|
|
82
|
+
return
|
|
86
83
|
}
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
passFn(`${prefix}
|
|
90
|
-
} else {
|
|
91
|
-
fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (allDeps['vue-macros']) {
|
|
95
|
-
passFn(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
|
|
96
|
-
} else {
|
|
97
|
-
fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (allDeps['unplugin-auto-import']) {
|
|
101
|
-
passFn(`${prefix}unplugin-auto-import присутній`)
|
|
102
|
-
} else {
|
|
103
|
-
fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (allDeps['vite-plugin-vue-layouts-next']) {
|
|
107
|
-
passFn(`${prefix}vite-plugin-vue-layouts-next присутній`)
|
|
84
|
+
const match = v.match(MAJOR_VERSION_RE)
|
|
85
|
+
if (match && Number(match[1]) >= 8) {
|
|
86
|
+
passFn(`${prefix}vite >= 8: ${v}`)
|
|
108
87
|
} else {
|
|
109
|
-
fail(`${prefix}vite
|
|
88
|
+
fail(`${prefix}vite має бути >= 8, знайдено: ${v}`)
|
|
110
89
|
}
|
|
90
|
+
}
|
|
111
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Перевіряє vite.config на наявність VueMacros і AutoImport.
|
|
94
|
+
* @param {string} rootDir параметр rootDir
|
|
95
|
+
* @param {string} prefix параметр prefix
|
|
96
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
97
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
98
|
+
*/
|
|
99
|
+
async function checkViteConfig(rootDir, prefix, passFn, fail) {
|
|
112
100
|
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
113
101
|
const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
|
|
114
|
-
if (viteConfig) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
if (!viteConfig) {
|
|
103
|
+
fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const content = await readFile(join(rootDir, viteConfig), 'utf8')
|
|
107
|
+
const checks = [
|
|
108
|
+
{ token: 'VueMacros', ok: `${viteConfig} використовує VueMacros`, err: `${viteConfig} не містить VueMacros` },
|
|
109
|
+
{ token: 'AutoImport', ok: `${viteConfig} використовує AutoImport`, err: `${viteConfig} не містить AutoImport` }
|
|
110
|
+
]
|
|
111
|
+
for (const { token, ok, err } of checks) {
|
|
112
|
+
if (content.includes(token)) {
|
|
113
|
+
passFn(`${prefix}${ok}`)
|
|
124
114
|
} else {
|
|
125
|
-
fail(`${prefix}${
|
|
115
|
+
fail(`${prefix}${err}`)
|
|
126
116
|
}
|
|
127
|
-
} else {
|
|
128
|
-
fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
|
|
129
117
|
}
|
|
118
|
+
}
|
|
130
119
|
|
|
131
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Сканує джерела пакета на заборонені value-імпорти з vue.
|
|
122
|
+
* @param {string} rootDir параметр rootDir
|
|
123
|
+
* @param {string} absPackageRoot параметр absPackageRoot
|
|
124
|
+
* @param {string} prefix параметр prefix
|
|
125
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
126
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
127
|
+
*/
|
|
128
|
+
async function checkVueImportViolations(rootDir, absPackageRoot, prefix, passFn, fail) {
|
|
132
129
|
/** @type {string[]} */
|
|
133
130
|
const sourcePaths = []
|
|
134
131
|
await walkDir(absPackageRoot, absPath => {
|
|
135
132
|
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
136
|
-
if (shouldSkipFileForVueImportScan(rel)
|
|
137
|
-
|
|
133
|
+
if (!shouldSkipFileForVueImportScan(rel) && isVueImportScanSourceFile(rel)) {
|
|
134
|
+
sourcePaths.push(absPath)
|
|
138
135
|
}
|
|
139
|
-
sourcePaths.push(absPath)
|
|
140
136
|
})
|
|
141
137
|
|
|
142
138
|
let importViolations = 0
|
|
143
139
|
for (const absPath of sourcePaths) {
|
|
144
140
|
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
145
141
|
const content = await readFile(absPath, 'utf8')
|
|
146
|
-
const
|
|
147
|
-
for (const v of hits) {
|
|
142
|
+
for (const v of findForbiddenVueImportsInSourceFile(content, rel)) {
|
|
148
143
|
importViolations++
|
|
149
144
|
fail(`${prefix}${rel}:${v.line} — прибери явний value-імпорт з 'vue' (unplugin-auto-import): ${v.snippet}`)
|
|
150
145
|
}
|
|
@@ -156,6 +151,52 @@ async function checkVuePackage(rootDir, fail, passFn) {
|
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Перевіряє залежності та vite.config одного Vue-пакета.
|
|
156
|
+
* @param {string} rootDir відносний шлях до пакета
|
|
157
|
+
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
158
|
+
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
159
|
+
* @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
|
|
160
|
+
*/
|
|
161
|
+
async function checkVuePackage(rootDir, fail, passFn) {
|
|
162
|
+
const prefix = `[${packageLabel(rootDir)}] `
|
|
163
|
+
const pkg = JSON.parse(await readFile(join(rootDir, 'package.json'), 'utf8'))
|
|
164
|
+
const deps = pkg.dependencies || {}
|
|
165
|
+
const devDeps = pkg.devDependencies || {}
|
|
166
|
+
const allDeps = { ...deps, ...devDeps }
|
|
167
|
+
|
|
168
|
+
checkRequiredDep(deps, 'vue', prefix, passFn, fail, 'vue відсутній в dependencies')
|
|
169
|
+
checkViteVersion(devDeps, prefix, passFn, fail)
|
|
170
|
+
checkRequiredDep(
|
|
171
|
+
devDeps,
|
|
172
|
+
'@vitejs/plugin-vue',
|
|
173
|
+
prefix,
|
|
174
|
+
passFn,
|
|
175
|
+
fail,
|
|
176
|
+
'@vitejs/plugin-vue відсутній в devDependencies'
|
|
177
|
+
)
|
|
178
|
+
checkRequiredDep(allDeps, 'vue-macros', prefix, passFn, fail, 'vue-macros відсутній — bun add -d vue-macros')
|
|
179
|
+
checkRequiredDep(
|
|
180
|
+
allDeps,
|
|
181
|
+
'unplugin-auto-import',
|
|
182
|
+
prefix,
|
|
183
|
+
passFn,
|
|
184
|
+
fail,
|
|
185
|
+
'unplugin-auto-import відсутній — bun add -d unplugin-auto-import'
|
|
186
|
+
)
|
|
187
|
+
checkRequiredDep(
|
|
188
|
+
allDeps,
|
|
189
|
+
'vite-plugin-vue-layouts-next',
|
|
190
|
+
prefix,
|
|
191
|
+
passFn,
|
|
192
|
+
fail,
|
|
193
|
+
'vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
await checkViteConfig(rootDir, prefix, passFn, fail)
|
|
197
|
+
await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
|
|
198
|
+
}
|
|
199
|
+
|
|
159
200
|
/**
|
|
160
201
|
* Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
|
|
161
202
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -59,11 +59,15 @@ export function lintDockerfileWithHadolint(root, absPath) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const docker = spawnSync(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
const docker = spawnSync(
|
|
63
|
+
dockerPath,
|
|
64
|
+
['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel],
|
|
65
|
+
{
|
|
66
|
+
cwd: root,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
maxBuffer: 10 * 1024 * 1024
|
|
69
|
+
}
|
|
70
|
+
)
|
|
67
71
|
if (docker.error) {
|
|
68
72
|
return {
|
|
69
73
|
ok: false,
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { parse } from 'yaml'
|
|
8
8
|
|
|
9
|
+
const CHECKOUT_USES_MARKER = 'actions/checkout@'
|
|
10
|
+
const CHECKOUT_V6_USES = 'actions/checkout@v6'
|
|
11
|
+
const LOCAL_SETUP_BUN_DEPS_MARKER = './.github/actions/setup-bun-deps'
|
|
12
|
+
const BUNX_OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Парсить workflow YAML у звичайний об’єкт; при синтаксичній помилці — `null`.
|
|
11
16
|
* @param {string} content вміст файлу
|
|
@@ -26,26 +31,12 @@ export function parseWorkflowYaml(content) {
|
|
|
26
31
|
* @returns {{ jobId: string, stepIndex: number, step: Record<string, unknown> }[]} плоский список кроків з метаданими
|
|
27
32
|
*/
|
|
28
33
|
export function flattenWorkflowSteps(root) {
|
|
29
|
-
const jobs = root?.jobs
|
|
30
|
-
if (!jobs || typeof jobs !== 'object') {
|
|
31
|
-
return []
|
|
32
|
-
}
|
|
33
34
|
/** @type {{ jobId: string, stepIndex: number, step: Record<string, unknown> }[]} */
|
|
34
35
|
const out = []
|
|
35
|
-
for (const [jobId, job] of
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const [stepIndex, step] of steps.entries()) {
|
|
40
|
-
if (step && typeof step === 'object') {
|
|
41
|
-
out.push({
|
|
42
|
-
jobId,
|
|
43
|
-
stepIndex,
|
|
44
|
-
step: /** @type {Record<string, unknown>} */ (step)
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
36
|
+
for (const [jobId, job] of workflowJobsEntries(root)) {
|
|
37
|
+
const steps = workflowJobSteps(job)
|
|
38
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
39
|
+
out.push({ jobId, stepIndex, step })
|
|
49
40
|
}
|
|
50
41
|
}
|
|
51
42
|
return out
|
|
@@ -100,37 +91,15 @@ export function hasAnyStepUsesContaining(root, substrings) {
|
|
|
100
91
|
* @returns {boolean} `false`, якщо є setup без попереднього checkout
|
|
101
92
|
*/
|
|
102
93
|
export function hasCheckoutBeforeLocalSetupBunDeps(root, setupPathSubstrings) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
const step = steps[i]
|
|
113
|
-
if (step && typeof step === 'object') {
|
|
114
|
-
const uses = getStepUses(/** @type {Record<string, unknown>} */ (step))
|
|
115
|
-
const isSetup = setupPathSubstrings.some(s => uses.includes(s))
|
|
116
|
-
if (isSetup) {
|
|
117
|
-
let foundCheckout = false
|
|
118
|
-
for (let j = 0; j < i; j++) {
|
|
119
|
-
const prev = steps[j]
|
|
120
|
-
if (prev && typeof prev === 'object') {
|
|
121
|
-
const u = getStepUses(/** @type {Record<string, unknown>} */ (prev))
|
|
122
|
-
if (u.includes('actions/checkout@')) {
|
|
123
|
-
foundCheckout = true
|
|
124
|
-
break
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (!foundCheckout) {
|
|
129
|
-
return false
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
94
|
+
for (const [, job] of workflowJobsEntries(root)) {
|
|
95
|
+
let hasCheckoutStep = false
|
|
96
|
+
for (const step of workflowJobSteps(job)) {
|
|
97
|
+
const uses = getStepUses(step)
|
|
98
|
+
if (uses.includes(CHECKOUT_USES_MARKER)) {
|
|
99
|
+
hasCheckoutStep = true
|
|
100
|
+
}
|
|
101
|
+
if (setupPathSubstrings.some(s => uses.includes(s)) && !hasCheckoutStep) {
|
|
102
|
+
return false
|
|
134
103
|
}
|
|
135
104
|
}
|
|
136
105
|
}
|
|
@@ -279,26 +248,15 @@ export function verifyLintJsWorkflowStructure(root) {
|
|
|
279
248
|
const usesList = steps.map(s => getStepUses(s.step))
|
|
280
249
|
const runBlob = steps.map(s => getStepRun(s.step)).join('\n')
|
|
281
250
|
|
|
282
|
-
if (!usesList.some(u => u.includes(
|
|
251
|
+
if (!usesList.some(u => u.includes(CHECKOUT_V6_USES))) {
|
|
283
252
|
failures.push('немає кроку uses: actions/checkout@v6')
|
|
284
253
|
}
|
|
285
254
|
|
|
286
|
-
|
|
287
|
-
for (const { step } of steps) {
|
|
288
|
-
const u = getStepUses(step)
|
|
289
|
-
if (u.includes('actions/checkout@v6')) {
|
|
290
|
-
const w = step.with
|
|
291
|
-
if (w && typeof w === 'object' && /** @type {Record<string, unknown>} */ (w)['persist-credentials'] === false) {
|
|
292
|
-
checkoutOk = true
|
|
293
|
-
break
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (!checkoutOk) {
|
|
255
|
+
if (!hasCheckoutWithPersistCredentialsFalse(steps)) {
|
|
298
256
|
failures.push('checkout@v6 без with.persist-credentials: false')
|
|
299
257
|
}
|
|
300
258
|
|
|
301
|
-
if (!usesList.some(u => u.includes(
|
|
259
|
+
if (!usesList.some(u => u.includes(LOCAL_SETUP_BUN_DEPS_MARKER))) {
|
|
302
260
|
failures.push('немає uses: ./.github/actions/setup-bun-deps')
|
|
303
261
|
}
|
|
304
262
|
|
|
@@ -312,15 +270,7 @@ export function verifyLintJsWorkflowStructure(root) {
|
|
|
312
270
|
failures.push('у run немає bunx jscpd .')
|
|
313
271
|
}
|
|
314
272
|
|
|
315
|
-
|
|
316
|
-
const run = getStepRun(step)
|
|
317
|
-
if (/bunx\s+oxlint[^\n]*--fix/u.test(run)) {
|
|
318
|
-
failures.push('у run є oxlint з --fix (у CI заборонено)')
|
|
319
|
-
}
|
|
320
|
-
if (run.includes('eslint --fix')) {
|
|
321
|
-
failures.push('у run є eslint --fix (у CI заборонено)')
|
|
322
|
-
}
|
|
323
|
-
}
|
|
273
|
+
appendCiFixFlagFailures(failures, steps)
|
|
324
274
|
|
|
325
275
|
return failures.length === 0 ? { ok: true, failures: [] } : { ok: false, failures }
|
|
326
276
|
}
|
|
@@ -348,3 +298,73 @@ export function anyRunStepIncludes(root, needle) {
|
|
|
348
298
|
export function anyRunStepIncludesStylelint(root) {
|
|
349
299
|
return anyRunStepIncludes(root, 'npx stylelint')
|
|
350
300
|
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Повертає jobs як список пар [jobId, job], якщо структура валідна.
|
|
304
|
+
* @param {Record<string, unknown>} root корінь workflow
|
|
305
|
+
* @returns {[string, Record<string, unknown>][]} список jobs
|
|
306
|
+
*/
|
|
307
|
+
function workflowJobsEntries(root) {
|
|
308
|
+
const jobs = root?.jobs
|
|
309
|
+
if (!jobs || typeof jobs !== 'object') {
|
|
310
|
+
return []
|
|
311
|
+
}
|
|
312
|
+
return Object.entries(jobs).flatMap(([jobId, job]) =>
|
|
313
|
+
job && typeof job === 'object' ? [[jobId, /** @type {Record<string, unknown>} */ (job)]] : []
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Повертає валідні кроки job.
|
|
319
|
+
* @param {Record<string, unknown>} job job-об’єкт
|
|
320
|
+
* @returns {Record<string, unknown>[]} кроки job
|
|
321
|
+
*/
|
|
322
|
+
function workflowJobSteps(job) {
|
|
323
|
+
const steps = /** @type {{ steps?: unknown }} */ (job).steps
|
|
324
|
+
if (!Array.isArray(steps)) {
|
|
325
|
+
return []
|
|
326
|
+
}
|
|
327
|
+
return steps.flatMap(step =>
|
|
328
|
+
step && typeof step === 'object' ? [/** @type {Record<string, unknown>} */ (step)] : []
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Чи є checkout@v6 з `persist-credentials: false`.
|
|
334
|
+
* @param {{ step: Record<string, unknown> }[]} steps кроки flattenWorkflowSteps
|
|
335
|
+
* @returns {boolean} true, якщо знайдено очікуваний checkout
|
|
336
|
+
*/
|
|
337
|
+
function hasCheckoutWithPersistCredentialsFalse(steps) {
|
|
338
|
+
for (const { step } of steps) {
|
|
339
|
+
const uses = getStepUses(step)
|
|
340
|
+
if (uses.includes(CHECKOUT_V6_USES)) {
|
|
341
|
+
const withObj = step.with
|
|
342
|
+
if (
|
|
343
|
+
withObj &&
|
|
344
|
+
typeof withObj === 'object' &&
|
|
345
|
+
/** @type {Record<string, unknown>} */ (withObj)['persist-credentials'] === false
|
|
346
|
+
) {
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Додає порушення для `--fix` у CI-кроках lint-js workflow.
|
|
356
|
+
* @param {string[]} failures акумулятор порушень
|
|
357
|
+
* @param {{ step: Record<string, unknown> }[]} steps кроки flattenWorkflowSteps
|
|
358
|
+
* @returns {void}
|
|
359
|
+
*/
|
|
360
|
+
function appendCiFixFlagFailures(failures, steps) {
|
|
361
|
+
for (const { step } of steps) {
|
|
362
|
+
const run = getStepRun(step)
|
|
363
|
+
if (BUNX_OXLINT_FIX_RE.test(run)) {
|
|
364
|
+
failures.push('у run є oxlint з --fix (у CI заборонено)')
|
|
365
|
+
}
|
|
366
|
+
if (run.includes('eslint --fix')) {
|
|
367
|
+
failures.push('у run є eslint --fix (у CI заборонено)')
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -10,6 +10,43 @@ import { dirname, join, relative } from 'node:path'
|
|
|
10
10
|
|
|
11
11
|
const TRAILING_SLASH_RE = /\/$/
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Нормалізує workspace-патерн до POSIX-формату і прибирає хвостові `/`.
|
|
15
|
+
* @param {string} pattern сирий workspace-патерн
|
|
16
|
+
* @returns {string} нормалізований патерн або `.`
|
|
17
|
+
*/
|
|
18
|
+
function normalizeWorkspacePattern(pattern) {
|
|
19
|
+
let normalized = pattern.replaceAll('\\', '/')
|
|
20
|
+
while (TRAILING_SLASH_RE.test(normalized)) {
|
|
21
|
+
normalized = normalized.slice(0, -1)
|
|
22
|
+
}
|
|
23
|
+
return normalized || '.'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Додає каталоги пакетів до set за workspace-патерном.
|
|
28
|
+
* @param {Set<string>} roots set коренів пакетів
|
|
29
|
+
* @param {string} repoRoot корінь репозиторію
|
|
30
|
+
* @param {string} workspacePattern нормалізований workspace-патерн
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
async function addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern) {
|
|
34
|
+
if (workspacePattern.includes('*')) {
|
|
35
|
+
const globPat = `${workspacePattern}/package.json`
|
|
36
|
+
for await (const relPkgJsonPath of glob(globPat, { cwd: repoRoot })) {
|
|
37
|
+
const absPkgJsonPath = join(repoRoot, relPkgJsonPath)
|
|
38
|
+
const relRoot = relative(repoRoot, dirname(absPkgJsonPath))
|
|
39
|
+
roots.add(relRoot === '' ? '.' : relRoot)
|
|
40
|
+
}
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pkgJsonPath = join(repoRoot, workspacePattern, 'package.json')
|
|
45
|
+
if (existsSync(pkgJsonPath)) {
|
|
46
|
+
roots.add(workspacePattern)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
/**
|
|
14
51
|
* Нормалізує поле `workspaces` з package.json до масиву шляхів / glob-патернів.
|
|
15
52
|
* @param {unknown} workspaces значення `workspaces` з кореневого package.json
|
|
@@ -37,22 +74,8 @@ export async function getMonorepoPackageRootDirs(repoRoot = '.') {
|
|
|
37
74
|
}
|
|
38
75
|
const pkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
39
76
|
for (const raw of normalizeWorkspacePatterns(pkg.workspaces)) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
w = w.slice(0, -1)
|
|
43
|
-
}
|
|
44
|
-
w = w || '.'
|
|
45
|
-
if (w.includes('*')) {
|
|
46
|
-
const globPat = `${w}/package.json`
|
|
47
|
-
for await (const f of glob(globPat, { cwd: repoRoot })) {
|
|
48
|
-
const abs = join(repoRoot, f)
|
|
49
|
-
const rel = relative(repoRoot, dirname(abs))
|
|
50
|
-
roots.add(rel === '' ? '.' : rel)
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
const pkgJson = join(repoRoot, w, 'package.json')
|
|
54
|
-
if (existsSync(pkgJson)) roots.add(w)
|
|
55
|
-
}
|
|
77
|
+
const workspacePattern = normalizeWorkspacePattern(raw)
|
|
78
|
+
await addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern)
|
|
56
79
|
}
|
|
57
80
|
const list = [...roots]
|
|
58
81
|
list.sort((a, b) => {
|