@nitra/cursor 1.8.132 → 1.8.135
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 +18 -0
- package/mdc/k8s.mdc +2 -0
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +9 -0
- package/scripts/check-capacitor.mjs +350 -0
- package/scripts/check-k8s.mjs +72 -1
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,18 @@
|
|
|
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
|
|
15
|
+
|
|
16
|
+
Нативний iOS-шар **не** повинен використовувати **CocoaPods** (наприклад файл **`Podfile`**
|
|
17
|
+
поза каталогом `Pods/`) — **Swift Package Manager (SPM)**. Якщо каталога **`ios/`** немає, перевірка iOS
|
|
18
|
+
у цьому кроці пропускається.
|
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,350 @@
|
|
|
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 лише через SPM (Swift Package Manager):** якщо в корні є каталог **`ios/`** — у ньому **не** має
|
|
18
|
+
* бути файлів **Podfile** (CocoaPods) **поза** каталогом **Pods** (тобто не використовувати **Podfile**
|
|
19
|
+
* у вихідному iOS-шарі; присутній **Podfile** — порушення). Якщо **немає** `ios/` — вимогу iOS у цьому
|
|
20
|
+
* прогоні пропущено.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync } from 'node:fs'
|
|
23
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
24
|
+
import { join, relative } from 'node:path'
|
|
25
|
+
|
|
26
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
27
|
+
|
|
28
|
+
/** Мінімальна допустима мажорна версія Capacitor (capacitor.mdc) */
|
|
29
|
+
const MIN_CAPACITOR_MAJOR = 8
|
|
30
|
+
|
|
31
|
+
/** @type {Set<string>} */
|
|
32
|
+
const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
|
|
33
|
+
'node_modules',
|
|
34
|
+
'.git',
|
|
35
|
+
'dist',
|
|
36
|
+
'coverage',
|
|
37
|
+
'Pods',
|
|
38
|
+
'.turbo',
|
|
39
|
+
'.next',
|
|
40
|
+
'build'
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
/** `||` у діапазоні npm-версій */
|
|
44
|
+
const NPM_OR_PARTS_RE = /\s*\|\|\s*/
|
|
45
|
+
|
|
46
|
+
/** `a - b` (діапазон діапазонів) */
|
|
47
|
+
const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
|
|
48
|
+
|
|
49
|
+
const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
|
|
50
|
+
|
|
51
|
+
const PREFIX_GEQ_RE = /^>=\s*/u
|
|
52
|
+
const PREFIX_GT_RE = /^>\s*/u
|
|
53
|
+
const STRIP_CARET_TILDE_EQ_RE = /^[=^~]+\s*/u
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Мінімальний **major** (нижня межа) для **однієї** OR-частини діапазону npm (без `||` всередині).
|
|
57
|
+
* @param {string} segment одна частина після `||` або весь рядок
|
|
58
|
+
* @returns {number | null} null, якщо **`*` / `x` / `latest`**, або **major** **нижньої** межі
|
|
59
|
+
*/
|
|
60
|
+
export function capacitorSegmentMinMajor(segment) {
|
|
61
|
+
if (typeof segment !== 'string') {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
const s0 = segment.trim()
|
|
65
|
+
if (!s0) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
const low = s0.toLowerCase()
|
|
69
|
+
if (s0 === '*' || low === 'x' || low === 'latest') {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
if (s0.startsWith('<') || s0.startsWith('<=')) {
|
|
73
|
+
return 0
|
|
74
|
+
}
|
|
75
|
+
if (s0.startsWith('>') && !s0.startsWith('>=')) {
|
|
76
|
+
return firstVersionMajorFromNpmValue(s0.replace(PREFIX_GT_RE, ''))
|
|
77
|
+
}
|
|
78
|
+
const rangeHyphen = s0.match(NPM_HYPHEN_RANGE_RE)
|
|
79
|
+
if (rangeHyphen) {
|
|
80
|
+
return firstVersionMajorFromNpmValue(rangeHyphen[1].trim())
|
|
81
|
+
}
|
|
82
|
+
if (s0.startsWith('^') || s0.startsWith('~') || s0.startsWith('=')) {
|
|
83
|
+
return firstVersionMajorFromNpmValue(s0.replace(STRIP_CARET_TILDE_EQ_RE, ''))
|
|
84
|
+
}
|
|
85
|
+
if (s0.startsWith('>=')) {
|
|
86
|
+
return firstVersionMajorFromNpmValue(s0.replace(PREFIX_GEQ_RE, ''))
|
|
87
|
+
}
|
|
88
|
+
return firstVersionMajorFromNpmValue(s0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Витягує **major** з першого числа у вигляді **X** або **X.Y** / **X.Y.Z** (опційно **v**).
|
|
93
|
+
* @param {string} t рядок ділянки **версії** (без префікса **операторів**)
|
|
94
|
+
* @returns {number | null} перше **ціле** (major) або **null**
|
|
95
|
+
*/
|
|
96
|
+
function firstVersionMajorFromNpmValue(t) {
|
|
97
|
+
const s = t.trim()
|
|
98
|
+
if (!s) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
const m = s.match(FIRST_VERSION_NUM_RE)
|
|
102
|
+
if (!m) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
return Number.parseInt(m[1], 10)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Мінімальна можлива (нижня) **major**-версія для повного діапазону npm, у т. ч. з `||`.
|
|
110
|
+
* @param {string} versionRange повне поле `package.json` для **@capacitor/core**
|
|
111
|
+
* @returns {number | null} **null** якщо **`*` / latest** в одній з частин
|
|
112
|
+
*/
|
|
113
|
+
export function capacitorVersionRangeMinMajor(versionRange) {
|
|
114
|
+
if (typeof versionRange !== 'string') {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
const parts = versionRange.split(NPM_OR_PARTS_RE)
|
|
118
|
+
let overallMin = /** @type {number | null} */ (null)
|
|
119
|
+
for (const p of parts) {
|
|
120
|
+
const m = capacitorSegmentMinMajor(p)
|
|
121
|
+
if (m === null) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
if (overallMin === null || m < overallMin) {
|
|
125
|
+
overallMin = m
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return overallMin
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} versionRange рядок **версії** з `package.json`
|
|
133
|
+
* @param {number} [min] мінімальний **major** (за замовчуванням `MIN_CAPACITOR_MAJOR`)
|
|
134
|
+
* @returns {boolean} **true**, якщо нижня межа **≥** **min**
|
|
135
|
+
*/
|
|
136
|
+
export function isCapacitorCoreVersionAtLeast8(versionRange, min = MIN_CAPACITOR_MAJOR) {
|
|
137
|
+
const low = capacitorVersionRangeMinMajor(versionRange)
|
|
138
|
+
if (low === null) {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
return low >= min
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {(m: string) => void} fail друк помилки
|
|
146
|
+
* @param {(m: string) => void} pass друк успіху
|
|
147
|
+
* @param {string} rel відносний **posix**-шлях `package.json`
|
|
148
|
+
* @param {string} range поле `version` для **@capacitor/core**
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
function reportOneCapacitorCoreRange(fail, pass, rel, range) {
|
|
152
|
+
if (isCapacitorCoreVersionAtLeast8(range)) {
|
|
153
|
+
pass(`«${rel}»: @capacitor/core — діапазон сумісний з ${MIN_CAPACITOR_MAJOR}+`)
|
|
154
|
+
} else {
|
|
155
|
+
fail(
|
|
156
|
+
`«${rel}»: @capacitor/core «${range}» — мінімальна допустима мажорна версія Capacitor ${MIN_CAPACITOR_MAJOR} (capacitor.mdc). Вкажи, наприклад, ^${MIN_CAPACITOR_MAJOR}.0.0`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} absPath шлях до `package.json`
|
|
163
|
+
* @param {string} root корінь репозиторію
|
|
164
|
+
* @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач **byPath** і **anyCapacitor**
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
export async function recordCapacitorFromOnePackageJson(absPath, root, out) {
|
|
168
|
+
let raw
|
|
169
|
+
try {
|
|
170
|
+
raw = await readFile(absPath, 'utf8')
|
|
171
|
+
} catch {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
let pkg
|
|
175
|
+
try {
|
|
176
|
+
pkg = JSON.parse(raw)
|
|
177
|
+
} catch {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
const rel = (relative(root, absPath) || absPath).replaceAll('\\', '/')
|
|
181
|
+
for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
182
|
+
const rec = pkg?.[block]
|
|
183
|
+
if (rec !== null && rec !== undefined && typeof rec === 'object' && !Array.isArray(rec)) {
|
|
184
|
+
const obj = /** @type {Record<string, unknown>} */ (rec)
|
|
185
|
+
for (const [name, val] of Object.entries(obj)) {
|
|
186
|
+
if (typeof name === 'string' && name.startsWith('@capacitor/')) {
|
|
187
|
+
out.anyCapacitor = true
|
|
188
|
+
}
|
|
189
|
+
if (name === '@capacitor/core' && typeof val === 'string' && val !== '') {
|
|
190
|
+
out.byPath.set(rel, val)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Зчитує всі `package.json` з дерева, накопичує `byPath` і `anyCapacitor`.
|
|
199
|
+
* @param {string} root корінь репозиторію
|
|
200
|
+
* @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач
|
|
201
|
+
* @returns {Promise<void>}
|
|
202
|
+
*/
|
|
203
|
+
export async function collectCapacitorDataFromAllPackageJson(root, out) {
|
|
204
|
+
out.anyCapacitor = false
|
|
205
|
+
if (out.byPath) {
|
|
206
|
+
out.byPath.clear()
|
|
207
|
+
} else {
|
|
208
|
+
out.byPath = new Map()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {string} dir абсолютний **каталог** для `readdir`
|
|
213
|
+
* @returns {Promise<void>}
|
|
214
|
+
*/
|
|
215
|
+
async function walk(dir) {
|
|
216
|
+
let entries
|
|
217
|
+
try {
|
|
218
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
219
|
+
} catch {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const absPath = join(dir, entry.name)
|
|
224
|
+
if (entry.isDirectory() && !IGNORED_DIRS_FOR_PACKAGE_JSON.has(entry.name)) {
|
|
225
|
+
await walk(absPath)
|
|
226
|
+
} else if (entry.isFile() && entry.name === 'package.json') {
|
|
227
|
+
await recordCapacitorFromOnePackageJson(absPath, root, out)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await walk(root)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {string} root абсолютний або **cwd**-відносний **корінь** репозиторію
|
|
237
|
+
* @returns {boolean} **true** якщо `capacitor.config.{json,ts,mjs}` існує
|
|
238
|
+
*/
|
|
239
|
+
export function hasCapacitorConfigInRoot(root) {
|
|
240
|
+
return (
|
|
241
|
+
existsSync(join(root, 'capacitor.config.json')) ||
|
|
242
|
+
existsSync(join(root, 'capacitor.config.ts')) ||
|
|
243
|
+
existsSync(join(root, 'capacitor.config.mjs'))
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Чи варто застосовувати правила: конфіг **або** **@capacitor/** у залежностях.
|
|
249
|
+
* @param {string} root корінь
|
|
250
|
+
* @param {boolean} anyCapacitor чи зустрілось **@capacitor/** у **package.json**
|
|
251
|
+
* @returns {boolean} **true** якщо застосовуємо **check capacitor**
|
|
252
|
+
*/
|
|
253
|
+
export function isCapacitorRelevantForCheck(root, anyCapacitor) {
|
|
254
|
+
return hasCapacitorConfigInRoot(root) || anyCapacitor
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Рекурсивно шукає `Podfile` у **ios/**, **не** заходячи в **Pods** (кеш CocoaPods) і типові build-каталоги.
|
|
259
|
+
* @param {string} root корінь репозиторію
|
|
260
|
+
* @param {string} dir абсолютний каталог
|
|
261
|
+
* @param {(rel: string) => void} onPodfileRelative **callback** з **posix**-шляхом `Podfile` від **root**
|
|
262
|
+
* @returns {Promise<boolean>} **true** — знайдено **хоча б один** `Podfile`
|
|
263
|
+
*/
|
|
264
|
+
export async function walkIosForPodfileSkipPods(root, dir, onPodfileRelative) {
|
|
265
|
+
let entries
|
|
266
|
+
try {
|
|
267
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
268
|
+
} catch {
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
for (const e of entries) {
|
|
272
|
+
if (e.name !== 'Pods' && e.name !== 'build' && e.name !== 'DerivedData') {
|
|
273
|
+
const abs = join(dir, e.name)
|
|
274
|
+
if (e.isFile() && e.name === 'Podfile') {
|
|
275
|
+
onPodfileRelative((relative(root, abs) || abs).replaceAll('\\', '/'))
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
if (e.isDirectory()) {
|
|
279
|
+
const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
|
|
280
|
+
if (found) {
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @param {string} root корінь
|
|
291
|
+
* @returns {Promise<string | null>} **relative**-шлях `Podfile` (або **null**)
|
|
292
|
+
*/
|
|
293
|
+
export async function findFirstPodfileUnderIosExcludingPods(root) {
|
|
294
|
+
const iosDir = join(root, 'ios')
|
|
295
|
+
if (!existsSync(iosDir)) {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
let first = /** @type {string | null} */ (null)
|
|
299
|
+
await walkIosForPodfileSkipPods(root, iosDir, rel => {
|
|
300
|
+
if (first === null || rel.length < first.length) {
|
|
301
|
+
first = rel
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
return first
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @returns {Promise<number>} **0** — **ok**; **1** — **fail** (див. **capacitor.mdc**)
|
|
309
|
+
*/
|
|
310
|
+
export async function check() {
|
|
311
|
+
const reporter = createCheckReporter()
|
|
312
|
+
const { pass, fail, getExitCode } = reporter
|
|
313
|
+
const root = process.cwd()
|
|
314
|
+
|
|
315
|
+
const acc = { byPath: new Map(), anyCapacitor: false }
|
|
316
|
+
await collectCapacitorDataFromAllPackageJson(root, acc)
|
|
317
|
+
const { byPath, anyCapacitor } = acc
|
|
318
|
+
|
|
319
|
+
if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
|
|
320
|
+
pass('Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено')
|
|
321
|
+
return getExitCode()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
pass('Проєкт з ознаками Capacitor — застосовую capacitor.mdc')
|
|
325
|
+
|
|
326
|
+
if (byPath.size === 0) {
|
|
327
|
+
fail(
|
|
328
|
+
`додай залежність @capacitor/core з діапазоном ^${MIN_CAPACITOR_MAJOR}.0.0 (або іншим, сумісним лише з ${MIN_CAPACITOR_MAJOR}+) у package.json (capacitor.mdc)`
|
|
329
|
+
)
|
|
330
|
+
} else {
|
|
331
|
+
for (const [rel, range] of byPath) {
|
|
332
|
+
reportOneCapacitorCoreRange(fail, pass, rel, range)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const podfileRel = await findFirstPodfileUnderIosExcludingPods(root)
|
|
337
|
+
if (podfileRel === null) {
|
|
338
|
+
if (existsSync(join(root, 'ios'))) {
|
|
339
|
+
pass('ios/ без Podfile поза Pods/ (лише SPM, capacitor.mdc)')
|
|
340
|
+
} else {
|
|
341
|
+
pass('каталог ios/ не знайдено — вимогу iOS/SPM пропущено')
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
fail(
|
|
345
|
+
`iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods (прибери Podfile, capacitor.mdc)`
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return getExitCode()
|
|
350
|
+
}
|
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)
|