@nitra/cursor 1.8.102 → 1.8.104
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/n-cursor.js +10 -5
- package/mdc/text.mdc +1 -1
- package/package.json +1 -1
- package/scripts/check-abie.mjs +1 -1
- package/scripts/check-bun.mjs +7 -3
- package/scripts/check-docker.mjs +2 -1
- package/scripts/check-graphql.mjs +12 -12
- package/scripts/check-js-lint.mjs +0 -2
- package/scripts/check-k8s.mjs +536 -2
- package/scripts/check-npm-module.mjs +2 -0
- package/scripts/check-text.mjs +32 -8
- package/scripts/check-vue.mjs +3 -1
- package/scripts/rename-yaml-extensions.mjs +7 -3
- package/scripts/run-docker.mjs +2 -1
- package/scripts/run-k8s.mjs +18 -4
- package/scripts/run-v8r.mjs +3 -1
- package/scripts/upgrade-nitra-cursor-and-install.mjs +15 -7
- package/scripts/utils/docker-hadolint.mjs +17 -10
- package/scripts/utils/graphql-gql-scan.mjs +10 -11
- package/scripts/utils/resolve-cmd.mjs +23 -0
- package/scripts/utils/vue-forbidden-imports.mjs +5 -2
- package/scripts/utils/workspaces.mjs +7 -1
package/bin/n-cursor.js
CHANGED
|
@@ -68,6 +68,11 @@ const BUNDLED_AGENTS_TEMPLATE_PATH = join(binDir, '..', AGENTS_TEMPLATE_FILE)
|
|
|
68
68
|
/** Корінь установленого пакету (каталог з `mdc/`, `github-actions/`, …) */
|
|
69
69
|
const BUNDLED_PACKAGE_ROOT = join(binDir, '..')
|
|
70
70
|
|
|
71
|
+
const YAML_FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
72
|
+
const SKILL_DESCRIPTION_RE = /description:\s*>-\s*\r?\n((?:[ \t]+[^\n]*(?:\r?\n|$))+)/m
|
|
73
|
+
const NEWLINE_RE = /\r?\n/
|
|
74
|
+
const LEADING_SPACES_RE = /^\s+/
|
|
75
|
+
|
|
71
76
|
/**
|
|
72
77
|
* Імена правил (без .mdc) з каталогу mdc поточної інсталяції пакету
|
|
73
78
|
* @param {string} [bundledMdcDir] каталог `mdc/` у корені пакету (за замовчуванням — з поточного процесу)
|
|
@@ -192,7 +197,7 @@ async function readConfig(paths = {}) {
|
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
if (config.$schema !== CONFIG_SCHEMA_URL) {
|
|
195
|
-
const
|
|
200
|
+
const rest = Object.fromEntries(Object.entries(config).filter(([k]) => k !== '$schema'))
|
|
196
201
|
const normalized = { $schema: CONFIG_SCHEMA_URL, ...rest }
|
|
197
202
|
await writeFile(configPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
|
198
203
|
console.log(`📝 Оновлено поле $schema у ${CONFIG_FILE}\n`)
|
|
@@ -259,18 +264,18 @@ function managedSkillDirName(skillId) {
|
|
|
259
264
|
* @returns {string | null} один рядок опису або null
|
|
260
265
|
*/
|
|
261
266
|
function extractSkillDescription(text) {
|
|
262
|
-
const fm = text.match(
|
|
267
|
+
const fm = text.match(YAML_FRONTMATTER_RE)
|
|
263
268
|
if (!fm) {
|
|
264
269
|
return null
|
|
265
270
|
}
|
|
266
271
|
const block = fm[1]
|
|
267
|
-
const desc = block.match(
|
|
272
|
+
const desc = block.match(SKILL_DESCRIPTION_RE)
|
|
268
273
|
if (!desc) {
|
|
269
274
|
return null
|
|
270
275
|
}
|
|
271
276
|
return desc[1]
|
|
272
|
-
.split(
|
|
273
|
-
.map(line => line.replace(
|
|
277
|
+
.split(NEWLINE_RE)
|
|
278
|
+
.map(line => line.replace(LEADING_SPACES_RE, '').trimEnd())
|
|
274
279
|
.join(' ')
|
|
275
280
|
.trim()
|
|
276
281
|
}
|
package/mdc/text.mdc
CHANGED
|
@@ -104,7 +104,7 @@ version: '1.25'
|
|
|
104
104
|
}
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
Поле **`ignorePatterns`**
|
|
107
|
+
Поле **`ignorePatterns`** обовʼязкове: у масиві мають бути **`**/hasura/metadata/**`** та **`**/schema.graphql`**; інші glob-и додавай за потреби (згенеровані каталоги тощо).
|
|
108
108
|
|
|
109
109
|
Також потрібно прибрати, якщо є в проєкті, модуль **`@nitra/prettier-config`**, **prettier** та всі виклики prettier і налаштування для нього.
|
|
110
110
|
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -423,7 +423,7 @@ export function jsonPatchRemovesPath(patchText, posixPath) {
|
|
|
423
423
|
? String.raw`path:\s*\/spec\/clusterIP\b`
|
|
424
424
|
: String.raw`path:\s*\/spec\/clusterIPs\b`
|
|
425
425
|
const opRe = String.raw`op:\s*remove\b`
|
|
426
|
-
return new RegExp(`${opRe}[
|
|
426
|
+
return new RegExp(String.raw`${opRe}[\s\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(String.raw`${pathRe}[\s\S]{0,200}?${opRe}`, 'mu').test(patchText)
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
/**
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import { readFile } from 'node:fs/promises'
|
|
|
19
19
|
|
|
20
20
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
21
21
|
|
|
22
|
+
const OXFMT_END_RE = /&&[ \t]+oxfmt[ \t]+\.[ \t]*$/
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише **`@nitra/*`**).
|
|
24
26
|
* @param {string} name ключ з поля `devDependencies`
|
|
@@ -142,20 +144,22 @@ export async function check() {
|
|
|
142
144
|
if (aggregate.trim()) {
|
|
143
145
|
const missing = lintPrefixed.filter(name => !aggregate.includes(`bun run ${name}`))
|
|
144
146
|
if (missing.length > 0) {
|
|
147
|
+
const missingList = missing.map(s => `\`${s}\``).join(', ')
|
|
145
148
|
fail(
|
|
146
|
-
`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${
|
|
149
|
+
`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${missingList}`
|
|
147
150
|
)
|
|
148
151
|
} else {
|
|
149
152
|
pass('package.json: агрегований `lint` покриває всі `lint-*` скрипти')
|
|
150
|
-
if (
|
|
153
|
+
if (OXFMT_END_RE.test(aggregate.trim())) {
|
|
151
154
|
pass('package.json: `lint` завершується `&& oxfmt .`')
|
|
152
155
|
} else {
|
|
153
156
|
fail('Скрипт `lint` має закінчуватися на `&& oxfmt .`')
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
} else {
|
|
160
|
+
const scriptList = lintPrefixed.map(s => `\`${s}\``).join(', ')
|
|
157
161
|
fail(
|
|
158
|
-
`У package.json є скрипти ${
|
|
162
|
+
`У package.json є скрипти ${scriptList}, але немає агрегованого \`lint\` — додай скрипт, який запускає їх через \`bun run\``
|
|
159
163
|
)
|
|
160
164
|
}
|
|
161
165
|
}
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export const GRAPHQL_RC_FILENAME = '.graphqlrc.yml'
|
|
|
24
24
|
export const REQUIRED_GRAPHQL_VSCODE_EXTENSION = 'graphql.vscode-graphql'
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Перевіряє graphql.mdc: умовна вимога
|
|
27
|
+
* Перевіряє graphql.mdc: умовна вимога .graphqlrc.yml і graphql.vscode-graphql за наявності gql tagged templates.
|
|
28
28
|
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
29
29
|
*/
|
|
30
30
|
export async function check() {
|
|
@@ -59,19 +59,15 @@ export async function check() {
|
|
|
59
59
|
|
|
60
60
|
pass(`Знайдено gql\`…\` у ${hits.length} файлі(ах): ${hits.slice(0, 5).join(', ')}${hits.length > 5 ? '…' : ''}`)
|
|
61
61
|
|
|
62
|
-
if (
|
|
62
|
+
if (existsSync(GRAPHQL_RC_FILENAME)) {
|
|
63
|
+
pass(`${GRAPHQL_RC_FILENAME} існує`)
|
|
64
|
+
} else {
|
|
63
65
|
fail(
|
|
64
66
|
`Відсутній ${GRAPHQL_RC_FILENAME} у корені — додай GraphQL Config (graphql.mdc), бо в проєкті є gql template literals`
|
|
65
67
|
)
|
|
66
|
-
} else {
|
|
67
|
-
pass(`${GRAPHQL_RC_FILENAME} існує`)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (
|
|
71
|
-
fail(
|
|
72
|
-
'.vscode/extensions.json не існує — створи файл і додай у recommendations graphql.vscode-graphql (graphql.mdc)'
|
|
73
|
-
)
|
|
74
|
-
} else {
|
|
70
|
+
if (existsSync('.vscode/extensions.json')) {
|
|
75
71
|
let ext
|
|
76
72
|
try {
|
|
77
73
|
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
@@ -83,14 +79,18 @@ export async function check() {
|
|
|
83
79
|
const rec = ext.recommendations
|
|
84
80
|
if (!Array.isArray(rec)) {
|
|
85
81
|
fail('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
86
|
-
} else if (
|
|
82
|
+
} else if (rec.includes(REQUIRED_GRAPHQL_VSCODE_EXTENSION)) {
|
|
83
|
+
pass(`.vscode/extensions.json: є ${REQUIRED_GRAPHQL_VSCODE_EXTENSION}`)
|
|
84
|
+
} else {
|
|
87
85
|
fail(
|
|
88
86
|
`.vscode/extensions.json: додай у recommendations "${REQUIRED_GRAPHQL_VSCODE_EXTENSION}" (graphql.mdc)`
|
|
89
87
|
)
|
|
90
|
-
} else {
|
|
91
|
-
pass(`.vscode/extensions.json: є ${REQUIRED_GRAPHQL_VSCODE_EXTENSION}`)
|
|
92
88
|
}
|
|
93
89
|
}
|
|
90
|
+
} else {
|
|
91
|
+
fail(
|
|
92
|
+
'.vscode/extensions.json не існує — створи файл і додай у recommendations graphql.vscode-graphql (graphql.mdc)'
|
|
93
|
+
)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
return reporter.getExitCode()
|
|
@@ -39,7 +39,6 @@ export function isCanonicalLintJs(script) {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.5.0).
|
|
42
|
-
*
|
|
43
42
|
* @param {unknown} versionSpec значення `devDependencies['@nitra/eslint-config']`
|
|
44
43
|
* @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.5.0
|
|
45
44
|
*/
|
|
@@ -60,7 +59,6 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
|
|
|
60
59
|
|
|
61
60
|
/**
|
|
62
61
|
* Перевіряє потрібні поля `.oxlintrc.json` для розширення e18e (js-lint.mdc).
|
|
63
|
-
*
|
|
64
62
|
* @param {unknown} cfg корінь JSON-конфігурації oxlint
|
|
65
63
|
* @returns {{ ok: boolean, failures: string[] }} `ok` і перелік повідомлень для `fail`
|
|
66
64
|
*/
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
* **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
|
|
46
46
|
* на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
|
|
47
47
|
*
|
|
48
|
+
* **Мішень patch:** у **`patches[].target`** і **`patchesJson6902[].target`** (без **labelSelector** / **annotationSelector**)
|
|
49
|
+
* має існувати відповідний ресурс у зібраному з **`resources`**, **`bases`**, **`components`**, **`crds`** каталозі (рекурсивно для підкаталогів з **`kustomization.yaml`**).
|
|
50
|
+
* Для **`patchesStrategicMerge`** і для **`patches[].path`** без **`target`** і без inline **`patch`** (зовнішній strategic-merge)
|
|
51
|
+
* кожен YAML-документ з кореневим **`kind`** і **`metadata.name`** також звіряється з цим каталогом.
|
|
52
|
+
*
|
|
48
53
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
49
54
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
50
55
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
@@ -471,6 +476,533 @@ export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
|
|
|
471
476
|
return managed
|
|
472
477
|
}
|
|
473
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Шляхи лише з полів ресурсів Kustomization (**без** patch-файлів).
|
|
481
|
+
* @param {unknown} obj корінь першого документа Kustomization
|
|
482
|
+
* @returns {string[]} відносні посилання
|
|
483
|
+
*/
|
|
484
|
+
function resourcePathRefsFromKustomizationObject(obj) {
|
|
485
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
|
|
486
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
487
|
+
/** @type {string[]} */
|
|
488
|
+
const out = []
|
|
489
|
+
pushStringPaths(rec.resources, out)
|
|
490
|
+
pushStringPaths(rec.bases, out)
|
|
491
|
+
pushStringPaths(rec.components, out)
|
|
492
|
+
pushStringPaths(rec.crds, out)
|
|
493
|
+
return out
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Дескриптор ресурсу для звірки з **`target`** Kustomize / strategic-merge фрагментом.
|
|
498
|
+
* @typedef {{ group: string, version: string, kind: string, name: string, namespace: string }} KustomizeResourceDescriptor
|
|
499
|
+
*/
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Розбиває **`apiVersion`** Kubernetes на **group** і **version**.
|
|
503
|
+
* @param {unknown} apiVersion значення з YAML
|
|
504
|
+
* @returns {{ group: string, version: string }} для `group/version` — два сегменти; для `v1` — core (**group** порожній).
|
|
505
|
+
*/
|
|
506
|
+
export function splitK8sApiVersion(apiVersion) {
|
|
507
|
+
if (typeof apiVersion !== 'string') {
|
|
508
|
+
return { group: '', version: '' }
|
|
509
|
+
}
|
|
510
|
+
const t = apiVersion.trim()
|
|
511
|
+
if (t === '') {
|
|
512
|
+
return { group: '', version: '' }
|
|
513
|
+
}
|
|
514
|
+
const i = t.indexOf('/')
|
|
515
|
+
if (i === -1) {
|
|
516
|
+
return { group: '', version: t }
|
|
517
|
+
}
|
|
518
|
+
return { group: t.slice(0, i), version: t.slice(i + 1) }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Чи patch-**target** використовує **labelSelector** / **annotationSelector** (тоді статична перевірка за іменем не застосовується).
|
|
523
|
+
* @param {Record<string, unknown>} t об’єкт **target**
|
|
524
|
+
* @returns {boolean} true, якщо є непорожній селектор
|
|
525
|
+
*/
|
|
526
|
+
function patchTargetUsesSelector(t) {
|
|
527
|
+
const ls = t.labelSelector
|
|
528
|
+
if (
|
|
529
|
+
ls !== undefined &&
|
|
530
|
+
ls !== null &&
|
|
531
|
+
ls !== '' &&
|
|
532
|
+
((typeof ls === 'object' && !Array.isArray(ls) && Object.keys(ls).length > 0) ||
|
|
533
|
+
(typeof ls === 'string' && ls.trim() !== ''))
|
|
534
|
+
) {
|
|
535
|
+
return true
|
|
536
|
+
}
|
|
537
|
+
const asel = t.annotationSelector
|
|
538
|
+
if (
|
|
539
|
+
asel !== undefined &&
|
|
540
|
+
asel !== null &&
|
|
541
|
+
asel !== '' &&
|
|
542
|
+
((typeof asel === 'object' && !Array.isArray(asel) && Object.keys(asel).length > 0) ||
|
|
543
|
+
(typeof asel === 'string' && asel.trim() !== ''))
|
|
544
|
+
) {
|
|
545
|
+
return true
|
|
546
|
+
}
|
|
547
|
+
return false
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Чи варто перевіряти **target** на наявність ресурсу в каталозі (є **kind** і **name**, немає селекторів).
|
|
552
|
+
* @param {unknown} target значення **patches[].target**
|
|
553
|
+
* @returns {boolean} true, якщо перевірка доречна
|
|
554
|
+
*/
|
|
555
|
+
export function shouldValidateKustomizePatchTarget(target) {
|
|
556
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
557
|
+
return false
|
|
558
|
+
}
|
|
559
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
560
|
+
const kind = t.kind
|
|
561
|
+
const name = t.name
|
|
562
|
+
if (typeof kind !== 'string' || kind.trim() === '' || typeof name !== 'string' || name.trim() === '') {
|
|
563
|
+
return false
|
|
564
|
+
}
|
|
565
|
+
return !patchTargetUsesSelector(t)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Чи **target** Kustomize відповідає дескриптору ресурсу (узгоджено з правилами відбору Kustomize: пропущені поля **target** не звужують).
|
|
570
|
+
* @param {unknown} target об’єкт **target**
|
|
571
|
+
* @param {KustomizeResourceDescriptor} res дескриптор з інвентарю
|
|
572
|
+
* @returns {boolean} true, якщо збігається
|
|
573
|
+
*/
|
|
574
|
+
export function kustomizePatchTargetMatchesDescriptor(target, res) {
|
|
575
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
578
|
+
const rec = /** @type {Record<string, unknown>} */ (target)
|
|
579
|
+
const tk = rec.kind
|
|
580
|
+
const tn = rec.name
|
|
581
|
+
if (typeof tk !== 'string' || typeof tn !== 'string') {
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
if (tk.trim() !== res.kind || tn.trim() !== res.name) {
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
const tgtGroup = rec.group
|
|
588
|
+
if (typeof tgtGroup === 'string' && tgtGroup.trim() !== '' && res.group !== tgtGroup.trim()) {
|
|
589
|
+
return false
|
|
590
|
+
}
|
|
591
|
+
const tgtVersion = rec.version
|
|
592
|
+
if (typeof tgtVersion === 'string' && tgtVersion.trim() !== '' && res.version !== tgtVersion.trim()) {
|
|
593
|
+
return false
|
|
594
|
+
}
|
|
595
|
+
const tgtNs = rec.namespace
|
|
596
|
+
if (typeof tgtNs === 'string' && tgtNs.trim() !== '' && res.namespace !== tgtNs.trim()) {
|
|
597
|
+
return false
|
|
598
|
+
}
|
|
599
|
+
return true
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Чи є в каталозі ресурс, який задовольняє **target**.
|
|
604
|
+
* @param {KustomizeResourceDescriptor[]} catalog зібрані дескриптори
|
|
605
|
+
* @param {unknown} target об’єкт **target**
|
|
606
|
+
* @returns {boolean} true, якщо перевірка не потрібна або знайдено збіг
|
|
607
|
+
*/
|
|
608
|
+
export function kustomizeResourceCatalogMatchesPatchTarget(catalog, target) {
|
|
609
|
+
if (!shouldValidateKustomizePatchTarget(target)) {
|
|
610
|
+
return true
|
|
611
|
+
}
|
|
612
|
+
return catalog.some(res => kustomizePatchTargetMatchesDescriptor(target, res))
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Чи два дескриптори повністю збігаються (для strategic-merge фрагмента).
|
|
617
|
+
* @param {KustomizeResourceDescriptor} a перший
|
|
618
|
+
* @param {KustomizeResourceDescriptor} b другий
|
|
619
|
+
* @returns {boolean} true, якщо всі поля однакові
|
|
620
|
+
*/
|
|
621
|
+
export function kustomizeResourceDescriptorsIdentityEqual(a, b) {
|
|
622
|
+
return (
|
|
623
|
+
a.group === b.group &&
|
|
624
|
+
a.version === b.version &&
|
|
625
|
+
a.kind === b.kind &&
|
|
626
|
+
a.name === b.name &&
|
|
627
|
+
a.namespace === b.namespace
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Будує дескриптор з маніфесту (пропускає **Kustomization** та об’єкти без **metadata.name**).
|
|
633
|
+
* @param {Record<string, unknown>} obj корінь документа
|
|
634
|
+
* @param {string} kustomizationDefaultNs значення **`namespace:`** з kustomization, що підключив файл
|
|
635
|
+
* @returns {KustomizeResourceDescriptor | null} дескриптор для звірки або **null**, якщо документ не підходить.
|
|
636
|
+
*/
|
|
637
|
+
export function kustomizeResourceDescriptorFromManifest(obj, kustomizationDefaultNs) {
|
|
638
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
639
|
+
return null
|
|
640
|
+
}
|
|
641
|
+
const kindRaw = obj.kind
|
|
642
|
+
if (typeof kindRaw !== 'string' || kindRaw.trim() === '') {
|
|
643
|
+
return null
|
|
644
|
+
}
|
|
645
|
+
const kind = kindRaw.trim()
|
|
646
|
+
if (kind === 'Kustomization') {
|
|
647
|
+
return null
|
|
648
|
+
}
|
|
649
|
+
const meta = obj.metadata
|
|
650
|
+
let name = ''
|
|
651
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
652
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
653
|
+
const n = m.name
|
|
654
|
+
if (typeof n === 'string' && n.trim() !== '') {
|
|
655
|
+
name = n.trim()
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (name === '') {
|
|
659
|
+
return null
|
|
660
|
+
}
|
|
661
|
+
const { group, version } = splitK8sApiVersion(obj.apiVersion)
|
|
662
|
+
let namespace = ''
|
|
663
|
+
if (!isClusterScopedKubernetesKind(kind)) {
|
|
664
|
+
let metaNs = ''
|
|
665
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
666
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
667
|
+
const ns = m.namespace
|
|
668
|
+
if (typeof ns === 'string' && ns.trim() !== '') {
|
|
669
|
+
metaNs = ns.trim()
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const def =
|
|
673
|
+
typeof kustomizationDefaultNs === 'string' && kustomizationDefaultNs.trim() !== ''
|
|
674
|
+
? kustomizationDefaultNs.trim()
|
|
675
|
+
: ''
|
|
676
|
+
namespace = metaNs || def
|
|
677
|
+
}
|
|
678
|
+
return { group, version, kind, name, namespace }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Читає k8s YAML і повертає корені документів-об’єктів (після modeline, якщо він є).
|
|
683
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
684
|
+
* @returns {Promise<Record<string, unknown>[]>} масив коренів-об’єктів YAML-документів (без масивів на корені).
|
|
685
|
+
*/
|
|
686
|
+
async function readK8sYamlDocumentRootsForInventory(abs) {
|
|
687
|
+
let raw
|
|
688
|
+
try {
|
|
689
|
+
raw = await readFile(abs, 'utf8')
|
|
690
|
+
} catch {
|
|
691
|
+
return []
|
|
692
|
+
}
|
|
693
|
+
const lines = toLines(raw)
|
|
694
|
+
const body =
|
|
695
|
+
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
696
|
+
/** @type {unknown[]} */
|
|
697
|
+
const roots = parseK8sYamlDocumentObjectRoots(body)
|
|
698
|
+
/** @type {Record<string, unknown>[]} */
|
|
699
|
+
const out = []
|
|
700
|
+
for (const r of roots) {
|
|
701
|
+
if (r !== null && typeof r === 'object' && !Array.isArray(r)) {
|
|
702
|
+
out.push(/** @type {Record<string, unknown>} */ (r))
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return out
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Збирає дескриптори ресурсів з **`resources` / `bases` / `components` / `crds`** для одного дерева kustomization.
|
|
710
|
+
* Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок (як у **`collectKustomizeManagedRelPaths`**).
|
|
711
|
+
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
712
|
+
* @param {string} rootNorm нормалізований абсолютний корінь репозиторію
|
|
713
|
+
* @param {Set<string>} visitedKustomization нормалізовані абсолютні шляхи відвіданих **kustomization.yaml**
|
|
714
|
+
* @returns {Promise<KustomizeResourceDescriptor[]>} плоский список дескрипторів із дерева **resources** / **bases** / **components** / **crds**.
|
|
715
|
+
*/
|
|
716
|
+
export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visitedKustomization) {
|
|
717
|
+
const normKust = resolve(kustAbs)
|
|
718
|
+
if (visitedKustomization.has(normKust)) {
|
|
719
|
+
return []
|
|
720
|
+
}
|
|
721
|
+
visitedKustomization.add(normKust)
|
|
722
|
+
|
|
723
|
+
let raw
|
|
724
|
+
try {
|
|
725
|
+
raw = await readFile(normKust, 'utf8')
|
|
726
|
+
} catch {
|
|
727
|
+
return []
|
|
728
|
+
}
|
|
729
|
+
const lines = toLines(raw)
|
|
730
|
+
const body =
|
|
731
|
+
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
732
|
+
|
|
733
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
734
|
+
let docs
|
|
735
|
+
try {
|
|
736
|
+
docs = parseAllDocuments(body)
|
|
737
|
+
} catch {
|
|
738
|
+
return []
|
|
739
|
+
}
|
|
740
|
+
const first = docs[0]?.toJSON()
|
|
741
|
+
if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) {
|
|
742
|
+
return []
|
|
743
|
+
}
|
|
744
|
+
const rec = /** @type {Record<string, unknown>} */ (first)
|
|
745
|
+
const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
746
|
+
const kustDir = dirname(normKust)
|
|
747
|
+
const pathRefs = resourcePathRefsFromKustomizationObject(first)
|
|
748
|
+
|
|
749
|
+
/** @type {KustomizeResourceDescriptor[]} */
|
|
750
|
+
const out = []
|
|
751
|
+
|
|
752
|
+
for (const ref of pathRefs) {
|
|
753
|
+
if (typeof ref === 'string' && !ref.includes('://')) {
|
|
754
|
+
const resolved = resolve(kustDir, ref)
|
|
755
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
756
|
+
/** @type {import('node:fs').Stats | undefined} */
|
|
757
|
+
let st
|
|
758
|
+
try {
|
|
759
|
+
st = await stat(resolved)
|
|
760
|
+
} catch {
|
|
761
|
+
st = undefined
|
|
762
|
+
}
|
|
763
|
+
if (st !== undefined) {
|
|
764
|
+
if (st.isFile() && /\.ya?ml$/iu.test(resolved)) {
|
|
765
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
766
|
+
for (const o of roots) {
|
|
767
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
768
|
+
if (d !== null) {
|
|
769
|
+
out.push(d)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} else if (st.isDirectory()) {
|
|
773
|
+
const childK = existsSync(join(resolved, 'kustomization.yaml'))
|
|
774
|
+
? join(resolved, 'kustomization.yaml')
|
|
775
|
+
: null
|
|
776
|
+
if (childK !== null) {
|
|
777
|
+
const sub = await collectResourceDescriptorsForKustomizationWalk(
|
|
778
|
+
childK,
|
|
779
|
+
rootNorm,
|
|
780
|
+
visitedKustomization
|
|
781
|
+
)
|
|
782
|
+
out.push(...sub)
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return out
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Витягує записи з явним **target** з **patches** / **patchesJson6902**.
|
|
795
|
+
* @param {unknown} obj перший документ Kustomization
|
|
796
|
+
* @returns {Array<{ section: string, index: number, target: unknown }>} пари **section** + індекс (1-based) і **target** з YAML.
|
|
797
|
+
*/
|
|
798
|
+
function extractExplicitPatchTargetsFromKustomization(obj) {
|
|
799
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
800
|
+
return []
|
|
801
|
+
}
|
|
802
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
803
|
+
/** @type {Array<{ section: string, index: number, target: unknown }>} */
|
|
804
|
+
const out = []
|
|
805
|
+
/**
|
|
806
|
+
* @param {string} section ім’я поля
|
|
807
|
+
* @param {unknown} arr масив з YAML
|
|
808
|
+
* @returns {void}
|
|
809
|
+
*/
|
|
810
|
+
const push = (section, arr) => {
|
|
811
|
+
if (!Array.isArray(arr)) {
|
|
812
|
+
return
|
|
813
|
+
}
|
|
814
|
+
let i = 0
|
|
815
|
+
for (const item of arr) {
|
|
816
|
+
i++
|
|
817
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
818
|
+
const it = /** @type {Record<string, unknown>} */ (item)
|
|
819
|
+
if ('target' in it) {
|
|
820
|
+
out.push({ section, index: i, target: it.target })
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
push('patches', rec.patches)
|
|
826
|
+
push('patchesJson6902', rec.patchesJson6902)
|
|
827
|
+
return out
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Людинозчитуваний опис **target** для повідомлення про помилку.
|
|
832
|
+
* @param {unknown} target об’єкт **target**
|
|
833
|
+
* @returns {string} короткий рядок
|
|
834
|
+
*/
|
|
835
|
+
function formatKustomizePatchTargetForMessage(target) {
|
|
836
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
837
|
+
return String(target)
|
|
838
|
+
}
|
|
839
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
840
|
+
const parts = []
|
|
841
|
+
const g = t.group
|
|
842
|
+
const v = t.version
|
|
843
|
+
const k = t.kind
|
|
844
|
+
const n = t.name
|
|
845
|
+
const ns = t.namespace
|
|
846
|
+
if (typeof g === 'string' && g.trim() !== '') {
|
|
847
|
+
parts.push(`group=${g.trim()}`)
|
|
848
|
+
}
|
|
849
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
850
|
+
parts.push(`version=${v.trim()}`)
|
|
851
|
+
}
|
|
852
|
+
if (typeof k === 'string' && k.trim() !== '') {
|
|
853
|
+
parts.push(`kind=${k.trim()}`)
|
|
854
|
+
}
|
|
855
|
+
if (typeof n === 'string' && n.trim() !== '') {
|
|
856
|
+
parts.push(`name=${n.trim()}`)
|
|
857
|
+
}
|
|
858
|
+
if (typeof ns === 'string' && ns.trim() !== '') {
|
|
859
|
+
parts.push(`namespace=${ns.trim()}`)
|
|
860
|
+
}
|
|
861
|
+
return parts.length > 0 ? parts.join(', ') : JSON.stringify(t)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: **target** patch і strategic-merge посилання не вказують на ресурс поза інвентарем **resources** / **bases** / **components** / **crds**.
|
|
866
|
+
* @param {string} root корінь репозиторію
|
|
867
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
|
|
868
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
869
|
+
* @returns {Promise<void>}
|
|
870
|
+
*/
|
|
871
|
+
async function validateKustomizationPatchTargetsResolved(root, yamlFilesAbs, fail) {
|
|
872
|
+
const rootNorm = resolve(root)
|
|
873
|
+
for (const kustAbs of yamlFilesAbs) {
|
|
874
|
+
if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
|
|
875
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
876
|
+
/** @type {string | undefined} */
|
|
877
|
+
let raw
|
|
878
|
+
let readOk = false
|
|
879
|
+
try {
|
|
880
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
881
|
+
readOk = true
|
|
882
|
+
} catch (error) {
|
|
883
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
884
|
+
fail(`${rel}: не вдалося прочитати для перевірки patch target (${msg})`)
|
|
885
|
+
}
|
|
886
|
+
if (readOk && raw !== undefined) {
|
|
887
|
+
const lines = toLines(raw)
|
|
888
|
+
const body =
|
|
889
|
+
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
890
|
+
/** @type {import('yaml').Document[] | null} */
|
|
891
|
+
let docs = null
|
|
892
|
+
try {
|
|
893
|
+
docs = parseAllDocuments(body)
|
|
894
|
+
} catch {
|
|
895
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки patch target`)
|
|
896
|
+
}
|
|
897
|
+
if (docs !== null) {
|
|
898
|
+
const first = docs[0]?.toJSON()
|
|
899
|
+
if (first !== null && first !== undefined && typeof first === 'object' && !Array.isArray(first)) {
|
|
900
|
+
const rec = /** @type {Record<string, unknown>} */ (first)
|
|
901
|
+
if (rec.kind === 'Kustomization') {
|
|
902
|
+
const visited = new Set()
|
|
903
|
+
const catalog = await collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visited)
|
|
904
|
+
const kustDir = dirname(resolve(kustAbs))
|
|
905
|
+
const kustNs =
|
|
906
|
+
typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
907
|
+
|
|
908
|
+
for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
909
|
+
if (
|
|
910
|
+
shouldValidateKustomizePatchTarget(target) &&
|
|
911
|
+
!kustomizeResourceCatalogMatchesPatchTarget(catalog, target)
|
|
912
|
+
) {
|
|
913
|
+
fail(
|
|
914
|
+
`${rel}: ${section}[${index}].target — немає відповідного ресурсу в resources/bases/components/crds (рекурсивно): ${formatKustomizePatchTargetForMessage(target)}`
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const patchesOnlyPath = rec.patches
|
|
920
|
+
if (Array.isArray(patchesOnlyPath)) {
|
|
921
|
+
let pIdx = 0
|
|
922
|
+
for (const p of patchesOnlyPath) {
|
|
923
|
+
pIdx++
|
|
924
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
925
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
926
|
+
const hasTargetKey = 'target' in pr && pr.target !== undefined && pr.target !== null
|
|
927
|
+
const pathStr = typeof pr.path === 'string' ? pr.path.trim() : ''
|
|
928
|
+
const inlinePatch = typeof pr.patch === 'string' && pr.patch.trim() !== ''
|
|
929
|
+
if (!hasTargetKey && pathStr !== '' && !inlinePatch && !pathStr.includes('://')) {
|
|
930
|
+
const resolved = resolve(kustDir, pathStr)
|
|
931
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
932
|
+
/** @type {import('node:fs').Stats | null} */
|
|
933
|
+
let st = null
|
|
934
|
+
try {
|
|
935
|
+
st = await stat(resolved)
|
|
936
|
+
} catch {
|
|
937
|
+
st = null
|
|
938
|
+
}
|
|
939
|
+
if (st !== null && st.isFile() && /\.ya?ml$/iu.test(resolved)) {
|
|
940
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
941
|
+
let docIdx = 0
|
|
942
|
+
for (const o of roots) {
|
|
943
|
+
docIdx++
|
|
944
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
945
|
+
if (
|
|
946
|
+
d !== null &&
|
|
947
|
+
!catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))
|
|
948
|
+
) {
|
|
949
|
+
const relPatch = (relative(root, resolved) || pathStr).replaceAll('\\', '/')
|
|
950
|
+
fail(
|
|
951
|
+
`${rel}: patches[${pIdx}] path «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const sm = rec.patchesStrategicMerge
|
|
963
|
+
if (Array.isArray(sm)) {
|
|
964
|
+
let smIdx = 0
|
|
965
|
+
for (const ref of sm) {
|
|
966
|
+
smIdx++
|
|
967
|
+
if (typeof ref === 'string' && ref.trim() !== '' && !ref.includes('://')) {
|
|
968
|
+
const resolved = resolve(kustDir, ref.trim())
|
|
969
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
970
|
+
/** @type {import('node:fs').Stats | null} */
|
|
971
|
+
let st = null
|
|
972
|
+
try {
|
|
973
|
+
st = await stat(resolved)
|
|
974
|
+
} catch {
|
|
975
|
+
st = null
|
|
976
|
+
}
|
|
977
|
+
if (st !== null && st.isFile() && /\.ya?ml$/iu.test(resolved)) {
|
|
978
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
979
|
+
let docIdx = 0
|
|
980
|
+
for (const o of roots) {
|
|
981
|
+
docIdx++
|
|
982
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
983
|
+
if (
|
|
984
|
+
d !== null &&
|
|
985
|
+
!catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))
|
|
986
|
+
) {
|
|
987
|
+
const relPatch = (relative(root, resolved) || ref).replaceAll('\\', '/')
|
|
988
|
+
fail(
|
|
989
|
+
`${rel}: patchesStrategicMerge[${smIdx}] «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
|
|
990
|
+
)
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
474
1006
|
/**
|
|
475
1007
|
* Чи це **`k8s/base/kustomization.yaml`** (перевірка обов’язкового непорожнього **`namespace:`**).
|
|
476
1008
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -511,8 +1043,8 @@ async function findK8sYamlFiles(root) {
|
|
|
511
1043
|
if (!/\.ya?ml$/iu.test(p)) return
|
|
512
1044
|
out.push(p)
|
|
513
1045
|
})
|
|
514
|
-
|
|
515
|
-
return
|
|
1046
|
+
|
|
1047
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
516
1048
|
}
|
|
517
1049
|
|
|
518
1050
|
/**
|
|
@@ -1808,6 +2340,8 @@ export async function check() {
|
|
|
1808
2340
|
|
|
1809
2341
|
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|
|
1810
2342
|
|
|
2343
|
+
await validateKustomizationPatchTargetsResolved(root, yamlFiles, fail)
|
|
2344
|
+
|
|
1811
2345
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
1812
2346
|
|
|
1813
2347
|
return reporter.getExitCode()
|
|
@@ -16,6 +16,8 @@ import { readFile, stat } from 'node:fs/promises'
|
|
|
16
16
|
import { join } from 'node:path'
|
|
17
17
|
|
|
18
18
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
19
|
+
|
|
20
|
+
const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
|
|
19
21
|
import {
|
|
20
22
|
hasIdTokenWritePermission,
|
|
21
23
|
hasNpmPublishStepWithPackage,
|
package/scripts/check-text.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє текстовий стек і форматування за правилом text.mdc.
|
|
3
3
|
*
|
|
4
|
-
* oxfmt: `.oxfmtrc.json` з обовʼязковими
|
|
4
|
+
* oxfmt: `.oxfmtrc.json` з обовʼязковими ключами та масивом ignorePatterns (два канонічні glob-и з text.mdc для hasura metadata і schema.graphql),
|
|
5
|
+
* VSCode (formatOnSave, defaultFormatter для js/ts/json/vue/css/html),
|
|
5
6
|
* відсутність Prettier у конфігах і залежностях.
|
|
6
7
|
*
|
|
7
8
|
* cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json); у кореневих **`devDependencies`**
|
|
@@ -20,18 +21,25 @@ import { isAllowedRootDevDependency } from './check-bun.mjs'
|
|
|
20
21
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
21
22
|
import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
22
23
|
|
|
24
|
+
const WORKSPACE_STAR_RE = /^workspace:\*/
|
|
25
|
+
const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/
|
|
26
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/
|
|
27
|
+
|
|
23
28
|
/** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
|
|
24
29
|
const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
|
|
25
30
|
|
|
31
|
+
/** Мінімальні glob-и в `ignorePatterns` у `.oxfmtrc.json` (text.mdc). */
|
|
32
|
+
const OXFMT_REQUIRED_IGNORE_PATTERNS = ['**/hasura/metadata/**', '**/schema.graphql']
|
|
33
|
+
|
|
26
34
|
/**
|
|
27
|
-
* Чи діапазон версії
|
|
35
|
+
* Чи діапазон версії `@nitra/cspell-dict` у package.json означає лінію 2.0.0+ (з цієї версії словники входять у пакет).
|
|
28
36
|
* @param {string|undefined} range наприклад "^2.0.0"
|
|
29
|
-
* @returns {boolean}
|
|
37
|
+
* @returns {boolean} true якщо мажорна версія >= 2
|
|
30
38
|
*/
|
|
31
39
|
function cspellDictVersionAtLeast200(range) {
|
|
32
40
|
if (typeof range !== 'string' || !range.trim()) return false
|
|
33
|
-
const cleaned = range.trim().replace(
|
|
34
|
-
const m = cleaned.match(
|
|
41
|
+
const cleaned = range.trim().replace(WORKSPACE_STAR_RE, '').replace(VERSION_PREFIX_RE, '')
|
|
42
|
+
const m = cleaned.match(SEMVER_RE)
|
|
35
43
|
if (!m) return false
|
|
36
44
|
const major = Number(m[1])
|
|
37
45
|
return major >= 2
|
|
@@ -158,6 +166,22 @@ export async function check() {
|
|
|
158
166
|
if (cfg.tabWidth !== 2) fail('.oxfmtrc.json: tabWidth має бути 2')
|
|
159
167
|
if (cfg.useTabs !== false) fail('.oxfmtrc.json: useTabs має бути false')
|
|
160
168
|
if (cfg.printWidth !== 120) fail('.oxfmtrc.json: printWidth має бути 120')
|
|
169
|
+
|
|
170
|
+
if (Array.isArray(cfg.ignorePatterns)) {
|
|
171
|
+
const set = new Set(cfg.ignorePatterns)
|
|
172
|
+
const missing = OXFMT_REQUIRED_IGNORE_PATTERNS.filter(p => !set.has(p))
|
|
173
|
+
if (missing.length === 0) {
|
|
174
|
+
pass('.oxfmtrc.json: ignorePatterns містить hasura/metadata та schema.graphql')
|
|
175
|
+
} else {
|
|
176
|
+
fail(
|
|
177
|
+
`.oxfmtrc.json ignorePatterns: додай відсутні елементи: ${missing.join(', ')} (канонічний приклад у text.mdc)`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
fail(
|
|
182
|
+
`.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
161
185
|
} else {
|
|
162
186
|
fail('.oxfmtrc.json не існує — створи його')
|
|
163
187
|
}
|
|
@@ -245,10 +269,10 @@ export async function check() {
|
|
|
245
269
|
const cspellRange = devDeps['@nitra/cspell-dict']
|
|
246
270
|
if (!cspellRange) {
|
|
247
271
|
fail('@nitra/cspell-dict у devDependencies обовʼязковий для cspell — bun add -d @nitra/cspell-dict@^2.0.0')
|
|
248
|
-
} else if (
|
|
249
|
-
fail('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
|
|
250
|
-
} else {
|
|
272
|
+
} else if (cspellDictVersionAtLeast200(cspellRange)) {
|
|
251
273
|
pass('@nitra/cspell-dict ^2.0.0+')
|
|
274
|
+
} else {
|
|
275
|
+
fail('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
|
|
252
276
|
}
|
|
253
277
|
|
|
254
278
|
const rootDeps = pkg.dependencies || {}
|
package/scripts/check-vue.mjs
CHANGED
|
@@ -12,6 +12,8 @@ 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+)/
|
|
15
17
|
import {
|
|
16
18
|
findForbiddenVueImportsInSourceFile,
|
|
17
19
|
isVueImportScanSourceFile,
|
|
@@ -73,7 +75,7 @@ async function checkVuePackage(rootDir, fail, passFn) {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
if (devDeps.vite) {
|
|
76
|
-
const match = devDeps.vite.match(
|
|
78
|
+
const match = devDeps.vite.match(MAJOR_VERSION_RE)
|
|
77
79
|
if (match && Number(match[1]) >= 8) {
|
|
78
80
|
passFn(`${prefix}vite >= 8: ${devDeps.vite}`)
|
|
79
81
|
} else {
|
|
@@ -16,6 +16,10 @@ import { relative, resolve } from 'node:path'
|
|
|
16
16
|
|
|
17
17
|
import { walkDir } from './utils/walkDir.mjs'
|
|
18
18
|
|
|
19
|
+
const K8S_YML_RE = /\.yml$/iu
|
|
20
|
+
const GITHUB_YAML_RE = /\.yaml$/iu
|
|
21
|
+
const EXTENSION_RE = /^(.+)(\.[^./\\]+)$/u
|
|
22
|
+
|
|
19
23
|
/**
|
|
20
24
|
* Відносний шлях від кореня з `/`; `null`, якщо поза root.
|
|
21
25
|
* @param {string} rootAbs абсолютний корінь
|
|
@@ -34,7 +38,7 @@ export function posixRelFromRoot(rootAbs, fileAbs) {
|
|
|
34
38
|
* @returns {boolean} true, якщо є сегмент k8s і суфікс .yml
|
|
35
39
|
*/
|
|
36
40
|
export function pathMatchesK8sYml(relPosix) {
|
|
37
|
-
if (
|
|
41
|
+
if (!K8S_YML_RE.test(relPosix)) return false
|
|
38
42
|
return relPosix.split('/').includes('k8s')
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -44,7 +48,7 @@ export function pathMatchesK8sYml(relPosix) {
|
|
|
44
48
|
* @returns {boolean} true, якщо є сегмент .github і суфікс .yaml
|
|
45
49
|
*/
|
|
46
50
|
export function pathMatchesGithubYaml(relPosix) {
|
|
47
|
-
if (
|
|
51
|
+
if (!GITHUB_YAML_RE.test(relPosix)) return false
|
|
48
52
|
return relPosix.split('/').includes('.github')
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -55,7 +59,7 @@ export function pathMatchesGithubYaml(relPosix) {
|
|
|
55
59
|
* @returns {string} шлях з останнім розширенням, заміненим на newExt
|
|
56
60
|
*/
|
|
57
61
|
export function replaceExtension(relPosix, newExt) {
|
|
58
|
-
const m = relPosix.match(
|
|
62
|
+
const m = relPosix.match(EXTENSION_RE)
|
|
59
63
|
if (!m) return relPosix + newExt
|
|
60
64
|
return m[1] + newExt
|
|
61
65
|
}
|
package/scripts/run-docker.mjs
CHANGED
package/scripts/run-k8s.mjs
CHANGED
|
@@ -16,8 +16,12 @@ import { spawnSync } from 'node:child_process'
|
|
|
16
16
|
import { basename, dirname } from 'node:path'
|
|
17
17
|
|
|
18
18
|
import { isRunAsCli } from './cli-entry.mjs'
|
|
19
|
+
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
19
20
|
import { walkDir } from './utils/walkDir.mjs'
|
|
20
21
|
|
|
22
|
+
const PATH_SEPARATOR_RE = /[/\\]/u
|
|
23
|
+
const YAML_EXT_RE = /\.yaml$/iu
|
|
24
|
+
|
|
21
25
|
/** Версія Kubernetes для kubeconform — синхронно з YANNH_PIN (без префікса v і суфікса -standalone-strict). */
|
|
22
26
|
const KUBERNETES_VERSION = '1.33.9'
|
|
23
27
|
|
|
@@ -31,7 +35,7 @@ const DATREE_CRD_SCHEMA_LOCATION =
|
|
|
31
35
|
* @returns {boolean} true, якщо серед компонентів шляху є каталог `k8s`
|
|
32
36
|
*/
|
|
33
37
|
export function pathHasK8sSegment(filePath) {
|
|
34
|
-
const parts = filePath.split(
|
|
38
|
+
const parts = filePath.split(PATH_SEPARATOR_RE)
|
|
35
39
|
return parts.includes('k8s')
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -61,7 +65,7 @@ export async function findK8sRoots(root) {
|
|
|
61
65
|
const roots = new Set()
|
|
62
66
|
await walkDir(root, p => {
|
|
63
67
|
if (!pathHasK8sSegment(p)) return
|
|
64
|
-
if (
|
|
68
|
+
if (!YAML_EXT_RE.test(p)) return
|
|
65
69
|
const k8sRoot = k8sRootFromFile(p)
|
|
66
70
|
if (k8sRoot) roots.add(k8sRoot)
|
|
67
71
|
})
|
|
@@ -85,7 +89,12 @@ function runKubeconform(dirs) {
|
|
|
85
89
|
'-ignore-missing-schemas',
|
|
86
90
|
...dirs
|
|
87
91
|
]
|
|
88
|
-
const
|
|
92
|
+
const kubeconformPath = resolveCmd('kubeconform')
|
|
93
|
+
if (!kubeconformPath) {
|
|
94
|
+
console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
|
|
95
|
+
return 127
|
|
96
|
+
}
|
|
97
|
+
const r = spawnSync(kubeconformPath, args, { stdio: 'inherit', shell: false })
|
|
89
98
|
if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
|
|
90
99
|
console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
|
|
91
100
|
return 127
|
|
@@ -101,7 +110,12 @@ function runKubeconform(dirs) {
|
|
|
101
110
|
*/
|
|
102
111
|
function runKubescape(dirs) {
|
|
103
112
|
for (const d of dirs) {
|
|
104
|
-
const
|
|
113
|
+
const kubescapePath = resolveCmd('kubescape')
|
|
114
|
+
if (!kubescapePath) {
|
|
115
|
+
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
116
|
+
return 127
|
|
117
|
+
}
|
|
118
|
+
const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high'], {
|
|
105
119
|
stdio: 'inherit',
|
|
106
120
|
shell: false
|
|
107
121
|
})
|
package/scripts/run-v8r.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { dirname, join } from 'node:path'
|
|
|
19
19
|
import { fileURLToPath } from 'node:url'
|
|
20
20
|
|
|
21
21
|
import { isRunAsCli } from './cli-entry.mjs'
|
|
22
|
+
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
22
23
|
|
|
23
24
|
/** Типові glob-и для форматів, які обробляє v8r (див. опис CLI v8r). */
|
|
24
25
|
export const DEFAULT_V8R_GLOBS = ['**/*.json', '**/*.json5', '**/*.yml', '**/*.yaml', '**/*.toml']
|
|
@@ -48,7 +49,8 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
for (const pattern of globs) {
|
|
51
|
-
const
|
|
52
|
+
const bunPath = resolveCmd('bun') ?? process.execPath
|
|
53
|
+
const result = spawnSync(bunPath, ['x', 'v8r', pattern, '-c', V8R_CATALOG_PATH], {
|
|
52
54
|
encoding: 'utf8',
|
|
53
55
|
maxBuffer: 50 * 1024 * 1024,
|
|
54
56
|
shell: false,
|
|
@@ -20,6 +20,14 @@ const NPM_LATEST_URL = 'https://registry.npmjs.org/@nitra/cursor/latest'
|
|
|
20
20
|
|
|
21
21
|
const execFileAsync = promisify(execFile)
|
|
22
22
|
|
|
23
|
+
const WORKSPACE_RE = /^workspace:/i
|
|
24
|
+
const FILE_RE = /^file:/i
|
|
25
|
+
const LINK_RE = /^link:/i
|
|
26
|
+
const PORTAL_RE = /^portal:/i
|
|
27
|
+
const GIT_RE = /^git(\+|:\/\/)/i
|
|
28
|
+
const NPM_PROTO_RE = /^npm:/i
|
|
29
|
+
const HTTP_RE = /^https?:\/\//i
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* Чи не можна безпечно підставити semver з npm замість поточного специфікатора залежності.
|
|
25
33
|
* @param {string} specifier значення з package.json
|
|
@@ -30,25 +38,25 @@ export function shouldSkipNpmVersionUpgrade(specifier) {
|
|
|
30
38
|
if (!s) {
|
|
31
39
|
return true
|
|
32
40
|
}
|
|
33
|
-
if (
|
|
41
|
+
if (WORKSPACE_RE.test(s)) {
|
|
34
42
|
return true
|
|
35
43
|
}
|
|
36
|
-
if (
|
|
44
|
+
if (FILE_RE.test(s)) {
|
|
37
45
|
return true
|
|
38
46
|
}
|
|
39
|
-
if (
|
|
47
|
+
if (LINK_RE.test(s)) {
|
|
40
48
|
return true
|
|
41
49
|
}
|
|
42
|
-
if (
|
|
50
|
+
if (PORTAL_RE.test(s)) {
|
|
43
51
|
return true
|
|
44
52
|
}
|
|
45
|
-
if (
|
|
53
|
+
if (GIT_RE.test(s)) {
|
|
46
54
|
return true
|
|
47
55
|
}
|
|
48
|
-
if (
|
|
56
|
+
if (NPM_PROTO_RE.test(s)) {
|
|
49
57
|
return true
|
|
50
58
|
}
|
|
51
|
-
if (
|
|
59
|
+
if (HTTP_RE.test(s)) {
|
|
52
60
|
return true
|
|
53
61
|
}
|
|
54
62
|
if (s.startsWith('./') || s.startsWith('../')) {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { spawnSync } from 'node:child_process'
|
|
9
9
|
import { relative, sep } from 'node:path'
|
|
10
10
|
|
|
11
|
+
import { resolveCmd } from './resolve-cmd.mjs'
|
|
12
|
+
|
|
11
13
|
/** Тег образу для резервного запуску (узгоджуй з docker.mdc). */
|
|
12
14
|
export const HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0'
|
|
13
15
|
|
|
@@ -29,12 +31,13 @@ export function posixRel(root, absPath) {
|
|
|
29
31
|
*/
|
|
30
32
|
export function lintDockerfileWithHadolint(root, absPath) {
|
|
31
33
|
const rel = posixRel(root, absPath)
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const hadolintPath = resolveCmd('hadolint')
|
|
35
|
+
if (hadolintPath) {
|
|
36
|
+
const local = spawnSync(hadolintPath, [rel], {
|
|
37
|
+
cwd: root,
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
maxBuffer: 10 * 1024 * 1024
|
|
40
|
+
})
|
|
38
41
|
const ok = local.status === 0
|
|
39
42
|
return {
|
|
40
43
|
ok,
|
|
@@ -43,16 +46,20 @@ export function lintDockerfileWithHadolint(root, absPath) {
|
|
|
43
46
|
via: 'hadolint'
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
|
-
|
|
49
|
+
|
|
50
|
+
const dockerPath = resolveCmd('docker')
|
|
51
|
+
if (!dockerPath) {
|
|
47
52
|
return {
|
|
48
53
|
ok: false,
|
|
49
54
|
stdout: '',
|
|
50
|
-
stderr:
|
|
51
|
-
|
|
55
|
+
stderr:
|
|
56
|
+
'Не знайдено hadolint у PATH і не знайдено docker у PATH. ' +
|
|
57
|
+
'Встанови hadolint (наприклад brew install hadolint) або Docker (див. docker.mdc).',
|
|
58
|
+
via: 'docker'
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
const docker = spawnSync(
|
|
62
|
+
const docker = spawnSync(dockerPath, ['run', '--rm', '-v', `${root}:/workdir`, '-w', '/workdir', HADOLINT_IMAGE, rel], {
|
|
56
63
|
cwd: root,
|
|
57
64
|
encoding: 'utf8',
|
|
58
65
|
maxBuffer: 10 * 1024 * 1024
|
|
@@ -12,10 +12,12 @@ import {
|
|
|
12
12
|
shouldSkipFileForVueImportScan
|
|
13
13
|
} from './vue-forbidden-imports.mjs'
|
|
14
14
|
|
|
15
|
+
const VUE_EXTENSION_RE = /\.vue$/u
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Мова для Oxc за шляхом файлу (розширення).
|
|
17
19
|
* @param {string} filePath віртуальний або реальний шлях
|
|
18
|
-
* @returns {'js' | 'jsx' | 'ts' | 'tsx'}
|
|
20
|
+
* @returns {'js' | 'jsx' | 'ts' | 'tsx'} мова для Oxc парсера
|
|
19
21
|
*/
|
|
20
22
|
function langFromPath(filePath) {
|
|
21
23
|
const lower = filePath.toLowerCase()
|
|
@@ -34,11 +36,11 @@ function langFromPath(filePath) {
|
|
|
34
36
|
/**
|
|
35
37
|
* Віртуальний шлях для парсера: SFC розбираємо як TypeScript.
|
|
36
38
|
* @param {string} relativePath відносний шлях до файлу
|
|
37
|
-
* @returns {string}
|
|
39
|
+
* @returns {string} шлях із заміненим розширенням для SFC
|
|
38
40
|
*/
|
|
39
41
|
function virtualPathForParse(relativePath) {
|
|
40
42
|
if (relativePath.endsWith('.vue')) {
|
|
41
|
-
return relativePath.replace(
|
|
43
|
+
return relativePath.replace(VUE_EXTENSION_RE, '.ts')
|
|
42
44
|
}
|
|
43
45
|
return relativePath
|
|
44
46
|
}
|
|
@@ -46,7 +48,7 @@ function virtualPathForParse(relativePath) {
|
|
|
46
48
|
/**
|
|
47
49
|
* Чи містить AST хоча б один `gql` tagged template.
|
|
48
50
|
* @param {unknown} node корінь або вузол AST
|
|
49
|
-
* @returns {boolean}
|
|
51
|
+
* @returns {boolean} true якщо знайдено тег gql
|
|
50
52
|
*/
|
|
51
53
|
function astContainsGqlTag(node) {
|
|
52
54
|
if (node === null || node === undefined) {
|
|
@@ -56,7 +58,7 @@ function astContainsGqlTag(node) {
|
|
|
56
58
|
return false
|
|
57
59
|
}
|
|
58
60
|
if (Array.isArray(node)) {
|
|
59
|
-
return node.some(astContainsGqlTag)
|
|
61
|
+
return node.some(n => astContainsGqlTag(n))
|
|
60
62
|
}
|
|
61
63
|
if (node.type === 'TaggedTemplateExpression') {
|
|
62
64
|
const tag = node.tag
|
|
@@ -65,10 +67,7 @@ function astContainsGqlTag(node) {
|
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
for (const key of Object.keys(node)) {
|
|
68
|
-
if (key
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
if (astContainsGqlTag(node[key])) {
|
|
70
|
+
if (key !== 'loc' && key !== 'range' && astContainsGqlTag(node[key])) {
|
|
72
71
|
return true
|
|
73
72
|
}
|
|
74
73
|
}
|
|
@@ -99,7 +98,7 @@ export function sourceFileHasGqlTaggedTemplate(content, relativePath) {
|
|
|
99
98
|
/**
|
|
100
99
|
* Чи підлягає файл скануванню за розширенням (узгоджено з vue-import scan).
|
|
101
100
|
* @param {string} relativePath відносний шлях
|
|
102
|
-
* @returns {boolean}
|
|
101
|
+
* @returns {boolean} true якщо файл підлягає скануванню
|
|
103
102
|
*/
|
|
104
103
|
export function isGqlScanSourceFile(relativePath) {
|
|
105
104
|
return isVueImportScanSourceFile(relativePath)
|
|
@@ -108,7 +107,7 @@ export function isGqlScanSourceFile(relativePath) {
|
|
|
108
107
|
/**
|
|
109
108
|
* Чи пропустити файл (декларації, auto-imports) — ті самі критерії, що для vue-import scan.
|
|
110
109
|
* @param {string} relativePosix шлях з posix-слешами
|
|
111
|
-
* @returns {boolean}
|
|
110
|
+
* @returns {boolean} true якщо файл потрібно пропустити
|
|
112
111
|
*/
|
|
113
112
|
export function shouldSkipFileForGqlScan(relativePosix) {
|
|
114
113
|
return shouldSkipFileForVueImportScan(relativePosix)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Утиліта для розв'язання абсолютного шляху до команди в PATH.
|
|
3
|
+
*
|
|
4
|
+
* Використовується для виклику зовнішніх інструментів через абсолютний шлях
|
|
5
|
+
* замість команди з PATH (sonarjs/no-os-command-from-path).
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { platform } from 'node:process'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Повертає абсолютний шлях до команди в PATH або null, якщо команда не знайдена.
|
|
12
|
+
* @param {string} cmd ім'я команди без шляху
|
|
13
|
+
* @returns {string | null} абсолютний шлях або null
|
|
14
|
+
*/
|
|
15
|
+
export function resolveCmd(cmd) {
|
|
16
|
+
const whichCmd = platform === 'win32' ? 'where' : 'which'
|
|
17
|
+
const result = spawnSync(whichCmd, [cmd], { encoding: 'utf8' })
|
|
18
|
+
if (result.status !== 0 || result.error) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
const line = result.stdout.trim().split('\n')[0].trim()
|
|
22
|
+
return line || null
|
|
23
|
+
}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseSync } from 'oxc-parser'
|
|
12
12
|
|
|
13
|
+
const VUE_EXT_RE = /\.vue$/u
|
|
14
|
+
const SOURCE_FILE_RE = /\.(vue|[cm]?[jt]sx?)$/
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Мова для Oxc за шляхом файлу (розширення).
|
|
15
18
|
* @param {string} filePath віртуальний або реальний шлях до файлу
|
|
@@ -74,7 +77,7 @@ function isAllowedVueStaticImport(imp) {
|
|
|
74
77
|
*/
|
|
75
78
|
function virtualPathForParse(relativePath) {
|
|
76
79
|
if (relativePath.endsWith('.vue')) {
|
|
77
|
-
return relativePath.replace(
|
|
80
|
+
return relativePath.replace(VUE_EXT_RE, '.ts')
|
|
78
81
|
}
|
|
79
82
|
return relativePath
|
|
80
83
|
}
|
|
@@ -162,7 +165,7 @@ export function shouldSkipFileForVueImportScan(relativePosix) {
|
|
|
162
165
|
* @returns {boolean} `true`, якщо розширення підходить для пошуку import
|
|
163
166
|
*/
|
|
164
167
|
export function isVueImportScanSourceFile(relativePath) {
|
|
165
|
-
return
|
|
168
|
+
return SOURCE_FILE_RE.test(relativePath)
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
/**
|
|
@@ -8,6 +8,8 @@ import { existsSync } from 'node:fs'
|
|
|
8
8
|
import { glob, readFile } from 'node:fs/promises'
|
|
9
9
|
import { dirname, join, relative } from 'node:path'
|
|
10
10
|
|
|
11
|
+
const TRAILING_SLASH_RE = /\/$/
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Нормалізує поле `workspaces` з package.json до масиву шляхів / glob-патернів.
|
|
13
15
|
* @param {unknown} workspaces значення `workspaces` з кореневого package.json
|
|
@@ -35,7 +37,11 @@ export async function getMonorepoPackageRootDirs(repoRoot = '.') {
|
|
|
35
37
|
}
|
|
36
38
|
const pkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
37
39
|
for (const raw of normalizeWorkspacePatterns(pkg.workspaces)) {
|
|
38
|
-
|
|
40
|
+
let w = raw.replaceAll('\\', '/')
|
|
41
|
+
while (TRAILING_SLASH_RE.test(w)) {
|
|
42
|
+
w = w.slice(0, -1)
|
|
43
|
+
}
|
|
44
|
+
w = w || '.'
|
|
39
45
|
if (w.includes('*')) {
|
|
40
46
|
const globPat = `${w}/package.json`
|
|
41
47
|
for await (const f of glob(globPat, { cwd: repoRoot })) {
|