@nitra/cursor 1.8.106 → 1.8.108
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/js-pino.mdc +2 -2
- package/mdc/k8s.mdc +11 -2
- package/mdc/text.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-js-pino.mjs +48 -3
- package/scripts/check-k8s.mjs +386 -1
- package/scripts/utils/bunyan-imports.mjs +182 -0
package/mdc/js-pino.mdc
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Використання @nitra/pino
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.1'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Проект використовує @nitra/pino для логування.
|
|
8
|
-
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino.
|
|
8
|
+
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у `package.json`, так і в коді: усі `import` / `require` / динамічні `import()` з `@nitra/bunyan` (і застарілого `bunyan`) треба замінити на `@nitra/pino` і за потреби адаптувати виклики під його API.
|
|
9
9
|
|
|
10
10
|
В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
|
|
11
11
|
а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
|
package/mdc/k8s.mdc
CHANGED
|
@@ -110,7 +110,16 @@ resources: {}
|
|
|
110
110
|
|
|
111
111
|
### HTTPRoute для Deployment з `hasura/graphql-engine`
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
**Прив'язка:** **check k8s** вважає **HTTPRoute** Hasura-маршрутом, якщо в **тому самому каталозі** є **`Deployment`** з образом **`hasura/graphql-engine`**, а його **`metadata.name`** збігається з **`metadata.name`** цього **`HTTPRoute`**. Саме за цією конвенцією скрипт шукає пари для звірки канону.
|
|
114
|
+
|
|
115
|
+
**Префікс параметризовано:** **`<prefix>`** — рядок перед **`/ql`** у першому Hasura-правилі (**`Exact <prefix>/ql`**). Може бути порожнім (**`<prefix>`** = **``**, шлях **`/ql`**) або непорожнім (наприклад **`<prefix>`** = **`/notify`**, шлях **`/notify/ql`**). Усі інші Hasura-правила цього **HTTPRoute** мають містити той самий **`<prefix>`**.
|
|
116
|
+
|
|
117
|
+
**Канон — 4 правила у цьому порядку** (додаткові правила поверх канону дозволені — вони просто ігноруються під час зіставлення):
|
|
118
|
+
|
|
119
|
+
1. **`Exact <prefix>/ql`** → **`RequestRedirect`** **`ReplaceFullPath <prefix>/ql/console`** **`statusCode: 302`**.
|
|
120
|
+
2. **`Exact <prefix>/ql/`** → те саме (редирект на **`<prefix>/ql/console`** 302).
|
|
121
|
+
3. **`PathPrefix <prefix>/ql`** → **`URLRewrite`** **`ReplacePrefixMatch /`**, один **`backendRef`** на headless **Service** (**`-hl`**).
|
|
122
|
+
4. **WebSocket:** **`PathPrefix <prefix>/ql`** + header **`Upgrade: websocket`** → **`URLRewrite`** **`ReplacePrefixMatch /`** + **`RequestHeaderModifier`** **`remove: [Authorization]`** (авторизація для WebSocket іде всередині messages). Той самий **`backendRef`**.
|
|
114
123
|
|
|
115
124
|
**`parentRefs`**, **`hostnames`**, **`metadata.namespace`** / **`name`** підлаштуй під середовище. У **`backendRefs.name`** вказуй **headless** **Service** з суфіксом **`-hl`** (див. розділ **«Service: `svc.yaml` і `svc-hl.yaml`»**); у прикладі **`db-h-hl`** заміни на фактичне ім’я.
|
|
116
125
|
|
|
@@ -278,7 +287,7 @@ patches:
|
|
|
278
287
|
|
|
279
288
|
**`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
|
|
280
289
|
|
|
281
|
-
**Не входить у check k8s:**
|
|
290
|
+
**Не входить у check k8s:** **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
282
291
|
|
|
283
292
|
## Коли застосовувати (агентам)
|
|
284
293
|
|
package/mdc/text.mdc
CHANGED
|
@@ -120,7 +120,7 @@ version: '1.25'
|
|
|
120
120
|
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|
|
123
|
-
"@nitra/cspell-dict": "^2.
|
|
123
|
+
"@nitra/cspell-dict": "^2.1.0"
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
```
|
|
@@ -235,7 +235,7 @@ jobs:
|
|
|
235
235
|
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
236
236
|
},
|
|
237
237
|
"devDependencies": {
|
|
238
|
-
"@nitra/cspell-dict": "^2.
|
|
238
|
+
"@nitra/cspell-dict": "^2.1.0"
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
```
|
|
@@ -252,7 +252,7 @@ jobs:
|
|
|
252
252
|
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
|
|
253
253
|
},
|
|
254
254
|
"devDependencies": {
|
|
255
|
-
"@nitra/cspell-dict": "^2.
|
|
255
|
+
"@nitra/cspell-dict": "^2.1.0"
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
```
|
package/package.json
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Для кожного workspace-пакета перевіряє правило js-pino.mdc.
|
|
3
3
|
*
|
|
4
|
-
* Заборона bunyan
|
|
5
|
-
*
|
|
4
|
+
* Заборона `@nitra/bunyan` / `bunyan` як у залежностях `package.json`, так і в коді
|
|
5
|
+
* (`import` / `require` / динамічний `import()`); наявність `OTEL_RESOURCE_ATTRIBUTES`
|
|
6
|
+
* у `k8s/base/configmap.yaml`, якщо такий файл існує.
|
|
7
|
+
*
|
|
8
|
+
* Імпорти в джерелах сканує AST через `oxc-parser` (див. `utils/bunyan-imports.mjs`),
|
|
9
|
+
* щоб виявити випадки на кшталт `import log from '@nitra/bunyan'`, які лишаються в коді
|
|
10
|
+
* після підміни залежності.
|
|
6
11
|
*/
|
|
7
12
|
import { existsSync } from 'node:fs'
|
|
8
13
|
import { readFile } from 'node:fs/promises'
|
|
9
|
-
import { join } from 'node:path'
|
|
14
|
+
import { join, relative } from 'node:path'
|
|
10
15
|
|
|
16
|
+
import {
|
|
17
|
+
findBunyanImportsInText,
|
|
18
|
+
isBunyanScanSourceFile,
|
|
19
|
+
shouldSkipFileForBunyanScan
|
|
20
|
+
} from './utils/bunyan-imports.mjs'
|
|
11
21
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
22
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
12
23
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
|
|
27
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
28
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
29
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
30
|
+
* @returns {Promise<number>} кількість знайдених порушень
|
|
31
|
+
*/
|
|
32
|
+
async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const sourcePaths = []
|
|
35
|
+
await walkDir(absPackageRoot, absPath => {
|
|
36
|
+
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
37
|
+
if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
|
|
38
|
+
sourcePaths.push(absPath)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
let violations = 0
|
|
43
|
+
for (const absPath of sourcePaths) {
|
|
44
|
+
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
45
|
+
const content = await readFile(absPath, 'utf8')
|
|
46
|
+
for (const v of findBunyanImportsInText(content, rel)) {
|
|
47
|
+
violations++
|
|
48
|
+
fail(`${label}${rel}:${v.line} — заміни '${v.module}' на '@nitra/pino': ${v.snippet}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return violations
|
|
52
|
+
}
|
|
53
|
+
|
|
14
54
|
/**
|
|
15
55
|
* Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
|
|
16
56
|
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
@@ -33,6 +73,11 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
|
33
73
|
}
|
|
34
74
|
}
|
|
35
75
|
|
|
76
|
+
const importViolations = await checkBunyanImports(join(process.cwd(), rootDir), label, fail)
|
|
77
|
+
if (importViolations === 0) {
|
|
78
|
+
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
const configmapPath = join(rootDir, 'k8s/base/configmap.yaml')
|
|
37
82
|
if (existsSync(configmapPath)) {
|
|
38
83
|
const content = await readFile(configmapPath, 'utf8')
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
56
56
|
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
58
|
+
* **Структура `HTTPRoute` для Hasura-Deployment:** звіряється канон 4 правил у **`spec.rules`** (редиректи **`<prefix>/ql`** і **`<prefix>/ql/`** на **`<prefix>/ql/console`** 302, **`PathPrefix <prefix>/ql`** + **URLRewrite** на **`/`**, окреме WebSocket-правило з **`RequestHeaderModifier`** remove **`Authorization`**). **Префікс параметризовано** (рядок перед **`/ql`** у першому Hasura-правилі). **Прив'язка** — за **`metadata.name`** у тому ж каталозі, що й **Deployment** з образом **`hasura/graphql-engine`** (див. k8s.mdc). **Додаткові правила** поверх канону дозволені.
|
|
59
59
|
*/
|
|
60
60
|
import { existsSync } from 'node:fs'
|
|
61
61
|
import { readFile, stat, unlink } from 'node:fs/promises'
|
|
@@ -1924,6 +1924,41 @@ export function deploymentHasuraGraphqlEngineImageViolation(manifest) {
|
|
|
1924
1924
|
return hasuraGraphqlEngineViolationInContainerList('initContainers', podSpec.initContainers)
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* Чи у списку контейнерів є хоча б один з образом **hasura/graphql-engine** (будь-який тег).
|
|
1929
|
+
* @param {unknown} containers значення **containers** / **initContainers** із podSpec
|
|
1930
|
+
* @returns {boolean} true — якщо знайдено хоча б один контейнер з образом Hasura
|
|
1931
|
+
*/
|
|
1932
|
+
function containerListHasHasuraImage(containers) {
|
|
1933
|
+
if (!Array.isArray(containers)) return false
|
|
1934
|
+
for (const c of containers) {
|
|
1935
|
+
if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
|
|
1936
|
+
const image = /** @type {Record<string, unknown>} */ (c).image
|
|
1937
|
+
if (typeof image === 'string' && image !== '' && isHasuraGraphqlEngineImageRef(image)) return true
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return false
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* Чи **Deployment** використовує образ **hasura/graphql-engine** у будь-якому контейнері (маркер для прив'язки HTTPRoute-канона).
|
|
1945
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
1946
|
+
* @returns {boolean} true — для Deployment з Hasura-контейнером у containers / initContainers
|
|
1947
|
+
*/
|
|
1948
|
+
export function isHasuraDeploymentManifest(manifest) {
|
|
1949
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return false
|
|
1950
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
1951
|
+
if (rec.kind !== 'Deployment') return false
|
|
1952
|
+
const spec = rec.spec
|
|
1953
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
1954
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
1955
|
+
if (template === null || typeof template !== 'object' || Array.isArray(template)) return false
|
|
1956
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
1957
|
+
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return false
|
|
1958
|
+
const p = /** @type {Record<string, unknown>} */ (podSpec)
|
|
1959
|
+
return containerListHasHasuraImage(p.containers) || containerListHasHasuraImage(p.initContainers)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1927
1962
|
/**
|
|
1928
1963
|
* Чи **Service** містить заборонені анотації GKE у **`metadata.annotations`** (k8s.mdc).
|
|
1929
1964
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -2154,6 +2189,247 @@ function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
|
2154
2189
|
}
|
|
2155
2190
|
}
|
|
2156
2191
|
|
|
2192
|
+
/**
|
|
2193
|
+
* Звузити `unknown` до `Record<string, unknown>` (`null`, масиви, примітиви → null).
|
|
2194
|
+
* @param {unknown} node довільний вузол YAML-документа
|
|
2195
|
+
* @returns {Record<string, unknown> | null} plain-об'єкт або null, якщо це не plain-запис
|
|
2196
|
+
*/
|
|
2197
|
+
function asPlainRecord(node) {
|
|
2198
|
+
if (node === null || node === undefined || typeof node !== 'object' || Array.isArray(node)) return null
|
|
2199
|
+
return /** @type {Record<string, unknown>} */ (node)
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Чи `match` — рівно один шлях заданого типу з потрібним значенням, **без** `headers`.
|
|
2204
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2205
|
+
* @param {'Exact' | 'PathPrefix'} pathType очікуваний `path.type`
|
|
2206
|
+
* @param {string} pathValue очікуваний `path.value`
|
|
2207
|
+
* @returns {boolean} true — якщо `matches` рівно один і відповідає критерію
|
|
2208
|
+
*/
|
|
2209
|
+
function hasuraRuleMatchesSinglePathNoHeaders(rule, pathType, pathValue) {
|
|
2210
|
+
const r = asPlainRecord(rule)
|
|
2211
|
+
if (r === null) return false
|
|
2212
|
+
const matches = r.matches
|
|
2213
|
+
if (!Array.isArray(matches) || matches.length !== 1) return false
|
|
2214
|
+
const m = asPlainRecord(matches[0])
|
|
2215
|
+
if (m === null) return false
|
|
2216
|
+
if (m.headers !== undefined) return false
|
|
2217
|
+
const p = asPlainRecord(m.path)
|
|
2218
|
+
if (p === null) return false
|
|
2219
|
+
return p.type === pathType && p.value === pathValue
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* Чи **filters** — рівно один `RequestRedirect` з `ReplaceFullPath` на `toPath` і `statusCode: 302`.
|
|
2224
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2225
|
+
* @param {string} toPath очікуваний `requestRedirect.path.replaceFullPath`
|
|
2226
|
+
* @returns {boolean} true — якщо filters відповідають канону редиректу
|
|
2227
|
+
*/
|
|
2228
|
+
function hasuraRuleHasExactRedirect(rule, toPath) {
|
|
2229
|
+
const r = asPlainRecord(rule)
|
|
2230
|
+
if (r === null) return false
|
|
2231
|
+
const filters = r.filters
|
|
2232
|
+
if (!Array.isArray(filters) || filters.length !== 1) return false
|
|
2233
|
+
const f = asPlainRecord(filters[0])
|
|
2234
|
+
if (f === null || f.type !== 'RequestRedirect') return false
|
|
2235
|
+
const rr = asPlainRecord(f.requestRedirect)
|
|
2236
|
+
if (rr === null || rr.statusCode !== 302) return false
|
|
2237
|
+
const p = asPlainRecord(rr.path)
|
|
2238
|
+
return p !== null && p.type === 'ReplaceFullPath' && p.replaceFullPath === toPath
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Чи серед **filters** є `URLRewrite` з `ReplacePrefixMatch: /`.
|
|
2243
|
+
* @param {unknown[]} filters масив filters з одного правила `HTTPRoute`
|
|
2244
|
+
* @returns {boolean} true — якщо фільтр `URLRewrite` має `ReplacePrefixMatch: /`
|
|
2245
|
+
*/
|
|
2246
|
+
function hasuraFiltersIncludeUrlRewriteToSlash(filters) {
|
|
2247
|
+
for (const f of filters) {
|
|
2248
|
+
const fr = asPlainRecord(f)
|
|
2249
|
+
if (fr !== null && fr.type === 'URLRewrite') {
|
|
2250
|
+
const rw = asPlainRecord(fr.urlRewrite)
|
|
2251
|
+
if (rw === null) return false
|
|
2252
|
+
const p = asPlainRecord(rw.path)
|
|
2253
|
+
return p !== null && p.type === 'ReplacePrefixMatch' && p.replacePrefixMatch === '/'
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return false
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/**
|
|
2260
|
+
* Чи серед **filters** є `RequestHeaderModifier` з `remove: [Authorization]`.
|
|
2261
|
+
* @param {unknown[]} filters масив filters з одного правила `HTTPRoute`
|
|
2262
|
+
* @returns {boolean} true — якщо фільтр `RequestHeaderModifier` видаляє саме `Authorization`
|
|
2263
|
+
*/
|
|
2264
|
+
function hasuraFiltersRemoveAuthorization(filters) {
|
|
2265
|
+
for (const f of filters) {
|
|
2266
|
+
const fr = asPlainRecord(f)
|
|
2267
|
+
if (fr !== null && fr.type === 'RequestHeaderModifier') {
|
|
2268
|
+
const mod = asPlainRecord(fr.requestHeaderModifier)
|
|
2269
|
+
if (mod === null) return false
|
|
2270
|
+
const remove = mod.remove
|
|
2271
|
+
if (!Array.isArray(remove) || remove.length !== 1) return false
|
|
2272
|
+
return remove[0] === 'Authorization'
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return false
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Ім'я єдиного `backendRef` у правилі (або null, якщо backend-ів не рівно один).
|
|
2280
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2281
|
+
* @returns {string | null} `backendRefs[0].name` або null, якщо backend-ів не рівно один
|
|
2282
|
+
*/
|
|
2283
|
+
function hasuraRuleSingleBackendName(rule) {
|
|
2284
|
+
const r = asPlainRecord(rule)
|
|
2285
|
+
if (r === null) return null
|
|
2286
|
+
const refs = r.backendRefs
|
|
2287
|
+
if (!Array.isArray(refs) || refs.length !== 1) return null
|
|
2288
|
+
const b = asPlainRecord(refs[0])
|
|
2289
|
+
if (b === null || typeof b.name !== 'string') return null
|
|
2290
|
+
return b.name
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Правило 3: `PathPrefix <qlPath>` + **filters** = 1 × `URLRewrite(ReplacePrefixMatch: /)`.
|
|
2295
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2296
|
+
* @param {string} qlPath очікуваний `path.value` (`<prefix>/ql`)
|
|
2297
|
+
* @returns {boolean} true — якщо правило відповідає канону пункту 3
|
|
2298
|
+
*/
|
|
2299
|
+
function hasuraRuleIsQlUrlRewrite(rule, qlPath) {
|
|
2300
|
+
if (!hasuraRuleMatchesSinglePathNoHeaders(rule, 'PathPrefix', qlPath)) return false
|
|
2301
|
+
const r = asPlainRecord(rule)
|
|
2302
|
+
if (r === null) return false
|
|
2303
|
+
const filters = r.filters
|
|
2304
|
+
if (!Array.isArray(filters) || filters.length !== 1) return false
|
|
2305
|
+
return hasuraFiltersIncludeUrlRewriteToSlash(filters)
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Правило 4: WebSocket — `PathPrefix <qlPath>` + `Upgrade: websocket`, **filters** = `URLRewrite` + `RequestHeaderModifier(remove Authorization)`.
|
|
2310
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2311
|
+
* @param {string} qlPath очікуваний `path.value` (`<prefix>/ql`)
|
|
2312
|
+
* @returns {boolean} true — якщо правило відповідає канону пункту 4 (WebSocket)
|
|
2313
|
+
*/
|
|
2314
|
+
function hasuraRuleIsWebsocket(rule, qlPath) {
|
|
2315
|
+
const r = asPlainRecord(rule)
|
|
2316
|
+
if (r === null) return false
|
|
2317
|
+
const matches = r.matches
|
|
2318
|
+
if (!Array.isArray(matches) || matches.length !== 1) return false
|
|
2319
|
+
const m = asPlainRecord(matches[0])
|
|
2320
|
+
if (m === null) return false
|
|
2321
|
+
const p = asPlainRecord(m.path)
|
|
2322
|
+
if (p === null || p.type !== 'PathPrefix' || p.value !== qlPath) return false
|
|
2323
|
+
const headers = m.headers
|
|
2324
|
+
if (!Array.isArray(headers) || headers.length !== 1) return false
|
|
2325
|
+
const h = asPlainRecord(headers[0])
|
|
2326
|
+
if (h === null || h.type !== 'Exact' || h.name !== 'Upgrade' || h.value !== 'websocket') return false
|
|
2327
|
+
const filters = r.filters
|
|
2328
|
+
if (!Array.isArray(filters) || filters.length !== 2) return false
|
|
2329
|
+
return hasuraFiltersIncludeUrlRewriteToSlash(filters) && hasuraFiltersRemoveAuthorization(filters)
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Знаходить перше правило з **`matches`** = `[{ path: { type: 'Exact', value: '<prefix>/ql' } }]` (без headers),
|
|
2334
|
+
* повертає `<prefix>` (може бути порожнім) і позицію правила 1.
|
|
2335
|
+
* @param {unknown[]} rules вміст `spec.rules` HTTPRoute
|
|
2336
|
+
* @returns {{ prefix: string, startIndex: number } | null} виявлений префікс і позиція правила 1 або null
|
|
2337
|
+
*/
|
|
2338
|
+
function findHasuraCanonStart(rules) {
|
|
2339
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2340
|
+
const r = asPlainRecord(rules[i])
|
|
2341
|
+
const matches = r === null ? null : r.matches
|
|
2342
|
+
if (!Array.isArray(matches) || matches.length !== 1) {
|
|
2343
|
+
// наступне правило
|
|
2344
|
+
} else {
|
|
2345
|
+
const m = asPlainRecord(matches[0])
|
|
2346
|
+
const p = m === null || m.headers !== undefined ? null : asPlainRecord(m.path)
|
|
2347
|
+
if (
|
|
2348
|
+
p !== null &&
|
|
2349
|
+
p.type === 'Exact' &&
|
|
2350
|
+
typeof p.value === 'string' &&
|
|
2351
|
+
p.value.endsWith('/ql')
|
|
2352
|
+
) {
|
|
2353
|
+
return { prefix: p.value.slice(0, -'/ql'.length), startIndex: i }
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return null
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/**
|
|
2361
|
+
* Знаходить перше правило за індексом ≥ `from`, що задовольняє `predicate`. Повертає індекс або -1.
|
|
2362
|
+
* @param {unknown[]} rules вміст `spec.rules` HTTPRoute
|
|
2363
|
+
* @param {number} from мінімальний індекс, з якого починати пошук
|
|
2364
|
+
* @param {(rule: unknown) => boolean} predicate предикат на одне правило
|
|
2365
|
+
* @returns {number} індекс знайденого правила або -1
|
|
2366
|
+
*/
|
|
2367
|
+
function findHasuraRule(rules, from, predicate) {
|
|
2368
|
+
for (let i = from; i < rules.length; i++) {
|
|
2369
|
+
if (predicate(rules[i])) return i
|
|
2370
|
+
}
|
|
2371
|
+
return -1
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/**
|
|
2375
|
+
* Чи **`HTTPRoute`** порушує канон 4 правил Hasura (див. k8s.mdc).
|
|
2376
|
+
* Повертає текст порушення або null, якщо канон витримано. Додаткові правила поверх канону допускаються.
|
|
2377
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
2378
|
+
* @returns {string | null} текст порушення або null, якщо канон витримано
|
|
2379
|
+
*/
|
|
2380
|
+
export function httpRouteHasuraCanonViolation(manifest) {
|
|
2381
|
+
const rec = asPlainRecord(manifest)
|
|
2382
|
+
if (rec === null) return null
|
|
2383
|
+
const spec = asPlainRecord(rec.spec)
|
|
2384
|
+
if (spec === null) return 'HTTPRoute без spec — канон Hasura вимагає 4 правил (див. k8s.mdc)'
|
|
2385
|
+
const rules = spec.rules
|
|
2386
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
2387
|
+
return 'spec.rules порожній — канон Hasura вимагає 4 правил у порядку (див. k8s.mdc)'
|
|
2388
|
+
}
|
|
2389
|
+
const start = findHasuraCanonStart(rules)
|
|
2390
|
+
if (start === null) {
|
|
2391
|
+
return 'не знайдено правило 1 Hasura-канона: Exact "<prefix>/ql" + RequestRedirect ReplaceFullPath "<prefix>/ql/console" statusCode 302 (див. k8s.mdc)'
|
|
2392
|
+
}
|
|
2393
|
+
const { prefix, startIndex } = start
|
|
2394
|
+
const qlPath = `${prefix}/ql`
|
|
2395
|
+
const qlSlashPath = `${prefix}/ql/`
|
|
2396
|
+
const consolePath = `${prefix}/ql/console`
|
|
2397
|
+
|
|
2398
|
+
if (!hasuraRuleHasExactRedirect(rules[startIndex], consolePath)) {
|
|
2399
|
+
return `правило 1 Hasura-канона (rules[${startIndex}], prefix «${prefix}»): Exact "${qlPath}" має мати RequestRedirect ReplaceFullPath "${consolePath}" statusCode 302 (див. k8s.mdc)`
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
const i2 = findHasuraRule(
|
|
2403
|
+
rules,
|
|
2404
|
+
startIndex + 1,
|
|
2405
|
+
r => hasuraRuleMatchesSinglePathNoHeaders(r, 'Exact', qlSlashPath) && hasuraRuleHasExactRedirect(r, consolePath)
|
|
2406
|
+
)
|
|
2407
|
+
if (i2 === -1) {
|
|
2408
|
+
return `правило 2 Hasura-канона: після правила 1 має бути Exact "${qlSlashPath}" + RequestRedirect ReplaceFullPath "${consolePath}" statusCode 302 (див. k8s.mdc)`
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
const i3 = findHasuraRule(
|
|
2412
|
+
rules,
|
|
2413
|
+
i2 + 1,
|
|
2414
|
+
r => hasuraRuleIsQlUrlRewrite(r, qlPath) && hasuraRuleSingleBackendName(r) !== null
|
|
2415
|
+
)
|
|
2416
|
+
if (i3 === -1) {
|
|
2417
|
+
return `правило 3 Hasura-канона: після правила 2 має бути PathPrefix "${qlPath}" + URLRewrite ReplacePrefixMatch "/" + один backendRef на headless Service (див. k8s.mdc)`
|
|
2418
|
+
}
|
|
2419
|
+
const backendName = /** @type {string} */ (hasuraRuleSingleBackendName(rules[i3]))
|
|
2420
|
+
|
|
2421
|
+
const i4 = findHasuraRule(
|
|
2422
|
+
rules,
|
|
2423
|
+
i3 + 1,
|
|
2424
|
+
r => hasuraRuleIsWebsocket(r, qlPath) && hasuraRuleSingleBackendName(r) === backendName
|
|
2425
|
+
)
|
|
2426
|
+
if (i4 === -1) {
|
|
2427
|
+
return `правило 4 Hasura-канона (WebSocket): після правила 3 має бути PathPrefix "${qlPath}" + header "Upgrade: websocket" + URLRewrite ReplacePrefixMatch "/" + RequestHeaderModifier remove [Authorization] + backendRef «${backendName}» (див. k8s.mdc)`
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
return null
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2157
2433
|
/**
|
|
2158
2434
|
* Збирає **`metadata.name`** для **kind: Service** у коренях документів; при помилці викликає **fail** і повертає false.
|
|
2159
2435
|
* @param {Record<string, unknown>[]} roots корені YAML-документів
|
|
@@ -2300,6 +2576,113 @@ async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
|
|
|
2300
2576
|
}
|
|
2301
2577
|
}
|
|
2302
2578
|
|
|
2579
|
+
/**
|
|
2580
|
+
* Індексує Hasura-Deployment-и за каталогом (ключ — абсолютний шлях каталогу, значення — множина `metadata.name`).
|
|
2581
|
+
* Паралельно збирає всі `kind: HTTPRoute` Gateway API (`gateway.networking.k8s.io/*`) із doc-індексом.
|
|
2582
|
+
* @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
|
|
2583
|
+
* @returns {Promise<{
|
|
2584
|
+
* hasuraByDir: Map<string, Set<string>>,
|
|
2585
|
+
* httpRoutes: { abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]
|
|
2586
|
+
* }>} індекс Hasura-Deployment-ів за каталогом і список HTTPRoute-документів
|
|
2587
|
+
*/
|
|
2588
|
+
async function collectHasuraDeploymentsAndHttpRoutes(yamlFiles) {
|
|
2589
|
+
/** @type {Map<string, Set<string>>} */
|
|
2590
|
+
const hasuraByDir = new Map()
|
|
2591
|
+
/** @type {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]} */
|
|
2592
|
+
const httpRoutes = []
|
|
2593
|
+
|
|
2594
|
+
for (const abs of yamlFiles) {
|
|
2595
|
+
await indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes)
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
return { hasuraByDir, httpRoutes }
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Читає один YAML і додає Hasura-Deployment-и / HTTPRoute-документи до відповідних колекцій (нещасливі читання ігнорує).
|
|
2603
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
2604
|
+
* @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом
|
|
2605
|
+
* @param {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]} httpRoutes колектор HTTPRoute-документів
|
|
2606
|
+
* @returns {Promise<void>}
|
|
2607
|
+
*/
|
|
2608
|
+
async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
|
|
2609
|
+
let raw
|
|
2610
|
+
try {
|
|
2611
|
+
raw = await readFile(abs, 'utf8')
|
|
2612
|
+
} catch {
|
|
2613
|
+
return
|
|
2614
|
+
}
|
|
2615
|
+
const lines = toLines(raw)
|
|
2616
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
2617
|
+
/** @type {import('yaml').Document[]} */
|
|
2618
|
+
let docs
|
|
2619
|
+
try {
|
|
2620
|
+
docs = parseAllDocuments(body)
|
|
2621
|
+
} catch {
|
|
2622
|
+
return
|
|
2623
|
+
}
|
|
2624
|
+
const dir = dirname(abs)
|
|
2625
|
+
|
|
2626
|
+
for (const [di, doc] of docs.entries()) {
|
|
2627
|
+
if (doc.errors.length === 0) {
|
|
2628
|
+
const rec = asPlainRecord(doc.toJSON())
|
|
2629
|
+
if (rec !== null) {
|
|
2630
|
+
recordHasuraDeploymentName(rec, dir, hasuraByDir)
|
|
2631
|
+
const av = rec.apiVersion
|
|
2632
|
+
if (rec.kind === 'HTTPRoute' && typeof av === 'string' && av.startsWith(GATEWAY_API_GROUP_PREFIX)) {
|
|
2633
|
+
httpRoutes.push({ abs, dir, docIndex: di + 1, obj: rec })
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* Якщо документ — Hasura-Deployment із непорожнім `metadata.name`, додає ім'я до індексу за каталогом.
|
|
2642
|
+
* @param {Record<string, unknown>} rec корінь YAML-документа
|
|
2643
|
+
* @param {string} dir абсолютний шлях до каталогу файлу
|
|
2644
|
+
* @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (мутується)
|
|
2645
|
+
* @returns {void}
|
|
2646
|
+
*/
|
|
2647
|
+
function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
|
|
2648
|
+
if (!isHasuraDeploymentManifest(rec)) return
|
|
2649
|
+
const meta = asPlainRecord(rec.metadata)
|
|
2650
|
+
const name = meta === null ? undefined : meta.name
|
|
2651
|
+
if (typeof name !== 'string' || name === '') return
|
|
2652
|
+
let set = hasuraByDir.get(dir)
|
|
2653
|
+
if (set === undefined) {
|
|
2654
|
+
set = new Set()
|
|
2655
|
+
hasuraByDir.set(dir, set)
|
|
2656
|
+
}
|
|
2657
|
+
set.add(name)
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* Для кожного `kind: HTTPRoute`, що прив'язаний до **Hasura-Deployment** у тому самому каталозі за **`metadata.name`**,
|
|
2662
|
+
* звіряє канон 4 правил (див. `httpRouteHasuraCanonViolation` і k8s.mdc).
|
|
2663
|
+
* @param {string} root корінь репозиторію
|
|
2664
|
+
* @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
|
|
2665
|
+
* @param {(msg: string) => void} fail callback реєстрації помилки
|
|
2666
|
+
* @returns {Promise<void>}
|
|
2667
|
+
*/
|
|
2668
|
+
async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
2669
|
+
const { hasuraByDir, httpRoutes } = await collectHasuraDeploymentsAndHttpRoutes(yamlFiles)
|
|
2670
|
+
if (hasuraByDir.size === 0 || httpRoutes.length === 0) return
|
|
2671
|
+
|
|
2672
|
+
for (const hr of httpRoutes) {
|
|
2673
|
+
const meta = asPlainRecord(hr.obj.metadata)
|
|
2674
|
+
const name = meta === null ? undefined : meta.name
|
|
2675
|
+
const set = typeof name === 'string' && name !== '' ? hasuraByDir.get(hr.dir) : undefined
|
|
2676
|
+
if (set !== undefined && typeof name === 'string' && set.has(name)) {
|
|
2677
|
+
const v = httpRouteHasuraCanonViolation(hr.obj)
|
|
2678
|
+
if (v !== null) {
|
|
2679
|
+
const rel = (relative(root, hr.abs) || hr.abs).replaceAll('\\', '/')
|
|
2680
|
+
fail(`${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`)
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2303
2686
|
/**
|
|
2304
2687
|
* Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
|
|
2305
2688
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -2811,6 +3194,8 @@ export async function check() {
|
|
|
2811
3194
|
|
|
2812
3195
|
await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
|
|
2813
3196
|
|
|
3197
|
+
await validateHasuraHttpRouteCanon(root, yamlFiles, fail)
|
|
3198
|
+
|
|
2814
3199
|
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
2815
3200
|
|
|
2816
3201
|
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Знаходить імпорти з `@nitra/bunyan` (і застарілого `bunyan`) у джерелах — їх треба замінити
|
|
3
|
+
* на `@nitra/pino` згідно з js-pino.mdc.
|
|
4
|
+
*
|
|
5
|
+
* Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
|
|
6
|
+
* Додатково по AST програми ловимо `require('@nitra/bunyan')` і динамічний `import('@nitra/bunyan')`,
|
|
7
|
+
* щоб правило працювало й у CommonJS/інлайн-завантаженні.
|
|
8
|
+
*
|
|
9
|
+
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається порожній
|
|
10
|
+
* результат — спочатку треба полагодити синтаксис, потім перезапустити перевірку.
|
|
11
|
+
*/
|
|
12
|
+
import { parseSync } from 'oxc-parser'
|
|
13
|
+
|
|
14
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
15
|
+
const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Мова для Oxc за шляхом файлу (розширення).
|
|
19
|
+
* @param {string} filePath віртуальний або реальний шлях до файлу
|
|
20
|
+
* @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
|
|
21
|
+
*/
|
|
22
|
+
function langFromPath(filePath) {
|
|
23
|
+
const lower = filePath.toLowerCase()
|
|
24
|
+
if (lower.endsWith('.tsx')) {
|
|
25
|
+
return 'tsx'
|
|
26
|
+
}
|
|
27
|
+
if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) {
|
|
28
|
+
return 'ts'
|
|
29
|
+
}
|
|
30
|
+
if (lower.endsWith('.jsx')) {
|
|
31
|
+
return 'jsx'
|
|
32
|
+
}
|
|
33
|
+
return 'js'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Номер рядка (1-based) за зміщенням у буфері.
|
|
38
|
+
* @param {string} content повний текст файлу
|
|
39
|
+
* @param {number} offset байтове зміщення початку фрагмента
|
|
40
|
+
* @returns {number} номер рядка від 1
|
|
41
|
+
*/
|
|
42
|
+
function offsetToLine(content, offset) {
|
|
43
|
+
let line = 1
|
|
44
|
+
const n = Math.min(offset, content.length)
|
|
45
|
+
for (let i = 0; i < n; i++) {
|
|
46
|
+
if (content.codePointAt(i) === 10) {
|
|
47
|
+
line++
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return line
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Стискає пробіли для повідомлення про порушення.
|
|
55
|
+
* @param {string} s фрагмент коду
|
|
56
|
+
* @returns {string} скорочений однорядковий рядок
|
|
57
|
+
*/
|
|
58
|
+
function normalizeSnippet(s) {
|
|
59
|
+
return s.replaceAll(/\s+/g, ' ').trim().slice(0, 160)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Перевіряє, чи це виклик `require('<module>')` з рядковим аргументом.
|
|
64
|
+
* @param {any} node вузол AST
|
|
65
|
+
* @returns {string | null} ім'я модуля з аргументу, інакше `null`
|
|
66
|
+
*/
|
|
67
|
+
function requireCallModule(node) {
|
|
68
|
+
if (!node || node.type !== 'CallExpression') return null
|
|
69
|
+
const callee = node.callee
|
|
70
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'require') return null
|
|
71
|
+
const arg = node.arguments?.[0]
|
|
72
|
+
if (!arg || arg.type !== 'Literal' || typeof arg.value !== 'string') return null
|
|
73
|
+
return arg.value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Перевіряє, чи це динамічний `import('<module>')` з рядковим аргументом.
|
|
78
|
+
* @param {any} node вузол AST
|
|
79
|
+
* @returns {string | null} ім'я модуля, інакше `null`
|
|
80
|
+
*/
|
|
81
|
+
function dynamicImportModule(node) {
|
|
82
|
+
if (!node || node.type !== 'ImportExpression') return null
|
|
83
|
+
const src = node.source
|
|
84
|
+
if (!src || src.type !== 'Literal' || typeof src.value !== 'string') return null
|
|
85
|
+
return src.value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Простий рекурсивний обхід AST: заходимо в усі об'єкти/масиви, щоб знайти require/import-вузли.
|
|
90
|
+
* @param {any} node корінь або під-вузол AST
|
|
91
|
+
* @param {(n: any) => void} visit виклик для кожного об'єкта-вузла
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*/
|
|
94
|
+
function walkAst(node, visit) {
|
|
95
|
+
if (!node || typeof node !== 'object') return
|
|
96
|
+
if (Array.isArray(node)) {
|
|
97
|
+
for (const item of node) walkAst(item, visit)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (typeof node.type === 'string') {
|
|
101
|
+
visit(node)
|
|
102
|
+
}
|
|
103
|
+
for (const key of Object.keys(node)) {
|
|
104
|
+
if (key === 'parent') continue
|
|
105
|
+
const v = node[key]
|
|
106
|
+
if (v && typeof v === 'object') walkAst(v, visit)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Знаходить заборонені імпорти/require з `@nitra/bunyan` у тексті.
|
|
112
|
+
* @param {string} content вихідний код
|
|
113
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/foo.ts`)
|
|
114
|
+
* @returns {{ line: number, snippet: string, module: string }[]} список порушень
|
|
115
|
+
*/
|
|
116
|
+
export function findBunyanImportsInText(content, virtualPath = 'scan.ts') {
|
|
117
|
+
const pathForLang = virtualPath || 'scan.ts'
|
|
118
|
+
const lang = langFromPath(pathForLang)
|
|
119
|
+
let result
|
|
120
|
+
try {
|
|
121
|
+
result = parseSync(pathForLang, content, { lang, sourceType: 'module' })
|
|
122
|
+
} catch {
|
|
123
|
+
return []
|
|
124
|
+
}
|
|
125
|
+
if (result.errors?.length) {
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @type {{ line: number, snippet: string, module: string }[]} */
|
|
130
|
+
const out = []
|
|
131
|
+
|
|
132
|
+
for (const imp of result.module?.staticImports ?? []) {
|
|
133
|
+
const mod = imp.moduleRequest?.value
|
|
134
|
+
if (mod && FORBIDDEN_MODULES.has(mod)) {
|
|
135
|
+
out.push({
|
|
136
|
+
line: offsetToLine(content, imp.start),
|
|
137
|
+
snippet: normalizeSnippet(content.slice(imp.start, imp.end)),
|
|
138
|
+
module: mod
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
walkAst(result.program, node => {
|
|
144
|
+
const reqMod = requireCallModule(node)
|
|
145
|
+
if (reqMod && FORBIDDEN_MODULES.has(reqMod)) {
|
|
146
|
+
out.push({
|
|
147
|
+
line: offsetToLine(content, node.start),
|
|
148
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
149
|
+
module: reqMod
|
|
150
|
+
})
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const dynMod = dynamicImportModule(node)
|
|
154
|
+
if (dynMod && FORBIDDEN_MODULES.has(dynMod)) {
|
|
155
|
+
out.push({
|
|
156
|
+
line: offsetToLine(content, node.start),
|
|
157
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
158
|
+
module: dynMod
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return out
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я).
|
|
168
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
169
|
+
* @returns {boolean} `true`, якщо розширення підходить для пошуку імпорту
|
|
170
|
+
*/
|
|
171
|
+
export function isBunyanScanSourceFile(relativePath) {
|
|
172
|
+
return SOURCE_FILE_RE.test(relativePath)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Чи слід пропустити файл під час обходу пакета (декларації типів).
|
|
177
|
+
* @param {string} relativePosix шлях з posix-слешами
|
|
178
|
+
* @returns {boolean} `true`, якщо файл не сканувати
|
|
179
|
+
*/
|
|
180
|
+
export function shouldSkipFileForBunyanScan(relativePosix) {
|
|
181
|
+
return relativePosix.endsWith('.d.ts')
|
|
182
|
+
}
|