@nitra/cursor 1.8.135 → 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/mdc/capacitor.mdc +24 -4
- package/mdc/docker.mdc +9 -3
- package/package.json +1 -1
- package/scripts/check-capacitor.mjs +153 -23
- package/scripts/check-docker.mjs +29 -7
- package/scripts/check-text.mjs +20 -0
package/mdc/capacitor.mdc
CHANGED
|
@@ -11,8 +11,28 @@ version: '1.0'
|
|
|
11
11
|
**`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні. Програма перевірки — **`check-capacitor.mjs`**
|
|
12
12
|
(репозиторій **@nitra/cursor**).
|
|
13
13
|
|
|
14
|
-
## iOS: лише SPM
|
|
14
|
+
## iOS: зазвичай лише SPM, виняток Podfile
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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/package.json
CHANGED
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* варто задати явний діапазон, наприклад **`^8.0.0`**. Якщо оголошено `capacitor.config.*` без жодного
|
|
15
15
|
* **`@capacitor/core`** у дереві `package.json` — також помилка.
|
|
16
16
|
*
|
|
17
|
-
* **iOS
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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-умови не застосовуються.
|
|
21
23
|
*/
|
|
22
24
|
import { existsSync } from 'node:fs'
|
|
23
25
|
import { readdir, readFile } from 'node:fs/promises'
|
|
@@ -41,9 +43,11 @@ const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
|
|
|
41
43
|
])
|
|
42
44
|
|
|
43
45
|
/** `||` у діапазоні npm-версій */
|
|
46
|
+
// eslint-disable-next-line sonarjs/slow-regex -- короткі **semver**-підрядки у **package.json**
|
|
44
47
|
const NPM_OR_PARTS_RE = /\s*\|\|\s*/
|
|
45
48
|
|
|
46
49
|
/** `a - b` (діапазон діапазонів) */
|
|
50
|
+
// eslint-disable-next-line sonarjs/slow-regex -- форма **X - Y** у **npm**-range
|
|
47
51
|
const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
|
|
48
52
|
|
|
49
53
|
const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
|
|
@@ -52,6 +56,14 @@ const PREFIX_GEQ_RE = /^>=\s*/u
|
|
|
52
56
|
const PREFIX_GT_RE = /^>\s*/u
|
|
53
57
|
const STRIP_CARET_TILDE_EQ_RE = /^[=^~]+\s*/u
|
|
54
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
|
+
|
|
55
67
|
/**
|
|
56
68
|
* Мінімальний **major** (нижня межа) для **однієї** OR-частини діапазону npm (без `||` всередині).
|
|
57
69
|
* @param {string} segment одна частина після `||` або весь рядок
|
|
@@ -158,6 +170,22 @@ function reportOneCapacitorCoreRange(fail, pass, rel, range) {
|
|
|
158
170
|
}
|
|
159
171
|
}
|
|
160
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
|
+
|
|
161
189
|
/**
|
|
162
190
|
* @param {string} absPath шлях до `package.json`
|
|
163
191
|
* @param {string} root корінь репозиторію
|
|
@@ -181,15 +209,7 @@ export async function recordCapacitorFromOnePackageJson(absPath, root, out) {
|
|
|
181
209
|
for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
182
210
|
const rec = pkg?.[block]
|
|
183
211
|
if (rec !== null && rec !== undefined && typeof rec === 'object' && !Array.isArray(rec)) {
|
|
184
|
-
|
|
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
|
-
}
|
|
212
|
+
recordCapacitorFromDependencyObject(rel, /** @type {Record<string, unknown>} */ (rec), out)
|
|
193
213
|
}
|
|
194
214
|
}
|
|
195
215
|
}
|
|
@@ -269,18 +289,18 @@ export async function walkIosForPodfileSkipPods(root, dir, onPodfileRelative) {
|
|
|
269
289
|
return false
|
|
270
290
|
}
|
|
271
291
|
for (const e of entries) {
|
|
272
|
-
if (e.name
|
|
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) {
|
|
273
299
|
const abs = join(dir, e.name)
|
|
274
|
-
|
|
275
|
-
|
|
300
|
+
const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
|
|
301
|
+
if (found === true) {
|
|
276
302
|
return true
|
|
277
303
|
}
|
|
278
|
-
if (e.isDirectory()) {
|
|
279
|
-
const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
|
|
280
|
-
if (found) {
|
|
281
|
-
return true
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
304
|
}
|
|
285
305
|
}
|
|
286
306
|
return false
|
|
@@ -304,6 +324,112 @@ export async function findFirstPodfileUnderIosExcludingPods(root) {
|
|
|
304
324
|
return first
|
|
305
325
|
}
|
|
306
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
|
+
|
|
307
433
|
/**
|
|
308
434
|
* @returns {Promise<number>} **0** — **ok**; **1** — **fail** (див. **capacitor.mdc**)
|
|
309
435
|
*/
|
|
@@ -340,9 +466,13 @@ export async function check() {
|
|
|
340
466
|
} else {
|
|
341
467
|
pass('каталог ios/ не знайдено — вимогу iOS/SPM пропущено')
|
|
342
468
|
}
|
|
469
|
+
} else if ((await isIosCocoaPodsExemptByNitraConfig(root)) === true) {
|
|
470
|
+
pass(
|
|
471
|
+
`iOS: Podfile «${podfileRel}» — дозволено виняток (nitra.iosCocoaPodsBecausePluginsLackSpm / iosCocoaPodsAllowed, capacitor.mdc)`
|
|
472
|
+
)
|
|
343
473
|
} else {
|
|
344
474
|
fail(
|
|
345
|
-
`iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods
|
|
475
|
+
`iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods, або додай виняток nitra (capacitor.mdc)`
|
|
346
476
|
)
|
|
347
477
|
}
|
|
348
478
|
|
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-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
|
/**
|