@nitra/cursor 1.27.9 → 1.28.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/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/rules/abie/js/applies.mjs +3 -2
- package/rules/abie/js/env_dns.mjs +4 -2
- package/rules/abie/js/firebase_hosting.mjs +3 -2
- package/rules/abie/js/hc_pairing.mjs +3 -2
- package/rules/abie/js/ua_http_route.mjs +4 -2
- package/rules/abie/js/ua_node_selector.mjs +4 -2
- package/rules/adr/js/hooks.mjs +36 -28
- package/rules/bun/js/layout.mjs +16 -11
- package/rules/capacitor/js/platforms.mjs +3 -2
- package/rules/changelog/js/consistency.mjs +85 -63
- package/rules/changelog/lib/package-manifest.mjs +5 -4
- package/rules/docker/js/lint.mjs +3 -2
- package/rules/ga/js/workflows.mjs +41 -32
- package/rules/graphql/js/tooling.mjs +15 -11
- package/rules/hasura/js/internal_urls.mjs +14 -10
- package/rules/image-avif/js/avif_generation.mjs +36 -23
- package/rules/image-compress/js/package_setup.mjs +18 -12
- package/rules/js-bun-db/js/safety.mjs +3 -2
- package/rules/js-lint/js/tooling.mjs +45 -32
- package/rules/js-run/js/runtime.mjs +21 -15
- package/rules/k8s/js/manifests.mjs +3 -2
- package/rules/nginx-default-tpl/js/template.mjs +7 -6
- package/rules/npm-module/js/package_structure.mjs +82 -57
- package/rules/rego/js/applies.mjs +4 -4
- package/rules/rust/js/applies.mjs +5 -3
- package/rules/security/js/sample_secret.mjs +2 -2
- package/rules/security/js/trufflehog.mjs +6 -4
- package/rules/style-lint/js/tooling.mjs +15 -8
- package/rules/test/coverage/coverage.mjs +1 -1
- package/rules/test/js/data/vitest_config/vitest.config.baseline.js +7 -0
- package/rules/test/js/location.mjs +3 -2
- package/rules/test/js/no-process-chdir.mjs +89 -0
- package/rules/test/js/no-relative-fs-path.mjs +259 -0
- package/rules/test/js/vitest-config-pool-forks.mjs +52 -0
- package/rules/test/test.mdc +21 -0
- package/rules/text/js/forbidden-prettier.mjs +4 -2
- package/rules/text/js/formatting.mjs +25 -16
- package/rules/vue/js/packages.mjs +33 -25
|
@@ -44,14 +44,15 @@ const REQUIRED_WORKFLOWS = ['clean-ga-workflows.yml', 'clean-merged-branch.yml',
|
|
|
44
44
|
* Використовує `git ls-files` з pathspec-магiєю `:(glob)`, щоб не реалізовувати glob engine вручну
|
|
45
45
|
* і не сканувати файлову систему рекурсивно.
|
|
46
46
|
* @param {string} globPattern glob з workflow (наприклад "files/**" або "image-migration-new/**")
|
|
47
|
+
* @param {string} cwd робочий каталог для `git`
|
|
47
48
|
* @returns {boolean} true, якщо є хоча б один збіг
|
|
48
49
|
*/
|
|
49
|
-
function gitHasAnyTrackedFileMatchingGlob(globPattern) {
|
|
50
|
+
function gitHasAnyTrackedFileMatchingGlob(globPattern, cwd) {
|
|
50
51
|
const p = String(globPattern ?? '').trim()
|
|
51
52
|
if (!p) return false
|
|
52
53
|
if (p.startsWith('!')) return true
|
|
53
54
|
try {
|
|
54
|
-
const out = execFileSync('git', ['ls-files', '-z', '--', `:(glob)${p}`], { encoding: 'utf8' })
|
|
55
|
+
const out = execFileSync('git', ['ls-files', '-z', '--', `:(glob)${p}`], { encoding: 'utf8', cwd })
|
|
55
56
|
return out.length > 0
|
|
56
57
|
} catch {
|
|
57
58
|
return false
|
|
@@ -84,15 +85,16 @@ function shouldValidateWorkflowPathsGlob(p) {
|
|
|
84
85
|
* @param {unknown} raw сирий елемент масиву paths
|
|
85
86
|
* @param {(msg: string) => void} passFn pass
|
|
86
87
|
* @param {(msg: string) => void} failFn fail
|
|
88
|
+
* @param {string} cwd робочий каталог для `git`
|
|
87
89
|
*/
|
|
88
|
-
function verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn) {
|
|
90
|
+
function verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn, cwd) {
|
|
89
91
|
const p = String(raw ?? '').trim()
|
|
90
92
|
if (!p) return
|
|
91
93
|
if (!shouldValidateWorkflowPathsGlob(p)) {
|
|
92
94
|
passFn(`${relPath}: on.${eventName}.paths glob пропущено для перевірки існування: ${JSON.stringify(p)}`)
|
|
93
95
|
return
|
|
94
96
|
}
|
|
95
|
-
if (gitHasAnyTrackedFileMatchingGlob(p)) {
|
|
97
|
+
if (gitHasAnyTrackedFileMatchingGlob(p, cwd)) {
|
|
96
98
|
passFn(`${relPath}: on.${eventName}.paths glob матчитсья: ${JSON.stringify(p)}`)
|
|
97
99
|
} else {
|
|
98
100
|
failFn(`${relPath}: on.${eventName}.paths glob не матчитсья ні на один файл: ${JSON.stringify(p)}`)
|
|
@@ -105,8 +107,9 @@ function verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn) {
|
|
|
105
107
|
* @param {Record<string, unknown>} root parsed YAML workflow
|
|
106
108
|
* @param {(msg: string) => void} passFn pass
|
|
107
109
|
* @param {(msg: string) => void} failFn fail
|
|
110
|
+
* @param {string} cwd робочий каталог для `git`
|
|
108
111
|
*/
|
|
109
|
-
function verifyWorkflowEventPathsGlobsExist(relPath, root, passFn, failFn) {
|
|
112
|
+
function verifyWorkflowEventPathsGlobsExist(relPath, root, passFn, failFn, cwd) {
|
|
110
113
|
const on = getObjKey(root, 'on')
|
|
111
114
|
if (!on || typeof on !== 'object') return
|
|
112
115
|
|
|
@@ -119,7 +122,7 @@ function verifyWorkflowEventPathsGlobsExist(relPath, root, passFn, failFn) {
|
|
|
119
122
|
for (const [eventName, paths] of candidates) {
|
|
120
123
|
if (!Array.isArray(paths)) continue
|
|
121
124
|
for (const raw of paths) {
|
|
122
|
-
verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn)
|
|
125
|
+
verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn, cwd)
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
}
|
|
@@ -138,7 +141,7 @@ function getObjKey(obj, key) {
|
|
|
138
141
|
|
|
139
142
|
/**
|
|
140
143
|
* Перевіряє apply-workflow на наявність paths trigger.
|
|
141
|
-
* @param {string} wfDir директорія workflows
|
|
144
|
+
* @param {string} wfDir абсолютна директорія workflows
|
|
142
145
|
* @param {string[]} files список файлів у директорії
|
|
143
146
|
* @param {string} filename параметр filename
|
|
144
147
|
* @param {string} expectedPath параметр expectedPath
|
|
@@ -147,7 +150,7 @@ function getObjKey(obj, key) {
|
|
|
147
150
|
*/
|
|
148
151
|
async function checkApplyWorkflow(wfDir, files, filename, expectedPath, passFn, failFn) {
|
|
149
152
|
if (!files.includes(filename)) return
|
|
150
|
-
const content = await readFile(
|
|
153
|
+
const content = await readFile(join(wfDir, filename), 'utf8')
|
|
151
154
|
const root = parseWorkflowYaml(content)
|
|
152
155
|
const ok = root ? eventPathsIncludeExact(root, 'push', expectedPath) : content.includes(expectedPath)
|
|
153
156
|
if (ok) {
|
|
@@ -159,22 +162,24 @@ async function checkApplyWorkflow(wfDir, files, filename, expectedPath, passFn,
|
|
|
159
162
|
|
|
160
163
|
/**
|
|
161
164
|
* Перевіряє відсутність MegaLinter у workflows та конфіг-файлах.
|
|
162
|
-
* @param {string} wfDir директорія workflows
|
|
165
|
+
* @param {string} wfDir абсолютна директорія workflows
|
|
163
166
|
* @param {string[]} ymlWorkflows параметр ymlWorkflows
|
|
167
|
+
* @param {string} wfDirRel відносний шлях директорії workflows для повідомлень
|
|
168
|
+
* @param {string} cwd корінь репозиторію
|
|
164
169
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
165
170
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
166
171
|
*/
|
|
167
|
-
async function checkMegalinter(wfDir, ymlWorkflows, passFn, failFn) {
|
|
172
|
+
async function checkMegalinter(wfDir, ymlWorkflows, wfDirRel, cwd, passFn, failFn) {
|
|
168
173
|
let found = false
|
|
169
174
|
for (const f of ymlWorkflows) {
|
|
170
175
|
const content = await readFile(join(wfDir, f), 'utf8')
|
|
171
176
|
if (MEGALINTER_USE_PATTERNS.some(re => re.test(content))) {
|
|
172
177
|
found = true
|
|
173
|
-
failFn(`MegaLinter у workflow ${
|
|
178
|
+
failFn(`MegaLinter у workflow ${wfDirRel}/${f} — видали інтеграцію (ga.mdc: MegaLinter)`)
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
for (const name of MEGALINTER_CONFIG_NAMES) {
|
|
177
|
-
if (existsSync(name)) {
|
|
182
|
+
if (existsSync(join(cwd, name))) {
|
|
178
183
|
found = true
|
|
179
184
|
failFn(`Файл ${name} — видали конфіг MegaLinter (ga.mdc: MegaLinter)`)
|
|
180
185
|
}
|
|
@@ -210,16 +215,16 @@ export function checkShellcheckInstalled(passFn, failFn) {
|
|
|
210
215
|
|
|
211
216
|
/**
|
|
212
217
|
* Перевіряє розширення workflow-файлів і наявність обов'язкових workflow.
|
|
213
|
-
* @param {string}
|
|
218
|
+
* @param {string} wfDirRel відносний шлях директорії workflows для повідомлень
|
|
214
219
|
* @param {string[]} files список файлів у wfDir
|
|
215
220
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
216
221
|
* @param {(msg: string) => void} fail callback при помилці
|
|
217
222
|
*/
|
|
218
|
-
function checkGaWorkflowFiles(
|
|
223
|
+
function checkGaWorkflowFiles(wfDirRel, files, pass, fail) {
|
|
219
224
|
const yamlFiles = files.filter(f => f.endsWith('.yaml'))
|
|
220
225
|
if (yamlFiles.length > 0) {
|
|
221
226
|
for (const f of yamlFiles) {
|
|
222
|
-
fail(`Workflow з розширенням .yaml: ${
|
|
227
|
+
fail(`Workflow з розширенням .yaml: ${wfDirRel}/${f} — перейменуй на .yml`)
|
|
223
228
|
}
|
|
224
229
|
} else {
|
|
225
230
|
pass('Всі workflows мають розширення .yml')
|
|
@@ -228,7 +233,7 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
228
233
|
const notYmlFiles = files.filter(f => !f.endsWith('.yml'))
|
|
229
234
|
if (notYmlFiles.length > 0) {
|
|
230
235
|
for (const f of notYmlFiles) {
|
|
231
|
-
fail(`Workflow має бути з розширенням .yml: ${
|
|
236
|
+
fail(`Workflow має бути з розширенням .yml: ${wfDirRel}/${f} (ga.mdc)`)
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
239
|
|
|
@@ -236,7 +241,7 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
236
241
|
if (files.includes(f)) {
|
|
237
242
|
pass(`${f} існує`)
|
|
238
243
|
} else {
|
|
239
|
-
fail(`Відсутній ${
|
|
244
|
+
fail(`Відсутній ${wfDirRel}/${f}`)
|
|
240
245
|
}
|
|
241
246
|
}
|
|
242
247
|
}
|
|
@@ -277,22 +282,24 @@ const GA_PER_WORKFLOW_REGO_TARGETS = [
|
|
|
277
282
|
* бо кожен namespace застосовний лише до свого файла), потім один батч-спавн
|
|
278
283
|
* `ga.workflow_common` на всі `.github/workflows/*.yml`. Hard-fail без
|
|
279
284
|
* `conftest` у PATH — узгоджено з Plan B (див. `runConftestBatch`).
|
|
280
|
-
* @param {string} wfDir шлях до `.github/workflows`
|
|
285
|
+
* @param {string} wfDir абсолютний шлях до `.github/workflows`
|
|
281
286
|
* @param {string[]} ymlWorkflows відносні (від `wfDir`) імена файлів `*.yml`
|
|
287
|
+
* @param {string} cwd корінь репозиторію
|
|
282
288
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
283
289
|
* @param {(msg: string) => void} fail callback при помилці
|
|
284
290
|
* @returns {void}
|
|
285
291
|
*/
|
|
286
|
-
async function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
|
|
292
|
+
async function runAllGaRego(wfDir, ymlWorkflows, cwd, pass, fail) {
|
|
287
293
|
for (const target of GA_PER_WORKFLOW_REGO_TARGETS) {
|
|
288
|
-
|
|
294
|
+
const targetAbs = join(cwd, target.workflow)
|
|
295
|
+
if (!existsSync(targetAbs)) continue
|
|
289
296
|
const concernDir = join(GA_POLICY_DIR, target.policyDirRel.split('/')[1])
|
|
290
297
|
const tpl = await loadTemplate(concernDir)
|
|
291
298
|
const templateData = tpl[basename(target.workflow)]
|
|
292
299
|
const violations = runConftestBatch({
|
|
293
300
|
policyDirRel: target.policyDirRel,
|
|
294
301
|
namespace: target.namespace,
|
|
295
|
-
files: [
|
|
302
|
+
files: [targetAbs],
|
|
296
303
|
templateData
|
|
297
304
|
})
|
|
298
305
|
for (const v of violations) fail(`${target.workflow}: ${v.message}`)
|
|
@@ -328,16 +335,18 @@ async function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
|
|
|
328
335
|
* config, megalinter залишки тощо). `bun run lint-ga` додатково запускає
|
|
329
336
|
* `actionlint` + `zizmor` зовнішніми тулчейнами і **викликає цю ж `check()`** —
|
|
330
337
|
* тобто rego-частина живе тут, не в `lint-ga.mjs`.
|
|
338
|
+
* @param {string} [cwd] корінь репозиторію
|
|
331
339
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
332
340
|
*/
|
|
333
|
-
export async function check() {
|
|
341
|
+
export async function check(cwd = process.cwd()) {
|
|
334
342
|
const reporter = createCheckReporter()
|
|
335
343
|
const { pass, fail } = reporter
|
|
336
344
|
|
|
337
|
-
const
|
|
345
|
+
const wfDirRel = '.github/workflows'
|
|
346
|
+
const wfDir = join(cwd, wfDirRel)
|
|
338
347
|
|
|
339
348
|
if (!existsSync(wfDir)) {
|
|
340
|
-
fail(`Директорія ${
|
|
349
|
+
fail(`Директорія ${wfDirRel} не існує`)
|
|
341
350
|
return reporter.getExitCode()
|
|
342
351
|
}
|
|
343
352
|
|
|
@@ -346,23 +355,23 @@ export async function check() {
|
|
|
346
355
|
|
|
347
356
|
// Rego-крок (per-workflow + workflow_common) — на початку, як єдине джерело
|
|
348
357
|
// істини для пер-документних структурних правил workflow-файлів.
|
|
349
|
-
await runAllGaRego(wfDir, ymlWorkflows, pass, fail)
|
|
358
|
+
await runAllGaRego(wfDir, ymlWorkflows, cwd, pass, fail)
|
|
350
359
|
|
|
351
|
-
const
|
|
352
|
-
if (existsSync(
|
|
353
|
-
pass(`${
|
|
360
|
+
const setupBunDepsActionRel = '.github/actions/setup-bun-deps/action.yml'
|
|
361
|
+
if (existsSync(join(cwd, setupBunDepsActionRel))) {
|
|
362
|
+
pass(`${setupBunDepsActionRel} існує`)
|
|
354
363
|
} else {
|
|
355
364
|
fail(
|
|
356
|
-
`Відсутній ${
|
|
365
|
+
`Відсутній ${setupBunDepsActionRel} — запустіть npx @nitra/cursor або скопіюйте з пакету (ga.mdc: composite setup-bun-deps)`
|
|
357
366
|
)
|
|
358
367
|
}
|
|
359
368
|
|
|
360
|
-
checkGaWorkflowFiles(
|
|
369
|
+
checkGaWorkflowFiles(wfDirRel, files, pass, fail)
|
|
361
370
|
|
|
362
371
|
await checkApplyWorkflow(wfDir, files, 'apply-k8s.yml', '**/k8s/**/*.yaml', pass, fail)
|
|
363
372
|
await checkApplyWorkflow(wfDir, files, 'apply-nats-consumer.yml', '**/consumer.yaml', pass, fail)
|
|
364
373
|
|
|
365
|
-
await checkMegalinter(wfDir, ymlWorkflows, pass, fail)
|
|
374
|
+
await checkMegalinter(wfDir, ymlWorkflows, wfDirRel, cwd, pass, fail)
|
|
366
375
|
|
|
367
376
|
// git-залежна перевірка `on.push.paths` glob-ів (вимагає `git ls-files`) —
|
|
368
377
|
// лишається в JS, бо conftest не має доступу до файлової системи репо.
|
|
@@ -370,7 +379,7 @@ export async function check() {
|
|
|
370
379
|
const content = await readFile(join(wfDir, f), 'utf8')
|
|
371
380
|
const parsed = parseWorkflowYaml(content)
|
|
372
381
|
if (parsed) {
|
|
373
|
-
verifyWorkflowEventPathsGlobsExist(`${
|
|
382
|
+
verifyWorkflowEventPathsGlobsExist(`${wfDirRel}/${f}`, parsed, pass, fail, cwd)
|
|
374
383
|
}
|
|
375
384
|
}
|
|
376
385
|
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; пропуск **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
|
|
6
6
|
*
|
|
7
7
|
* Виявлення **`gql`** — **oxc-parser** після витягування `<script>` з SFC (**`graphql-gql-scan.mjs`**). Якщо збігів немає — перевірка завершується успішно без вимог до конфігів.
|
|
8
|
+
* @param {string} cwd корінь репозиторію
|
|
8
9
|
*/
|
|
9
10
|
import { existsSync } from 'node:fs'
|
|
10
11
|
import { readFile } from 'node:fs/promises'
|
|
11
|
-
import { relative } from 'node:path'
|
|
12
|
+
import { join, relative } from 'node:path'
|
|
12
13
|
|
|
13
14
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
14
15
|
import {
|
|
@@ -75,20 +76,22 @@ async function collectGqlHits(root, candidates) {
|
|
|
75
76
|
* @param {(msg: string) => void} pass success-репортер
|
|
76
77
|
* @param {(msg: string) => void} fail fail-репортер
|
|
77
78
|
* @returns {void}
|
|
79
|
+
* @param {string} cwd корінь репозиторію
|
|
78
80
|
*/
|
|
79
|
-
function checkExtensionsRecommendation(pass, fail) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
function checkExtensionsRecommendation(pass, fail, cwd) {
|
|
82
|
+
const pathRel = '.vscode/extensions.json'
|
|
83
|
+
const pathAbs = join(cwd, pathRel)
|
|
84
|
+
if (!existsSync(pathAbs)) {
|
|
85
|
+
fail(`${pathRel} не існує — створи файл і додай у recommendations ${REQUIRED_GRAPHQL_VSCODE_EXTENSION} (graphql.mdc)`)
|
|
83
86
|
return
|
|
84
87
|
}
|
|
85
88
|
const violations = runConftestBatch({
|
|
86
89
|
policyDirRel: 'graphql/vscode_extensions',
|
|
87
90
|
namespace: 'graphql.vscode_extensions',
|
|
88
|
-
files: [
|
|
91
|
+
files: [pathAbs]
|
|
89
92
|
})
|
|
90
93
|
if (violations.length === 0) {
|
|
91
|
-
pass(`${
|
|
94
|
+
pass(`${pathRel} відповідає graphql.vscode_extensions (rego)`)
|
|
92
95
|
return
|
|
93
96
|
}
|
|
94
97
|
for (const v of violations) fail(v.message)
|
|
@@ -98,12 +101,13 @@ function checkExtensionsRecommendation(pass, fail) {
|
|
|
98
101
|
* Перевіряє graphql.mdc: умовна вимога .graphqlrc.yml і graphql.vscode-graphql
|
|
99
102
|
* за наявності gql tagged templates.
|
|
100
103
|
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
104
|
+
* @param {string} [cwd] корінь репозиторію
|
|
101
105
|
*/
|
|
102
|
-
export async function check() {
|
|
106
|
+
export async function check(cwd = process.cwd()) {
|
|
103
107
|
const reporter = createCheckReporter()
|
|
104
108
|
const { pass, fail } = reporter
|
|
105
109
|
|
|
106
|
-
const root =
|
|
110
|
+
const root = cwd
|
|
107
111
|
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
108
112
|
const candidates = await collectScanCandidates(root, ignorePaths)
|
|
109
113
|
const hits = await collectGqlHits(root, candidates)
|
|
@@ -117,7 +121,7 @@ export async function check() {
|
|
|
117
121
|
|
|
118
122
|
pass(`Знайдено gql\`…\` у ${hits.length} файлі(ах): ${hits.slice(0, 5).join(', ')}${hits.length > 5 ? '…' : ''}`)
|
|
119
123
|
|
|
120
|
-
if (existsSync(GRAPHQL_RC_FILENAME)) {
|
|
124
|
+
if (existsSync(join(root, GRAPHQL_RC_FILENAME))) {
|
|
121
125
|
pass(`${GRAPHQL_RC_FILENAME} існує`)
|
|
122
126
|
} else {
|
|
123
127
|
fail(
|
|
@@ -125,7 +129,7 @@ export async function check() {
|
|
|
125
129
|
)
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
checkExtensionsRecommendation(pass, fail)
|
|
132
|
+
checkExtensionsRecommendation(pass, fail, root)
|
|
129
133
|
|
|
130
134
|
return reporter.getExitCode()
|
|
131
135
|
}
|
|
@@ -129,14 +129,15 @@ async function collectEnvFiles(root, ignorePaths) {
|
|
|
129
129
|
/**
|
|
130
130
|
* Перевіряє один `.env` файл на коректність `HASURA_GRAPHQL_ENDPOINT`.
|
|
131
131
|
* Якщо в файлі немає змінної — вважаємо OK.
|
|
132
|
-
* @param {string} relPath відносний шлях файла
|
|
132
|
+
* @param {string} relPath відносний (posix) шлях файла відносно `cwd` (для повідомлень)
|
|
133
|
+
* @param {string} cwd корінь репозиторію
|
|
133
134
|
* @param {{ service: string | null, namespace: string | null }} expected очікувані сегменти з YAML
|
|
134
135
|
* @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер
|
|
135
136
|
* @returns {Promise<void>}
|
|
136
137
|
*/
|
|
137
|
-
async function checkEnvFile(relPath, expected, reporter) {
|
|
138
|
+
async function checkEnvFile(relPath, cwd, expected, reporter) {
|
|
138
139
|
const { pass, fail } = reporter
|
|
139
|
-
const content = await readFile(relPath, 'utf8')
|
|
140
|
+
const content = await readFile(join(cwd, relPath), 'utf8')
|
|
140
141
|
const m = content.match(HASURA_ENDPOINT_LINE_RE)
|
|
141
142
|
if (!m) {
|
|
142
143
|
return
|
|
@@ -169,14 +170,16 @@ async function checkEnvFile(relPath, expected, reporter) {
|
|
|
169
170
|
|
|
170
171
|
/**
|
|
171
172
|
* Зчитує URL репозиторію з кореневого `package.json` (або null, якщо файла немає / не валідний).
|
|
173
|
+
* @param {string} cwd корінь репозиторію
|
|
172
174
|
* @returns {Promise<string | null>} URL з поля `repository`
|
|
173
175
|
*/
|
|
174
|
-
async function readRootRepositoryUrl() {
|
|
175
|
-
|
|
176
|
+
async function readRootRepositoryUrl(cwd) {
|
|
177
|
+
const pkgPath = join(cwd, 'package.json')
|
|
178
|
+
if (!existsSync(pkgPath)) {
|
|
176
179
|
return null
|
|
177
180
|
}
|
|
178
181
|
try {
|
|
179
|
-
const pkg = JSON.parse(await readFile(
|
|
182
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
180
183
|
return getRepositoryUrl(pkg?.repository)
|
|
181
184
|
} catch {
|
|
182
185
|
return null
|
|
@@ -198,19 +201,20 @@ export function isNitraOrAbieRepository(url) {
|
|
|
198
201
|
|
|
199
202
|
/**
|
|
200
203
|
* Перевіряє hasura.mdc для поточного робочого каталогу.
|
|
204
|
+
* @param {string} [cwd] корінь репозиторію
|
|
201
205
|
* @returns {Promise<number>} 0 — OK / правило не застосовується, 1 — порушення
|
|
202
206
|
*/
|
|
203
|
-
export async function check() {
|
|
207
|
+
export async function check(cwd = process.cwd()) {
|
|
204
208
|
const reporter = createCheckReporter()
|
|
205
209
|
const { pass } = reporter
|
|
206
210
|
|
|
207
|
-
const repositoryUrl = await readRootRepositoryUrl()
|
|
211
|
+
const repositoryUrl = await readRootRepositoryUrl(cwd)
|
|
208
212
|
if (!isNitraOrAbieRepository(repositoryUrl)) {
|
|
209
213
|
pass('Пропущено: репозиторій не nitra і не abie (hasura.mdc застосовується лише до них)')
|
|
210
214
|
return reporter.getExitCode()
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
const root =
|
|
217
|
+
const root = cwd
|
|
214
218
|
const expected = {
|
|
215
219
|
service: await readYamlMetadataName(join(root, HASURA_SVC_HL_FILE), 'Service'),
|
|
216
220
|
namespace: await readYamlMetadataName(join(root, HASURA_NAMESPACE_FILE), 'Namespace')
|
|
@@ -224,7 +228,7 @@ export async function check() {
|
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
for (const rel of envFiles) {
|
|
227
|
-
await checkEnvFile(rel, expected, reporter)
|
|
231
|
+
await checkEnvFile(rel, root, expected, reporter)
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
// Якщо у файлах не було жодної згадки HASURA_GRAPHQL_ENDPOINT — повідом про це.
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* підтримується сучасними браузерами) і не вмикати в публічних сайтах. Перевірка скрипта
|
|
24
24
|
* `lint-image` (заборона `--avif` у ньому) залишається у `image-compress` — тут вона не
|
|
25
25
|
* дублюється.
|
|
26
|
+
* @param {string} cwd корінь репозиторію
|
|
26
27
|
*/
|
|
27
28
|
import { existsSync } from 'node:fs'
|
|
28
29
|
import { readFile, unlink, writeFile } from 'node:fs/promises'
|
|
@@ -47,6 +48,7 @@ const PKG_CONFIG_FIELD = '@nitra/minify-image'
|
|
|
47
48
|
* платформи — `.avif` всередині — це продукт попереднього `bun run build`/Capacitor sync,
|
|
48
49
|
* а не кандидати на видалення. `walkDir` уже скіпає `node_modules`, `.git`, `dist`,
|
|
49
50
|
* `coverage`, `.turbo`, `.next` — додатково для cleanup ігноруємо ще ці.
|
|
51
|
+
* @param {string} cwd корінь репозиторію
|
|
50
52
|
*/
|
|
51
53
|
const CLEANUP_EXTRA_IGNORE_DIR_NAMES = new Set(['build', 'android', 'ios', '.output', '.nuxt', '.cache'])
|
|
52
54
|
|
|
@@ -54,6 +56,7 @@ const CLEANUP_EXTRA_IGNORE_DIR_NAMES = new Set(['build', 'android', 'ios', '.out
|
|
|
54
56
|
* Регексп для імпортів raster-зображень у `.vue` файлах.
|
|
55
57
|
* Захоплює `import name from '...ext'` (як default, так і type-only форми не потрібні —
|
|
56
58
|
* type-imports asset-ів не існує). Захоплюється повний шлях у групі 1.
|
|
59
|
+
* @param {string} cwd корінь репозиторію
|
|
57
60
|
*/
|
|
58
61
|
const VUE_RASTER_IMPORT_RE = /import\s+\w[\w$]*\s+from\s+['"]([^'"\n]+\.(?:png|jpe?g|gif))['"]/giu
|
|
59
62
|
|
|
@@ -64,6 +67,7 @@ const VUE_RASTER_IMPORT_RE = /import\s+\w[\w$]*\s+from\s+['"]([^'"\n]+\.(?:png|j
|
|
|
64
67
|
*
|
|
65
68
|
* Лукбехайнд `(?<![:\-_.])` виключає реактивне `:src="..."` (там JS-вираз — змінна або виклик,
|
|
66
69
|
* перевіряється через імпорт), `data-src="..."` і `obj.src=...` у `<script>`.
|
|
70
|
+
* @param {string} cwd корінь репозиторію
|
|
67
71
|
*/
|
|
68
72
|
const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|jpe?g|gif))['"]/giu
|
|
69
73
|
|
|
@@ -72,6 +76,7 @@ const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|
|
|
|
72
76
|
* так і `<img src="....png.avif" />`). Потрібен лише для збору множини «живих» AVIF —
|
|
73
77
|
* щоб після авто-заміни знати, які `<...>.avif` файли ще на щось посилаються, а які
|
|
74
78
|
* є сиротами і підлягають видаленню.
|
|
79
|
+
* @param {string} cwd корінь репозиторію
|
|
75
80
|
*/
|
|
76
81
|
const VUE_AVIF_REF_RE = /['"]([^'"\s]+\.(?:png|jpe?g|gif)\.avif)['"]/giu
|
|
77
82
|
|
|
@@ -144,6 +149,7 @@ function resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs) {
|
|
|
144
149
|
* @property {number} rewrittenRefs скільки конкретних посилань переписано на `.avif`
|
|
145
150
|
* @property {number} rewrittenFiles у скількох `.vue`/`.html` файлах хоч одне посилання змінилося
|
|
146
151
|
* @property {number} failedRefs скільки конкретних посилань не вдалося переписати (`.avif` не існував)
|
|
152
|
+
* @param {string} cwd корінь репозиторію
|
|
147
153
|
*/
|
|
148
154
|
|
|
149
155
|
/**
|
|
@@ -166,9 +172,10 @@ function resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs) {
|
|
|
166
172
|
* @param {RewriteStats} stats глобальні лічильники, що мутуються тут
|
|
167
173
|
* @param {(msg: string) => void} fail callback при помилці
|
|
168
174
|
* @returns {Promise<void>} визначається по завершенню перевірки одного пакета
|
|
175
|
+
* @param {string} cwd корінь репозиторію
|
|
169
176
|
*/
|
|
170
|
-
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail) {
|
|
171
|
-
const absRoot = join(
|
|
177
|
+
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail, cwd) {
|
|
178
|
+
const absRoot = join(cwd, packageRoot)
|
|
172
179
|
const label = packageRoot === '.' ? 'корінь' : packageRoot
|
|
173
180
|
/** @type {string[]} */
|
|
174
181
|
const targetFiles = []
|
|
@@ -184,7 +191,7 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
184
191
|
if (targetFiles.length === 0) return
|
|
185
192
|
|
|
186
193
|
for (const absPath of targetFiles) {
|
|
187
|
-
const rel = relative(
|
|
194
|
+
const rel = relative(cwd, absPath).split('\\').join('/')
|
|
188
195
|
const original = await readFile(absPath, 'utf8')
|
|
189
196
|
let updated = original
|
|
190
197
|
|
|
@@ -258,26 +265,27 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
258
265
|
* @param {RewriteStats} stats глобальні лічильники rewrite/fail (мутуються нижче)
|
|
259
266
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
260
267
|
* @param {(msg: string) => void} fail callback при помилці
|
|
268
|
+
* @param {string} cwd корінь репозиторію
|
|
261
269
|
* @returns {Promise<string[]>} абсолютні шляхи коренів пакетів з активним opt-out
|
|
262
270
|
*/
|
|
263
|
-
async function checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail) {
|
|
264
|
-
const roots = await getMonorepoPackageRootDirs()
|
|
265
|
-
const absRootsByRel = new Map(roots.map(r => [r, join(
|
|
271
|
+
async function checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail, cwd) {
|
|
272
|
+
const roots = await getMonorepoPackageRootDirs(cwd)
|
|
273
|
+
const absRootsByRel = new Map(roots.map(r => [r, join(cwd, r)]))
|
|
266
274
|
/** @type {string[]} */
|
|
267
275
|
const optedOutAbs = []
|
|
268
276
|
for (const root of roots) {
|
|
269
|
-
const pkgPath = join(root, 'package.json')
|
|
277
|
+
const pkgPath = join(cwd, root, 'package.json')
|
|
270
278
|
if (!existsSync(pkgPath)) continue
|
|
271
279
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
272
280
|
if (packageHasAvifDisabled(pkg)) {
|
|
273
281
|
pass(
|
|
274
282
|
`[${root === '.' ? 'корінь' : root}] avif-import enforcement вимкнено через "@nitra/minify-image.disable-avif"`
|
|
275
283
|
)
|
|
276
|
-
optedOutAbs.push(absRootsByRel.get(root) ?? join(
|
|
284
|
+
optedOutAbs.push(absRootsByRel.get(root) ?? join(cwd, root))
|
|
277
285
|
continue
|
|
278
286
|
}
|
|
279
287
|
const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
|
|
280
|
-
await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail)
|
|
288
|
+
await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail, cwd)
|
|
281
289
|
}
|
|
282
290
|
return optedOutAbs
|
|
283
291
|
}
|
|
@@ -296,17 +304,18 @@ async function checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail)
|
|
|
296
304
|
* все одно не сканували б, тож вони не мають провокувати запуск AVIF-етапу).
|
|
297
305
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
298
306
|
* @returns {Promise<boolean>} `true`, якщо знайдено принаймні одне raster-посилання
|
|
307
|
+
* @param {string} cwd корінь репозиторію
|
|
299
308
|
*/
|
|
300
|
-
async function hasAnyVueRasterReference(ignorePaths) {
|
|
301
|
-
const roots = await getMonorepoPackageRootDirs()
|
|
302
|
-
const absRootsByRel = new Map(roots.map(r => [r, join(
|
|
309
|
+
async function hasAnyVueRasterReference(ignorePaths, cwd) {
|
|
310
|
+
const roots = await getMonorepoPackageRootDirs(cwd)
|
|
311
|
+
const absRootsByRel = new Map(roots.map(r => [r, join(cwd, r)]))
|
|
303
312
|
for (const root of roots) {
|
|
304
|
-
const pkgPath = join(root, 'package.json')
|
|
313
|
+
const pkgPath = join(cwd, root, 'package.json')
|
|
305
314
|
if (existsSync(pkgPath)) {
|
|
306
315
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
307
316
|
if (packageHasAvifDisabled(pkg)) continue
|
|
308
317
|
}
|
|
309
|
-
const absRoot = absRootsByRel.get(root) ?? join(
|
|
318
|
+
const absRoot = absRootsByRel.get(root) ?? join(cwd, root)
|
|
310
319
|
const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
|
|
311
320
|
/** @type {string[]} */
|
|
312
321
|
const targetFiles = []
|
|
@@ -338,8 +347,9 @@ async function hasAnyVueRasterReference(ignorePaths) {
|
|
|
338
347
|
* відсутні `.avif` фейлять окремо). У тестах та інших ізольованих середовищах npx
|
|
339
348
|
* можна вимкнути через `NITRA_CURSOR_NO_AVIF_RUN=1` — тоді ця функція no-op.
|
|
340
349
|
* @returns {void}
|
|
350
|
+
* @param {string} cwd корінь репозиторію
|
|
341
351
|
*/
|
|
342
|
-
function runAvifGeneration() {
|
|
352
|
+
function runAvifGeneration(cwd) {
|
|
343
353
|
if (env.NITRA_CURSOR_NO_AVIF_RUN === '1') return
|
|
344
354
|
const npxPath = resolveCmd('npx')
|
|
345
355
|
if (!npxPath) {
|
|
@@ -350,6 +360,7 @@ function runAvifGeneration() {
|
|
|
350
360
|
}
|
|
351
361
|
const result = spawnSync(npxPath, [MINIFY_PACKAGE_NAME, '--src=.', '--write', '--avif'], {
|
|
352
362
|
stdio: 'inherit',
|
|
363
|
+
cwd,
|
|
353
364
|
env
|
|
354
365
|
})
|
|
355
366
|
if (result.error) {
|
|
@@ -379,12 +390,13 @@ function runAvifGeneration() {
|
|
|
379
390
|
* `.avif` під ними не вважаємо сиротами і не видаляємо
|
|
380
391
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
381
392
|
* @returns {Promise<number>} кількість видалених сиріт
|
|
393
|
+
* @param {string} cwd корінь репозиторію
|
|
382
394
|
*/
|
|
383
|
-
async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths) {
|
|
395
|
+
async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths, cwd) {
|
|
384
396
|
/** @type {string[]} */
|
|
385
397
|
const orphans = []
|
|
386
398
|
await walkDir(
|
|
387
|
-
|
|
399
|
+
cwd,
|
|
388
400
|
absPath => {
|
|
389
401
|
if (!absPath.endsWith('.avif')) return
|
|
390
402
|
if (usedAvifAbs.has(absPath)) return
|
|
@@ -404,27 +416,28 @@ async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths) {
|
|
|
404
416
|
/**
|
|
405
417
|
* Виконує AVIF-етап: запуск AVIF-генерації, авто-заміна raster-посилань у `.vue`/`.html`,
|
|
406
418
|
* видалення AVIF-сиріт. Не валідує `package.json`/`lint-image` — це вже у `image-compress`.
|
|
419
|
+
* @param {string} [cwd] корінь репозиторію
|
|
407
420
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
408
421
|
*/
|
|
409
|
-
export async function check() {
|
|
422
|
+
export async function check(cwd = process.cwd()) {
|
|
410
423
|
const reporter = createCheckReporter()
|
|
411
424
|
const { pass, fail } = reporter
|
|
412
425
|
|
|
413
|
-
const ignorePaths = await loadCursorIgnorePaths(
|
|
426
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
414
427
|
|
|
415
|
-
if (!(await hasAnyVueRasterReference(ignorePaths))) {
|
|
428
|
+
if (!(await hasAnyVueRasterReference(ignorePaths, cwd))) {
|
|
416
429
|
pass('image-avif: у .vue/.html немає raster-посилань для переписування — AVIF-генерація і cleanup пропущені')
|
|
417
430
|
return reporter.getExitCode()
|
|
418
431
|
}
|
|
419
432
|
|
|
420
|
-
runAvifGeneration()
|
|
433
|
+
runAvifGeneration(cwd)
|
|
421
434
|
|
|
422
435
|
/** @type {Set<string>} */
|
|
423
436
|
const usedAvifAbs = new Set()
|
|
424
437
|
/** @type {RewriteStats} */
|
|
425
438
|
const stats = { rewrittenRefs: 0, rewrittenFiles: 0, failedRefs: 0 }
|
|
426
|
-
const optedOutAbs = await checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail)
|
|
427
|
-
const orphansDeleted = await cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths)
|
|
439
|
+
const optedOutAbs = await checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail, cwd)
|
|
440
|
+
const orphansDeleted = await cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths, cwd)
|
|
428
441
|
|
|
429
442
|
pass(
|
|
430
443
|
`image-avif: rewrote ${stats.rewrittenRefs} reference${stats.rewrittenRefs === 1 ? '' : 's'} in ${stats.rewrittenFiles} file${stats.rewrittenFiles === 1 ? '' : 's'}; ` +
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { existsSync } from 'node:fs'
|
|
20
20
|
import { readFile } from 'node:fs/promises'
|
|
21
|
+
import { join } from 'node:path'
|
|
21
22
|
|
|
22
23
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
23
24
|
|
|
@@ -29,11 +30,13 @@ const LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'
|
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Зчитує всі змістовні рядки `.gitignore` (без коментарів і порожніх). Якщо файла нема — `null`.
|
|
33
|
+
* @param {string} cwd корінь репозиторію
|
|
32
34
|
* @returns {Promise<string[] | null>} список trim-нутих рядків або `null`
|
|
33
35
|
*/
|
|
34
|
-
async function readGitignoreLines() {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
async function readGitignoreLines(cwd) {
|
|
37
|
+
const gitignorePath = join(cwd, '.gitignore')
|
|
38
|
+
if (!existsSync(gitignorePath)) return null
|
|
39
|
+
const raw = await readFile(gitignorePath, 'utf8')
|
|
37
40
|
return raw
|
|
38
41
|
.split('\n')
|
|
39
42
|
.map(l => l.trim())
|
|
@@ -49,9 +52,10 @@ async function readGitignoreLines() {
|
|
|
49
52
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
50
53
|
* @param {(msg: string) => void} fail callback при помилці
|
|
51
54
|
* @returns {Promise<void>}
|
|
55
|
+
* @param {string} cwd корінь репозиторію
|
|
52
56
|
*/
|
|
53
|
-
async function checkHashCacheNotIgnored(pass, fail) {
|
|
54
|
-
const lines = await readGitignoreLines()
|
|
57
|
+
async function checkHashCacheNotIgnored(pass, fail, cwd) {
|
|
58
|
+
const lines = await readGitignoreLines(cwd)
|
|
55
59
|
if (lines && lines.includes(HASH_CACHE_FILENAME)) {
|
|
56
60
|
fail(
|
|
57
61
|
`.gitignore: прибери рядок \`${HASH_CACHE_FILENAME}\` — це закомічений source of truth split-cache 3.2.0 (image-compress.mdc)`
|
|
@@ -68,9 +72,10 @@ async function checkHashCacheNotIgnored(pass, fail) {
|
|
|
68
72
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
69
73
|
* @param {(msg: string) => void} fail callback при помилці
|
|
70
74
|
* @returns {Promise<void>}
|
|
75
|
+
* @param {string} cwd корінь репозиторію
|
|
71
76
|
*/
|
|
72
|
-
async function checkLegacyCacheRemoved(pass, fail) {
|
|
73
|
-
if (existsSync(LEGACY_CACHE_FILENAME)) {
|
|
77
|
+
async function checkLegacyCacheRemoved(pass, fail, cwd) {
|
|
78
|
+
if (existsSync(join(cwd, LEGACY_CACHE_FILENAME))) {
|
|
74
79
|
fail(
|
|
75
80
|
`${LEGACY_CACHE_FILENAME} застарілий (split-cache 3.2.0) — видали: ` +
|
|
76
81
|
`\`git rm --cached ${LEGACY_CACHE_FILENAME} 2>/dev/null || true && rm -f ${LEGACY_CACHE_FILENAME}\` ` +
|
|
@@ -78,7 +83,7 @@ async function checkLegacyCacheRemoved(pass, fail) {
|
|
|
78
83
|
)
|
|
79
84
|
return
|
|
80
85
|
}
|
|
81
|
-
const lines = await readGitignoreLines()
|
|
86
|
+
const lines = await readGitignoreLines(cwd)
|
|
82
87
|
if (lines && lines.includes(LEGACY_CACHE_FILENAME)) {
|
|
83
88
|
fail(`.gitignore: прибери застарілий рядок \`${LEGACY_CACHE_FILENAME}\` — split-cache 3.2.0 його не використовує`)
|
|
84
89
|
return
|
|
@@ -90,20 +95,21 @@ async function checkLegacyCacheRemoved(pass, fail) {
|
|
|
90
95
|
* Перевіряє відповідність проєкту правилу `image-compress.mdc`: `.n-minify-image.tsv` НЕ
|
|
91
96
|
* в `.gitignore`, застарілий `.minify-image-cache.tsv` видалений. CI-workflow для image
|
|
92
97
|
* не вимагається — лінт зображень виконується лише локально.
|
|
98
|
+
* @param {string} [cwd] корінь репозиторію
|
|
93
99
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
94
100
|
*/
|
|
95
|
-
export async function check() {
|
|
101
|
+
export async function check(cwd = process.cwd()) {
|
|
96
102
|
const reporter = createCheckReporter()
|
|
97
103
|
const { pass, fail } = reporter
|
|
98
104
|
|
|
99
|
-
if (!existsSync('package.json')) {
|
|
105
|
+
if (!existsSync(join(cwd, 'package.json'))) {
|
|
100
106
|
fail('package.json не знайдено в корені — додай (image-compress.mdc)')
|
|
101
107
|
return reporter.getExitCode()
|
|
102
108
|
}
|
|
103
109
|
pass('package.json є (структуру перевіряє npx @nitra/cursor fix → image_compress.package_json)')
|
|
104
110
|
|
|
105
|
-
await checkHashCacheNotIgnored(pass, fail)
|
|
106
|
-
await checkLegacyCacheRemoved(pass, fail)
|
|
111
|
+
await checkHashCacheNotIgnored(pass, fail, cwd)
|
|
112
|
+
await checkLegacyCacheRemoved(pass, fail, cwd)
|
|
107
113
|
|
|
108
114
|
return reporter.getExitCode()
|
|
109
115
|
}
|