@nitra/cursor 1.8.132 → 1.8.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/auto-rules.md +2 -0
- package/mdc/capacitor.mdc +38 -0
- package/mdc/docker.mdc +9 -3
- package/mdc/k8s.mdc +2 -0
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +9 -0
- package/scripts/check-capacitor.mjs +480 -0
- package/scripts/check-docker.mjs +29 -7
- package/scripts/check-k8s.mjs +72 -1
- package/scripts/check-text.mjs +20 -0
package/bin/auto-rules.md
CHANGED
|
@@ -8,6 +8,8 @@ abie - якщо в кореневому package.json в секції "repository
|
|
|
8
8
|
|
|
9
9
|
bun - якщо в корені проекту є package.json
|
|
10
10
|
|
|
11
|
+
capacitor - якщо в проекті є хоч один файл capacitor.config.json
|
|
12
|
+
|
|
11
13
|
docker - якщо в проекті є хоч один Dockerfile
|
|
12
14
|
|
|
13
15
|
ga - якщо присутня директорія .github/workflows
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Правила для проєктів Capacitor
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Версія Capacitor
|
|
8
|
+
|
|
9
|
+
У `package.json` (у **корені** репозиторію чи **workspace**-пакеті) оголошення **`@capacitor/core`**
|
|
10
|
+
має вказувати діапазон, **сумісний лише з мажорною версією 8 і вище** (наприклад `^8.0.0`).
|
|
11
|
+
**`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні. Програма перевірки — **`check-capacitor.mjs`**
|
|
12
|
+
(репозиторій **@nitra/cursor**).
|
|
13
|
+
|
|
14
|
+
## iOS: зазвичай лише SPM, виняток Podfile
|
|
15
|
+
|
|
16
|
+
- **Правило за замовчуванням:** не залишай `Podfile` (поза `Pods/`) у вихідному iOS-шарі, **якщо** уся потрібна
|
|
17
|
+
iOS-функціональність (нативні плагіни/модулі) може працювати **лише** через **SPM** (Swift Package Manager).
|
|
18
|
+
|
|
19
|
+
- **Плагіни зі скоупу @nitra/:** за політикою вони **підтримуюють SPM**; **перевіряти** їх на **SPM** **не
|
|
20
|
+
потрібно** (і **check** цього **не** робить — **немає** обходу `package.json` на предмет **@nitra/**).
|
|
21
|
+
|
|
22
|
+
- **Коли `Podfile` дозволений:** якщо **не** вся потрібна iOS-функціональність **поза** **@nitra/** (сторонні
|
|
23
|
+
**Capacitor**-плагіни, інша нативна залежність) **доступна** через **SPM** — `Podfile` **дозволяється**,
|
|
24
|
+
але це **обов’язково** треба явно задати в кореневому **`package.json`** або в
|
|
25
|
+
**`capacitor.config.json` / `capacitor.config.ts` / `capacitor.config.mjs`**
|
|
26
|
+
|
|
27
|
+
- **`"iosCocoaPodsBecausePluginsLackSpm": true`** (семантика: **не** **вся** потрібна нативна частина
|
|
28
|
+
**поза** **@nitra/** **на** **SPM**; **@nitra/** у це **не** входить);
|
|
29
|
+
- або **`"iosCocoaPodsAllowed": true`** (короткий **alias** для того самого **винятку**);
|
|
30
|
+
|
|
31
|
+
Без **одного** з цих прапорів `true` наявний **Podfile** поза **`Pods/`** вважається **порушенням** правила **«лише** **SPM**».
|
|
32
|
+
|
|
33
|
+
- Перевірка читає **лише** кореневі файли: **`package.json`**, потім **capacitor-конфіги** у **корені** (див. вище).
|
|
34
|
+
У **`.ts` / `.mjs`**: шукається блок **nitra** `{ ... }` і **на його тілі** перевіряються ці **boolean**-поля.
|
|
35
|
+
|
|
36
|
+
## Перевірка
|
|
37
|
+
|
|
38
|
+
`npx @nitra/cursor check capacitor` (коли **check-скрипт** підключено до цієї **ruleset**).
|
package/mdc/docker.mdc
CHANGED
|
@@ -13,10 +13,13 @@ alwaysApply: false
|
|
|
13
13
|
|
|
14
14
|
Також Dockerfile/Containerfile **має бути multistage build**: окремий build stage (залежності/компіляція) і окремий runtime stage. У фінальному stage дозволені лише мінімальні базові образи:
|
|
15
15
|
|
|
16
|
-
- **backend**: `mirror.gcr.io/library/alpine:*`
|
|
17
|
-
-
|
|
16
|
+
- **backend (типово)**: `mirror.gcr.io/library/alpine:*`
|
|
17
|
+
- **ультра-легкі (glibc / одна статична збірка)**: `scratch` — тільки як `FROM scratch` (офіційний порожній базовий шар), коли весь **runtime** уже в `COPY --from=…`
|
|
18
|
+
- **glibc, Debian (slim)**: `mirror.gcr.io/library/debian:*` **лише** з тегом, у якому є `slim` (наприклад `bookworm-slim`, `trixie-slim`), а не `bookworm` без `slim`
|
|
19
|
+
- **виняток (інтерпретовані стеки)**: `mirror.gcr.io/library/php:*` або `mirror.gcr.io/library/python:*` — якщо сервіс має крутитися в офіційному runtime PHP чи Python, а не як один бінарник на Alpine; інакше лишай **alpine** у фінальному stage
|
|
20
|
+
- **frontend**: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
|
|
18
21
|
|
|
19
|
-
Це
|
|
22
|
+
Це стримує зайвий build tooling (Bun, **node_modules** зі збірки) у фінальному образі; для **alpine** / **nginx** / **openresty** у **runtime** лишаються лише відповідні вимоги, для **php** / **python** (виняток) — цільовий інтерпретований **stack**; **scratch** і **debian** з тегом **`*slim*`** — коли glibc і мінімальне оточення Debian важливіші за musl в **alpine**.
|
|
20
23
|
|
|
21
24
|
## компіляція
|
|
22
25
|
|
|
@@ -182,7 +185,10 @@ jobs:
|
|
|
182
185
|
```yaml title=".hadolint.yaml"
|
|
183
186
|
ignored:
|
|
184
187
|
- DL3007
|
|
188
|
+
- DL3018
|
|
185
189
|
```
|
|
190
|
+
Де DL3007 - «Не використовуй тег latest у FROM»
|
|
191
|
+
Де DL3018 - «Піни версії пакетів у apk add»
|
|
186
192
|
|
|
187
193
|
Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
|
|
188
194
|
|
package/mdc/k8s.mdc
CHANGED
|
@@ -416,6 +416,8 @@ spec:
|
|
|
416
416
|
|
|
417
417
|
У маніфестах під **`k8s`** заборонено **`apiVersion: autoscaling/v1`** (legacy HPA з єдиною метрикою CPU). Мігруй **HorizontalPodAutoscaler** на **`autoscaling/v2`**: поле **`spec.metrics`** (замість **`spec.targetCPUUtilizationPercentage`**) з **`type: Resource`** і **`target.type: Utilization`** / **`AverageUtilization`** — підтримує декілька метрик і зовнішні метрики. `check k8s` падає на будь-якому документі з **`apiVersion: autoscaling/v1`**.
|
|
418
418
|
|
|
419
|
+
Ресурси **batch** (наприклад **CronJob**, **Job**): застаріле **`apiVersion: batch/v1beta1`** у файлах під **`k8s` під час `check k8s` переписується** на **`apiVersion: batch/v1`**.
|
|
420
|
+
|
|
419
421
|
```yaml
|
|
420
422
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
|
|
421
423
|
apiVersion: autoscaling/v2
|
package/package.json
CHANGED
package/scripts/auto-rules.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
export const AUTO_RULE_ORDER = Object.freeze([
|
|
22
22
|
'abie',
|
|
23
23
|
'bun',
|
|
24
|
+
'capacitor',
|
|
24
25
|
'docker',
|
|
25
26
|
'ga',
|
|
26
27
|
'graphql',
|
|
@@ -119,6 +120,7 @@ function updateDirFacts(dirName, facts) {
|
|
|
119
120
|
* @param {string} fileName базове імʼя файлу
|
|
120
121
|
* @param {string} relPath шлях відносно кореня
|
|
121
122
|
* @param {{
|
|
123
|
+
* hasCapacitorConfig: boolean,
|
|
122
124
|
* hasDockerfile: boolean,
|
|
123
125
|
* hasJsLikeSource: boolean,
|
|
124
126
|
* hasNginxDefaultTplFile: boolean,
|
|
@@ -129,6 +131,9 @@ function updateDirFacts(dirName, facts) {
|
|
|
129
131
|
* @returns {void}
|
|
130
132
|
*/
|
|
131
133
|
function updateFileFacts(fileName, relPath, facts) {
|
|
134
|
+
if (fileName === 'capacitor.config.json') {
|
|
135
|
+
facts.hasCapacitorConfig = true
|
|
136
|
+
}
|
|
132
137
|
if (fileName === 'Dockerfile' || fileName.startsWith('Dockerfile.')) {
|
|
133
138
|
facts.hasDockerfile = true
|
|
134
139
|
}
|
|
@@ -182,6 +187,7 @@ async function updateGqlFactFromFile(absPath, relPath, facts) {
|
|
|
182
187
|
* @param {string} absPath абсолютний шлях до файлу
|
|
183
188
|
* @param {string} root абсолютний шлях кореня
|
|
184
189
|
* @param {{
|
|
190
|
+
* hasCapacitorConfig: boolean,
|
|
185
191
|
* hasDockerfile: boolean,
|
|
186
192
|
* hasGqlTaggedTemplates: boolean,
|
|
187
193
|
* hasJsLikeSource: boolean,
|
|
@@ -261,6 +267,7 @@ export function isMonorepoPackage(packageJson) {
|
|
|
261
267
|
* Обходить дерево проєкту, збираючи факти для автоувімкнення правил.
|
|
262
268
|
* @param {string} root абсолютний шлях кореня репозиторію
|
|
263
269
|
* @returns {Promise<{
|
|
270
|
+
* hasCapacitorConfig: boolean,
|
|
264
271
|
* hasDockerfile: boolean,
|
|
265
272
|
* hasGaWorkflowsDir: boolean,
|
|
266
273
|
* hasGqlTaggedTemplates: boolean,
|
|
@@ -275,6 +282,7 @@ export function isMonorepoPackage(packageJson) {
|
|
|
275
282
|
*/
|
|
276
283
|
export async function collectAutoRuleFacts(root) {
|
|
277
284
|
const facts = {
|
|
285
|
+
hasCapacitorConfig: false,
|
|
278
286
|
hasDockerfile: false,
|
|
279
287
|
hasGaWorkflowsDir: existsSync(join(root, '.github', 'workflows')),
|
|
280
288
|
hasGqlTaggedTemplates: false,
|
|
@@ -386,6 +394,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
386
394
|
const autoRuleChecks = [
|
|
387
395
|
{ enabled: isAbie, id: 'abie' },
|
|
388
396
|
{ enabled: packageJsonExists, id: 'bun' },
|
|
397
|
+
{ enabled: facts.hasCapacitorConfig, id: 'capacitor' },
|
|
389
398
|
{ enabled: facts.hasDockerfile, id: 'docker' },
|
|
390
399
|
{ enabled: facts.hasGaWorkflowsDir, id: 'ga' },
|
|
391
400
|
{ enabled: facts.hasGqlTaggedTemplates, id: 'graphql' },
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє відповідність проєкту правилам capacitor.mdc для застосунків **Capacitor**.
|
|
3
|
+
*
|
|
4
|
+
* Якщо у репозиторії **немає** ознак Capacitor (див. наведене) — вихід **0**, перевірка не застосовується.
|
|
5
|
+
*
|
|
6
|
+
* **Ознака Capacitor:** наявні **`capacitor.config.json`**, **`capacitor.config.ts`**, **`capacitor.config.mjs`**
|
|
7
|
+
* (у корені) **або** у будь-якому `package.json` (рекурсивно, з пропуском типових каталогів) оголошено
|
|
8
|
+
* хоча б одну залежність **`@capacitor/…`** (у **`dependencies`**, **`devDependencies`**, опційно
|
|
9
|
+
* **`optionalDependencies`**, **`peerDependencies`**).
|
|
10
|
+
*
|
|
11
|
+
* **Версія мінімум 8:** у кожному `package.json`, де вказано **`@capacitor/core`**, діапазон версії
|
|
12
|
+
* мусить допускати лише **Capacitor 8+** (оцінка мінімального **major** з рядка діапазону npm, зокрема
|
|
13
|
+
* `||` і діапазонів через `-` у спрощеному вигляді). **`*`**, **latest** та нерозпізнані випадки — **порушення**:
|
|
14
|
+
* варто задати явний діапазон, наприклад **`^8.0.0`**. Якщо оголошено `capacitor.config.*` без жодного
|
|
15
|
+
* **`@capacitor/core`** у дереві `package.json` — також помилка.
|
|
16
|
+
*
|
|
17
|
+
* **iOS:** зазвичай **без** **Podfile** поза **Pods** (тільки **SPM**). **@nitra/**-плагіни за політикою **SPM** —
|
|
18
|
+
* їх **не** перелічуємо й **не** перевіряємо. Якщо **Podfile** є, його можна зареєструвати як **виняток**
|
|
19
|
+
* (див. **capacitor.mdc**): у кореневому **`package.json`** або
|
|
20
|
+
* **`capacitor.config.json` / `capacitor.config.ts` / `capacitor.config.mjs`**, об’єкт **nitra** з
|
|
21
|
+
* **`iosCocoaPodsBecausePluginsLackSpm: true`** (або **`iosCocoaPodsAllowed: true`**); тоді **Podfile** **не** fail.
|
|
22
|
+
* Якщо **`ios/`** **немає** — iOS-умови не застосовуються.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync } from 'node:fs'
|
|
25
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
26
|
+
import { join, relative } from 'node:path'
|
|
27
|
+
|
|
28
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
29
|
+
|
|
30
|
+
/** Мінімальна допустима мажорна версія Capacitor (capacitor.mdc) */
|
|
31
|
+
const MIN_CAPACITOR_MAJOR = 8
|
|
32
|
+
|
|
33
|
+
/** @type {Set<string>} */
|
|
34
|
+
const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
|
|
35
|
+
'node_modules',
|
|
36
|
+
'.git',
|
|
37
|
+
'dist',
|
|
38
|
+
'coverage',
|
|
39
|
+
'Pods',
|
|
40
|
+
'.turbo',
|
|
41
|
+
'.next',
|
|
42
|
+
'build'
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
/** `||` у діапазоні npm-версій */
|
|
46
|
+
// eslint-disable-next-line sonarjs/slow-regex -- короткі **semver**-підрядки у **package.json**
|
|
47
|
+
const NPM_OR_PARTS_RE = /\s*\|\|\s*/
|
|
48
|
+
|
|
49
|
+
/** `a - b` (діапазон діапазонів) */
|
|
50
|
+
// eslint-disable-next-line sonarjs/slow-regex -- форма **X - Y** у **npm**-range
|
|
51
|
+
const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
|
|
52
|
+
|
|
53
|
+
const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
|
|
54
|
+
|
|
55
|
+
const PREFIX_GEQ_RE = /^>=\s*/u
|
|
56
|
+
const PREFIX_GT_RE = /^>\s*/u
|
|
57
|
+
const STRIP_CARET_TILDE_EQ_RE = /^[=^~]+\s*/u
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Початок блока **nitra: {** у **.ts** / **.mjs** (**capacitor.config**; ключ **nitra**).
|
|
61
|
+
*/
|
|
62
|
+
const RE_NITRA_CONFIG_OBJECT_LEAD_IN = /\bnitra\b\s*:\s*\{|[\u0027"]nitra[\u0027"]\s*:\s*\{/
|
|
63
|
+
|
|
64
|
+
const RE_COCOAPODS_EXEMPT_SPM = /\biosCocoaPodsBecausePluginsLackSpm\s*:\s*true\b/
|
|
65
|
+
const RE_COCOAPODS_EXEMPT_ALLOW = /\biosCocoaPodsAllowed\s*:\s*true\b/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Мінімальний **major** (нижня межа) для **однієї** OR-частини діапазону npm (без `||` всередині).
|
|
69
|
+
* @param {string} segment одна частина після `||` або весь рядок
|
|
70
|
+
* @returns {number | null} null, якщо **`*` / `x` / `latest`**, або **major** **нижньої** межі
|
|
71
|
+
*/
|
|
72
|
+
export function capacitorSegmentMinMajor(segment) {
|
|
73
|
+
if (typeof segment !== 'string') {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
const s0 = segment.trim()
|
|
77
|
+
if (!s0) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
const low = s0.toLowerCase()
|
|
81
|
+
if (s0 === '*' || low === 'x' || low === 'latest') {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
if (s0.startsWith('<') || s0.startsWith('<=')) {
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
if (s0.startsWith('>') && !s0.startsWith('>=')) {
|
|
88
|
+
return firstVersionMajorFromNpmValue(s0.replace(PREFIX_GT_RE, ''))
|
|
89
|
+
}
|
|
90
|
+
const rangeHyphen = s0.match(NPM_HYPHEN_RANGE_RE)
|
|
91
|
+
if (rangeHyphen) {
|
|
92
|
+
return firstVersionMajorFromNpmValue(rangeHyphen[1].trim())
|
|
93
|
+
}
|
|
94
|
+
if (s0.startsWith('^') || s0.startsWith('~') || s0.startsWith('=')) {
|
|
95
|
+
return firstVersionMajorFromNpmValue(s0.replace(STRIP_CARET_TILDE_EQ_RE, ''))
|
|
96
|
+
}
|
|
97
|
+
if (s0.startsWith('>=')) {
|
|
98
|
+
return firstVersionMajorFromNpmValue(s0.replace(PREFIX_GEQ_RE, ''))
|
|
99
|
+
}
|
|
100
|
+
return firstVersionMajorFromNpmValue(s0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Витягує **major** з першого числа у вигляді **X** або **X.Y** / **X.Y.Z** (опційно **v**).
|
|
105
|
+
* @param {string} t рядок ділянки **версії** (без префікса **операторів**)
|
|
106
|
+
* @returns {number | null} перше **ціле** (major) або **null**
|
|
107
|
+
*/
|
|
108
|
+
function firstVersionMajorFromNpmValue(t) {
|
|
109
|
+
const s = t.trim()
|
|
110
|
+
if (!s) {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
const m = s.match(FIRST_VERSION_NUM_RE)
|
|
114
|
+
if (!m) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
return Number.parseInt(m[1], 10)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Мінімальна можлива (нижня) **major**-версія для повного діапазону npm, у т. ч. з `||`.
|
|
122
|
+
* @param {string} versionRange повне поле `package.json` для **@capacitor/core**
|
|
123
|
+
* @returns {number | null} **null** якщо **`*` / latest** в одній з частин
|
|
124
|
+
*/
|
|
125
|
+
export function capacitorVersionRangeMinMajor(versionRange) {
|
|
126
|
+
if (typeof versionRange !== 'string') {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
const parts = versionRange.split(NPM_OR_PARTS_RE)
|
|
130
|
+
let overallMin = /** @type {number | null} */ (null)
|
|
131
|
+
for (const p of parts) {
|
|
132
|
+
const m = capacitorSegmentMinMajor(p)
|
|
133
|
+
if (m === null) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
if (overallMin === null || m < overallMin) {
|
|
137
|
+
overallMin = m
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return overallMin
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} versionRange рядок **версії** з `package.json`
|
|
145
|
+
* @param {number} [min] мінімальний **major** (за замовчуванням `MIN_CAPACITOR_MAJOR`)
|
|
146
|
+
* @returns {boolean} **true**, якщо нижня межа **≥** **min**
|
|
147
|
+
*/
|
|
148
|
+
export function isCapacitorCoreVersionAtLeast8(versionRange, min = MIN_CAPACITOR_MAJOR) {
|
|
149
|
+
const low = capacitorVersionRangeMinMajor(versionRange)
|
|
150
|
+
if (low === null) {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
return low >= min
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {(m: string) => void} fail друк помилки
|
|
158
|
+
* @param {(m: string) => void} pass друк успіху
|
|
159
|
+
* @param {string} rel відносний **posix**-шлях `package.json`
|
|
160
|
+
* @param {string} range поле `version` для **@capacitor/core**
|
|
161
|
+
* @returns {void}
|
|
162
|
+
*/
|
|
163
|
+
function reportOneCapacitorCoreRange(fail, pass, rel, range) {
|
|
164
|
+
if (isCapacitorCoreVersionAtLeast8(range)) {
|
|
165
|
+
pass(`«${rel}»: @capacitor/core — діапазон сумісний з ${MIN_CAPACITOR_MAJOR}+`)
|
|
166
|
+
} else {
|
|
167
|
+
fail(
|
|
168
|
+
`«${rel}»: @capacitor/core «${range}» — мінімальна допустима мажорна версія Capacitor ${MIN_CAPACITOR_MAJOR} (capacitor.mdc). Вкажи, наприклад, ^${MIN_CAPACITOR_MAJOR}.0.0`
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} rel відносний шлях `package.json`
|
|
175
|
+
* @param {Record<string, unknown>} obj `dependencies` / `devDependencies` / **…**
|
|
176
|
+
* @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач **@capacitor** у дереві
|
|
177
|
+
*/
|
|
178
|
+
function recordCapacitorFromDependencyObject(rel, obj, out) {
|
|
179
|
+
for (const [name, val] of Object.entries(obj)) {
|
|
180
|
+
if (typeof name === 'string' && name.startsWith('@capacitor/')) {
|
|
181
|
+
out.anyCapacitor = true
|
|
182
|
+
}
|
|
183
|
+
if (name === '@capacitor/core' && typeof val === 'string' && val !== '') {
|
|
184
|
+
out.byPath.set(rel, val)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} absPath шлях до `package.json`
|
|
191
|
+
* @param {string} root корінь репозиторію
|
|
192
|
+
* @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач **byPath** і **anyCapacitor**
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
export async function recordCapacitorFromOnePackageJson(absPath, root, out) {
|
|
196
|
+
let raw
|
|
197
|
+
try {
|
|
198
|
+
raw = await readFile(absPath, 'utf8')
|
|
199
|
+
} catch {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
let pkg
|
|
203
|
+
try {
|
|
204
|
+
pkg = JSON.parse(raw)
|
|
205
|
+
} catch {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const rel = (relative(root, absPath) || absPath).replaceAll('\\', '/')
|
|
209
|
+
for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
210
|
+
const rec = pkg?.[block]
|
|
211
|
+
if (rec !== null && rec !== undefined && typeof rec === 'object' && !Array.isArray(rec)) {
|
|
212
|
+
recordCapacitorFromDependencyObject(rel, /** @type {Record<string, unknown>} */ (rec), out)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Зчитує всі `package.json` з дерева, накопичує `byPath` і `anyCapacitor`.
|
|
219
|
+
* @param {string} root корінь репозиторію
|
|
220
|
+
* @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач
|
|
221
|
+
* @returns {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
export async function collectCapacitorDataFromAllPackageJson(root, out) {
|
|
224
|
+
out.anyCapacitor = false
|
|
225
|
+
if (out.byPath) {
|
|
226
|
+
out.byPath.clear()
|
|
227
|
+
} else {
|
|
228
|
+
out.byPath = new Map()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {string} dir абсолютний **каталог** для `readdir`
|
|
233
|
+
* @returns {Promise<void>}
|
|
234
|
+
*/
|
|
235
|
+
async function walk(dir) {
|
|
236
|
+
let entries
|
|
237
|
+
try {
|
|
238
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
239
|
+
} catch {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
const absPath = join(dir, entry.name)
|
|
244
|
+
if (entry.isDirectory() && !IGNORED_DIRS_FOR_PACKAGE_JSON.has(entry.name)) {
|
|
245
|
+
await walk(absPath)
|
|
246
|
+
} else if (entry.isFile() && entry.name === 'package.json') {
|
|
247
|
+
await recordCapacitorFromOnePackageJson(absPath, root, out)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await walk(root)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {string} root абсолютний або **cwd**-відносний **корінь** репозиторію
|
|
257
|
+
* @returns {boolean} **true** якщо `capacitor.config.{json,ts,mjs}` існує
|
|
258
|
+
*/
|
|
259
|
+
export function hasCapacitorConfigInRoot(root) {
|
|
260
|
+
return (
|
|
261
|
+
existsSync(join(root, 'capacitor.config.json')) ||
|
|
262
|
+
existsSync(join(root, 'capacitor.config.ts')) ||
|
|
263
|
+
existsSync(join(root, 'capacitor.config.mjs'))
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Чи варто застосовувати правила: конфіг **або** **@capacitor/** у залежностях.
|
|
269
|
+
* @param {string} root корінь
|
|
270
|
+
* @param {boolean} anyCapacitor чи зустрілось **@capacitor/** у **package.json**
|
|
271
|
+
* @returns {boolean} **true** якщо застосовуємо **check capacitor**
|
|
272
|
+
*/
|
|
273
|
+
export function isCapacitorRelevantForCheck(root, anyCapacitor) {
|
|
274
|
+
return hasCapacitorConfigInRoot(root) || anyCapacitor
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Рекурсивно шукає `Podfile` у **ios/**, **не** заходячи в **Pods** (кеш CocoaPods) і типові build-каталоги.
|
|
279
|
+
* @param {string} root корінь репозиторію
|
|
280
|
+
* @param {string} dir абсолютний каталог
|
|
281
|
+
* @param {(rel: string) => void} onPodfileRelative **callback** з **posix**-шляхом `Podfile` від **root**
|
|
282
|
+
* @returns {Promise<boolean>} **true** — знайдено **хоча б один** `Podfile`
|
|
283
|
+
*/
|
|
284
|
+
export async function walkIosForPodfileSkipPods(root, dir, onPodfileRelative) {
|
|
285
|
+
let entries
|
|
286
|
+
try {
|
|
287
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
288
|
+
} catch {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
for (const e of entries) {
|
|
292
|
+
if (e.name === 'Pods' || e.name === 'build' || e.name === 'DerivedData') {
|
|
293
|
+
// skip
|
|
294
|
+
} else if (e.isFile() === true && e.name === 'Podfile') {
|
|
295
|
+
const abs = join(dir, e.name)
|
|
296
|
+
onPodfileRelative((relative(root, abs) || abs).replaceAll('\\', '/'))
|
|
297
|
+
return true
|
|
298
|
+
} else if (e.isDirectory() === true) {
|
|
299
|
+
const abs = join(dir, e.name)
|
|
300
|
+
const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
|
|
301
|
+
if (found === true) {
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} root корінь
|
|
311
|
+
* @returns {Promise<string | null>} **relative**-шлях `Podfile` (або **null**)
|
|
312
|
+
*/
|
|
313
|
+
export async function findFirstPodfileUnderIosExcludingPods(root) {
|
|
314
|
+
const iosDir = join(root, 'ios')
|
|
315
|
+
if (!existsSync(iosDir)) {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
let first = /** @type {string | null} */ (null)
|
|
319
|
+
await walkIosForPodfileSkipPods(root, iosDir, rel => {
|
|
320
|
+
if (first === null || rel.length < first.length) {
|
|
321
|
+
first = rel
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
return first
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Чи дозволяє об’єкт **nitra** використання **Podfile** (CocoaPods) на iOS (див. **capacitor.mdc**; **@nitra/**
|
|
329
|
+
* **SPM** **не** аналізуємо).
|
|
330
|
+
* @param {unknown} o об’єкт **nitra** з **package.json** / **capacitor.config**
|
|
331
|
+
* @returns {boolean} **true** якщо **Podfile** дозволено (один з прапорів **true**)
|
|
332
|
+
*/
|
|
333
|
+
export function nitrAObjectAllowsIosCocoaPods(o) {
|
|
334
|
+
if (o === null || o === undefined || typeof o !== 'object' || Array.isArray(o)) {
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
const r = /** @type {Record<string, unknown>} */ (o)
|
|
338
|
+
if (r.iosCocoaPodsBecausePluginsLackSpm === true) {
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
if (r.iosCocoaPodsAllowed === true) {
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Витягає вихідний текст тіла **{ ... }** після **nitra:** / **"nitra":** у **config**-файлі (**ts** / **mjs**)
|
|
349
|
+
* (перша відповідність, баланс фігурних дужок).
|
|
350
|
+
* @param {string} source вміст **.ts** / **.mjs** config
|
|
351
|
+
* @returns {string | null} фрагмент **`{...}`** після **nitra:** або **null**
|
|
352
|
+
*/
|
|
353
|
+
function extractNitraObjectBodySource(source) {
|
|
354
|
+
const m = RE_NITRA_CONFIG_OBJECT_LEAD_IN.exec(source)
|
|
355
|
+
if (!m) {
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
const openBrace = m.index + m[0].length - 1
|
|
359
|
+
let d = 0
|
|
360
|
+
for (let i = openBrace; i < source.length; i += 1) {
|
|
361
|
+
const c = source[i]
|
|
362
|
+
if (c === '{') {
|
|
363
|
+
d += 1
|
|
364
|
+
} else if (c === '}') {
|
|
365
|
+
d -= 1
|
|
366
|
+
if (d === 0) {
|
|
367
|
+
return source.slice(openBrace, i + 1)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return null
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @param {string} objectBody фрагмент **`{ ... }`** (текст)
|
|
376
|
+
* @returns {boolean} **true**, якщо в тілі є **iosCocoaPods**…**:** **true**
|
|
377
|
+
*/
|
|
378
|
+
function nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody) {
|
|
379
|
+
return (
|
|
380
|
+
RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @param {string} absPath повний шлях до **JSON**-файла
|
|
386
|
+
* @returns {Promise<boolean>} **true**, якщо `obj.nitra` (ключ **nitra** у JSON) — виняток
|
|
387
|
+
*/
|
|
388
|
+
async function pathJsonShowsNitraCocoapodsExempt(absPath) {
|
|
389
|
+
if (existsSync(absPath) !== true) {
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const t = await readFile(absPath, 'utf8')
|
|
394
|
+
const j = /** @type {Record<string, unknown>} */ (JSON.parse(t))
|
|
395
|
+
return nitrAObjectAllowsIosCocoaPods(j['nitra']) === true
|
|
396
|
+
} catch {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @param {string} root корінь репозиторію
|
|
403
|
+
* @returns {Promise<boolean>} **true**, якщо **.ts** / **.mjs** містить валідний виняток **nitra**
|
|
404
|
+
*/
|
|
405
|
+
async function capacitorConfigTsMjsNitraCocoapodsExempt(root) {
|
|
406
|
+
for (const name of ['capacitor.config.ts', 'capacitor.config.mjs']) {
|
|
407
|
+
const p = join(root, name)
|
|
408
|
+
if (existsSync(p) === true) {
|
|
409
|
+
const text = await readFile(p, 'utf8')
|
|
410
|
+
const body = extractNitraObjectBodySource(text)
|
|
411
|
+
if (body !== null && nitraObjectBodyStringAllowsCocoaPodsExempt(body) === true) {
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return false
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @param {string} root корінь репозиторію (**process.cwd()** у **check**)
|
|
421
|
+
* @returns {Promise<boolean>} **true** якщо **Podfile** виправдано **nitra**-конфігом
|
|
422
|
+
*/
|
|
423
|
+
async function isIosCocoaPodsExemptByNitraConfig(root) {
|
|
424
|
+
if ((await pathJsonShowsNitraCocoapodsExempt(join(root, 'package.json'))) === true) {
|
|
425
|
+
return true
|
|
426
|
+
}
|
|
427
|
+
if ((await pathJsonShowsNitraCocoapodsExempt(join(root, 'capacitor.config.json'))) === true) {
|
|
428
|
+
return true
|
|
429
|
+
}
|
|
430
|
+
return capacitorConfigTsMjsNitraCocoapodsExempt(root)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* @returns {Promise<number>} **0** — **ok**; **1** — **fail** (див. **capacitor.mdc**)
|
|
435
|
+
*/
|
|
436
|
+
export async function check() {
|
|
437
|
+
const reporter = createCheckReporter()
|
|
438
|
+
const { pass, fail, getExitCode } = reporter
|
|
439
|
+
const root = process.cwd()
|
|
440
|
+
|
|
441
|
+
const acc = { byPath: new Map(), anyCapacitor: false }
|
|
442
|
+
await collectCapacitorDataFromAllPackageJson(root, acc)
|
|
443
|
+
const { byPath, anyCapacitor } = acc
|
|
444
|
+
|
|
445
|
+
if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
|
|
446
|
+
pass('Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено')
|
|
447
|
+
return getExitCode()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
pass('Проєкт з ознаками Capacitor — застосовую capacitor.mdc')
|
|
451
|
+
|
|
452
|
+
if (byPath.size === 0) {
|
|
453
|
+
fail(
|
|
454
|
+
`додай залежність @capacitor/core з діапазоном ^${MIN_CAPACITOR_MAJOR}.0.0 (або іншим, сумісним лише з ${MIN_CAPACITOR_MAJOR}+) у package.json (capacitor.mdc)`
|
|
455
|
+
)
|
|
456
|
+
} else {
|
|
457
|
+
for (const [rel, range] of byPath) {
|
|
458
|
+
reportOneCapacitorCoreRange(fail, pass, rel, range)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const podfileRel = await findFirstPodfileUnderIosExcludingPods(root)
|
|
463
|
+
if (podfileRel === null) {
|
|
464
|
+
if (existsSync(join(root, 'ios'))) {
|
|
465
|
+
pass('ios/ без Podfile поза Pods/ (лише SPM, capacitor.mdc)')
|
|
466
|
+
} else {
|
|
467
|
+
pass('каталог ios/ не знайдено — вимогу iOS/SPM пропущено')
|
|
468
|
+
}
|
|
469
|
+
} else if ((await isIosCocoaPodsExemptByNitraConfig(root)) === true) {
|
|
470
|
+
pass(
|
|
471
|
+
`iOS: Podfile «${podfileRel}» — дозволено виняток (nitra.iosCocoaPodsBecausePluginsLackSpm / iosCocoaPodsAllowed, capacitor.mdc)`
|
|
472
|
+
)
|
|
473
|
+
} else {
|
|
474
|
+
fail(
|
|
475
|
+
`iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods, або додай виняток nitra (capacitor.mdc)`
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return getExitCode()
|
|
480
|
+
}
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* вказуються через `mirror.gcr.io` (див. `utils/docker-mirror.mjs`).
|
|
6
6
|
*
|
|
7
7
|
* Також перевіряє, що Dockerfile/Containerfile має **multistage build** і що фінальний stage
|
|
8
|
-
* використовує
|
|
9
|
-
* - backend: `mirror.gcr.io/library/alpine
|
|
8
|
+
* використовує дозволений runtime-образ (див. docker.mdc):
|
|
9
|
+
* - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
|
|
10
|
+
* містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
|
|
11
|
+
* `mirror.gcr.io/library/python:*`
|
|
10
12
|
* - frontend: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
|
|
11
13
|
*
|
|
12
14
|
* Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
* фінальному stage не повинно залишатися build tooling (Bun/Node).
|
|
15
17
|
*
|
|
16
18
|
* Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
|
|
17
|
-
* runtime (alpine
|
|
19
|
+
* дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
|
|
18
20
|
*
|
|
19
21
|
* Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
|
|
20
22
|
* тощо. Спочатку hadolint з PATH, інакше docker run з образом hadolint/hadolint.
|
|
@@ -84,10 +86,31 @@ export function parseFromStages(fileContent) {
|
|
|
84
86
|
|
|
85
87
|
const RUNTIME_IMAGES = /** @type {const} */ ([
|
|
86
88
|
'mirror.gcr.io/library/alpine',
|
|
89
|
+
'mirror.gcr.io/library/php',
|
|
90
|
+
'mirror.gcr.io/library/python',
|
|
87
91
|
'mirror.gcr.io/library/nginx',
|
|
88
92
|
'mirror.gcr.io/openresty/openresty'
|
|
89
93
|
])
|
|
90
94
|
|
|
95
|
+
/** @type {RegExp} */
|
|
96
|
+
const DEBIAN_VIA_MIRROR_RE = /^mirror\.gcr\.io\/library\/debian:(.+)$/i
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Чи ref фінального `FROM` відповідає дозволеним у docker.mdc (multistage / runtime).
|
|
100
|
+
* @param {string} lastLower ref без digest, lower case
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
function isAllowedFinalRuntimeImage(lastLower) {
|
|
104
|
+
if (lastLower === 'scratch' || lastLower.startsWith('scratch:')) {
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
const deb = lastLower.match(DEBIAN_VIA_MIRROR_RE)
|
|
108
|
+
if (deb) {
|
|
109
|
+
return deb[1].toLowerCase().includes('slim')
|
|
110
|
+
}
|
|
111
|
+
return RUNTIME_IMAGES.some(img => lastLower.startsWith(`${img}:`) || lastLower === img)
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
/**
|
|
92
115
|
* Розбиває Dockerfile на stages за `FROM` (порожній масив, якщо FROM немає).
|
|
93
116
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
@@ -113,7 +136,7 @@ export function splitDockerfileStages(fileContent) {
|
|
|
113
136
|
/**
|
|
114
137
|
* Перевіряє базові вимоги до структури Dockerfile:
|
|
115
138
|
* - multistage: мінімум 2 FROM
|
|
116
|
-
* - фінальний FROM: alpine
|
|
139
|
+
* - фінальний FROM: дозволені образи в docker.mdc (alpine, scratch, debian slim, php, python, nginx, openresty, …)
|
|
117
140
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
118
141
|
* @returns {string | null} повідомлення помилки або null
|
|
119
142
|
*/
|
|
@@ -129,9 +152,8 @@ export function getMultistageAndRuntimeHint(fileContent) {
|
|
|
129
152
|
const lastImage = (last?.image || '').split('@')[0] || ''
|
|
130
153
|
const lastLower = lastImage.toLowerCase()
|
|
131
154
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return `фінальний FROM має бути ${RUNTIME_IMAGES.join(' або ')} (runtime stage), зараз: ${last?.image} (рядок ${last?.line})`
|
|
155
|
+
if (!isAllowedFinalRuntimeImage(lastLower)) {
|
|
156
|
+
return `фінальний FROM має бути дозволеним runtime-образом (див. docker.mdc: multistage), зараз: ${last?.image} (рядок ${last?.line})`
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
return null
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
*
|
|
24
24
|
* **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
|
|
25
25
|
* **`apiVersion: autoscaling/v1`** заборонено (мігруй **HorizontalPodAutoscaler** на **`autoscaling/v2`**).
|
|
26
|
+
* Рядок **`apiVersion: batch/v1beta1`** (CronJob, Job) **автоматично** переписується на **`apiVersion: batch/v1`**
|
|
27
|
+
* (окрім рядків-коментарів і рядків, де після значення йде наприклад `# …`).
|
|
26
28
|
*
|
|
27
29
|
* Файли під **`k8s`**, де всі YAML-документи — лише **`kind: BackendConfig`**, **видаляються** автоматично.
|
|
28
30
|
* Якщо **BackendConfig** змішано з іншими ресурсами в одному файлі — перевірка завершується помилкою (розділи маніфести).
|
|
@@ -93,7 +95,7 @@
|
|
|
93
95
|
* не з’явиться `Deployment` (k8s.mdc).
|
|
94
96
|
*/
|
|
95
97
|
import { existsSync } from 'node:fs'
|
|
96
|
-
import { readFile, readdir, stat, unlink } from 'node:fs/promises'
|
|
98
|
+
import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
97
99
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
98
100
|
|
|
99
101
|
import { parseAllDocuments } from 'yaml'
|
|
@@ -1561,6 +1563,73 @@ async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
|
|
|
1561
1563
|
}
|
|
1562
1564
|
}
|
|
1563
1565
|
|
|
1566
|
+
/**
|
|
1567
|
+
* Один рядок YAML: якщо це `apiVersion` зі значенням **`batch/v1beta1`**, повертає той самий рядок із **`batch/v1`**
|
|
1568
|
+
* (з тими самими відступами/пробілами після `apiVersion:`, крім випадків з лапками — нормалізується до `apiVersion: batch/v1`).
|
|
1569
|
+
* Рядки, що після trim починаються з `#`, не змінюються.
|
|
1570
|
+
* @param {string} line
|
|
1571
|
+
* @returns {string}
|
|
1572
|
+
*/
|
|
1573
|
+
function rewriteLineBatchV1beta1ApiVersion(line) {
|
|
1574
|
+
const t = line.trimStart()
|
|
1575
|
+
if (t.startsWith('#')) {
|
|
1576
|
+
return line
|
|
1577
|
+
}
|
|
1578
|
+
const m = line.match(/^(\s*apiVersion:\s*)(?:"|')?batch\/v1beta1(?:"|')?(\s*)$/)
|
|
1579
|
+
if (m) {
|
|
1580
|
+
return `${m[1]}batch/v1${m[2]}`
|
|
1581
|
+
}
|
|
1582
|
+
return line
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* У повному тексті YAML замінює всі **цілі** рядки `apiVersion: batch/v1beta1` (за потреби в лапках) на `apiVersion: batch/v1`.
|
|
1587
|
+
* Зберігає **CRLF** / **LF** як у вихідному рядку.
|
|
1588
|
+
* @param {string} raw вміст файлу
|
|
1589
|
+
* @returns {{ changed: boolean, content: string }}
|
|
1590
|
+
*/
|
|
1591
|
+
export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
|
|
1592
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
1593
|
+
const lines = raw.split(/\r?\n/)
|
|
1594
|
+
let changed = false
|
|
1595
|
+
const out = lines.map(line => {
|
|
1596
|
+
const n = rewriteLineBatchV1beta1ApiVersion(line)
|
|
1597
|
+
if (n !== line) {
|
|
1598
|
+
changed = true
|
|
1599
|
+
}
|
|
1600
|
+
return n
|
|
1601
|
+
})
|
|
1602
|
+
if (!changed) {
|
|
1603
|
+
return { changed: false, content: raw }
|
|
1604
|
+
}
|
|
1605
|
+
return { changed: true, content: out.join(eol) }
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Проходить усі `*.yaml` / `*.yml` під сегментом `k8s` і на диску застосовує **`replaceBatchV1beta1ApiVersionInYamlText`**.
|
|
1610
|
+
* @param {string} root корінь репозиторію
|
|
1611
|
+
* @param {(msg: string) => void} fail
|
|
1612
|
+
* @param {(msg: string) => void} pass
|
|
1613
|
+
* @returns {Promise<void>}
|
|
1614
|
+
*/
|
|
1615
|
+
async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass) {
|
|
1616
|
+
const yamlFiles = await findK8sYamlFiles(root)
|
|
1617
|
+
for (const abs of yamlFiles) {
|
|
1618
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
1619
|
+
try {
|
|
1620
|
+
const raw = await readFile(abs, 'utf8')
|
|
1621
|
+
const { changed, content } = replaceBatchV1beta1ApiVersionInYamlText(raw)
|
|
1622
|
+
if (changed) {
|
|
1623
|
+
await writeFile(abs, content, 'utf8')
|
|
1624
|
+
pass(`${rel}: оновлено apiVersion batch/v1beta1 → batch/v1 (k8s.mdc)`)
|
|
1625
|
+
}
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1628
|
+
fail(`${rel}: не вдалося прочитати/записати при заміні batch/v1beta1 → batch/v1 (${msg})`)
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1564
1633
|
/**
|
|
1565
1634
|
* Прибирає BOM і ділить на рядки.
|
|
1566
1635
|
* @param {string} content вміст файлу
|
|
@@ -4819,6 +4888,8 @@ export async function check() {
|
|
|
4819
4888
|
|
|
4820
4889
|
const root = process.cwd()
|
|
4821
4890
|
|
|
4891
|
+
await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass)
|
|
4892
|
+
|
|
4822
4893
|
await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
|
|
4823
4894
|
|
|
4824
4895
|
const yamlFiles = await findK8sYamlFiles(root)
|
package/scripts/check-text.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* VSCode (formatOnSave, defaultFormatter для js/ts/json/vue/css/html),
|
|
6
6
|
* відсутність Prettier у конфігах і залежностях.
|
|
7
7
|
*
|
|
8
|
+
* cspell: `.cspell.json` з обовʼязковим набором `ignorePaths` (клон text.mdc: node_modules, vscode, git, report, svg, k8s yaml);
|
|
8
9
|
* cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json); у кореневих **`devDependencies`**
|
|
9
10
|
* дозволені лише **`@nitra/*`** (як у bun.mdc), зокрема **`@nitra/cspell-dict` ^2.0.0+**; без імпорту **`@cspell/dict-*`** у `.cspell.json`, заборона
|
|
10
11
|
* `markdownlint-cli2` у dependencies/devDependencies, v8r (`run-v8r.mjs` або чотири `bunx v8r`),
|
|
@@ -31,6 +32,17 @@ const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
|
|
|
31
32
|
/** Мінімальні glob-и в `ignorePatterns` у `.oxfmtrc.json` (text.mdc). */
|
|
32
33
|
const OXFMT_REQUIRED_IGNORE_PATTERNS = ['**/hasura/metadata/**', '**/schema.graphql']
|
|
33
34
|
|
|
35
|
+
/** Канонічні записи `ignorePaths` у `.cspell.json` (text.mdc) — кожен має бути присутнім. */
|
|
36
|
+
const CSPELL_REQUIRED_IGNORE_PATHS = [
|
|
37
|
+
'**/node_modules/**',
|
|
38
|
+
'**/vscode-extension/**',
|
|
39
|
+
'**/.git/**',
|
|
40
|
+
'.vscode',
|
|
41
|
+
'report',
|
|
42
|
+
'*.svg',
|
|
43
|
+
'**/k8s/**/*.yaml',
|
|
44
|
+
]
|
|
45
|
+
|
|
34
46
|
/**
|
|
35
47
|
* Чи діапазон версії `@nitra/cspell-dict` у package.json означає лінію 2.0.0+ (з цієї версії словники входять у пакет).
|
|
36
48
|
* @param {string|undefined} range наприклад "^2.0.0"
|
|
@@ -384,6 +396,14 @@ async function checkCspellConfig(pass, fail) {
|
|
|
384
396
|
} else {
|
|
385
397
|
fail('.cspell.json не містить ignorePaths')
|
|
386
398
|
}
|
|
399
|
+
if (Array.isArray(cfg.ignorePaths)) {
|
|
400
|
+
const missing = CSPELL_REQUIRED_IGNORE_PATHS.filter(p => !cfg.ignorePaths.includes(p))
|
|
401
|
+
if (missing.length === 0) {
|
|
402
|
+
pass(`.cspell.json ignorePaths містить усі обовʼязкові glob-и з text.mdc`)
|
|
403
|
+
} else {
|
|
404
|
+
fail(`.cspell.json ignorePaths бракує за замовчанням: ${missing.join(', ')} (див. text.mdc)`)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
387
407
|
}
|
|
388
408
|
|
|
389
409
|
/**
|