@nitra/cursor 1.13.76 → 1.13.82
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/CHANGELOG.md +70 -0
- package/package.json +1 -1
- package/rules/abie/abie.mdc +11 -3
- package/rules/abie/policy/base_deployment_preem/base_deployment_preem.rego +9 -11
- package/rules/abie/policy/health_check_policy/health_check_policy.rego +9 -10
- package/rules/abie/policy/http_route_base/http_route_base.rego +7 -8
- package/rules/changelog/fix/consistency/check.mjs +16 -16
- package/{scripts/utils → rules/changelog/fix/consistency}/package-manifest.mjs +1 -1
- package/rules/docker/docker.mdc +3 -3
- package/rules/docker/fix/lint/check.mjs +3 -3
- package/{scripts/utils → rules/docker/fix/lint}/docker-hadolint.mjs +3 -3
- package/rules/docker/lint/lint.mjs +2 -2
- package/rules/ga/lint/lint.mjs +4 -1
- package/rules/graphql/fix/tooling/check.mjs +1 -1
- package/{scripts/utils → rules/graphql/fix/tooling}/graphql-gql-scan.mjs +47 -12
- package/rules/hasura/fix/internal_urls/check.mjs +1 -1
- package/{scripts/utils → rules/js-bun-db/fix/safety}/bun-sql-scan.mjs +1 -1
- package/rules/js-bun-db/fix/safety/check.mjs +1 -1
- package/rules/js-lint/fix/tooling/check.mjs +3 -15
- package/{scripts/utils → rules/js-lint/fix/tooling}/rebuild-oxlint-canonical.mjs +1 -1
- package/rules/js-lint/js-lint.mdc +2 -2
- package/rules/js-mssql/fix/deps/check.mjs +1 -1
- package/{scripts/utils → rules/js-mssql/fix/deps}/mssql-pool-scan.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/bunyan-imports.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/check-env-scan.mjs +1 -1
- package/rules/js-run/fix/runtime/check.mjs +5 -5
- package/{scripts/utils → rules/js-run/fix/runtime}/conn-file-rules.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/conn-imports-scan.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/promise-settimeout-scan.mjs +1 -1
- package/rules/k8s/k8s.mdc +1 -1
- package/rules/npm-module/fix/package_structure/check.mjs +11 -1
- package/rules/test/auto.md +1 -0
- package/rules/test/fix/location/check.mjs +77 -0
- package/rules/test/test.mdc +60 -0
- package/rules/vue/fix/packages/check.mjs +2 -2
- package/rules/vue/vue.mdc +1 -1
- package/scripts/auto-rules.mjs +3 -3
- package/scripts/utils/ast-scan-utils.mjs +3 -2
- package/scripts/utils/with-lock.mjs +120 -0
- package/scripts/utils/workspaces.mjs +1 -1
- package/scripts/utils/worktree-fingerprint.mjs +30 -0
- /package/{scripts/utils → rules/docker/fix/lint}/docker-mirror.mjs +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/knip-canonical.json +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-canonical-skeleton.json +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-canonical.json +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-rules.tsv +0 -0
- /package/{scripts/utils → rules/vue/fix/packages}/vue-forbidden-imports.mjs +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
3
|
*
|
|
4
4
|
* Flat ESLint з getConfig і ignore для auto-imports,
|
|
5
|
-
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/
|
|
5
|
+
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/rules/js-lint/fix/tooling/oxlint-canonical.json`):
|
|
6
6
|
* plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
|
|
7
7
|
* globals, ignorePatterns. Також перевіряє workspace `package.json` на `type: "module"`
|
|
8
8
|
* і `engines`, workflow-дубль у `lint.yml`, `knip.json` autofill і застарілі `.eslintrc*`.
|
|
@@ -20,24 +20,12 @@ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mj
|
|
|
20
20
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
21
21
|
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
22
22
|
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
-
'..',
|
|
24
|
-
'..',
|
|
25
|
-
'..',
|
|
26
|
-
'..',
|
|
27
|
-
'scripts',
|
|
28
|
-
'utils',
|
|
29
23
|
'oxlint-canonical.json'
|
|
30
24
|
)
|
|
31
25
|
|
|
32
26
|
/** Шлях до канонічного knip JSON у цьому пакеті — копіюється у корінь проєкту-споживача, якщо відсутній. */
|
|
33
27
|
export const KNIP_CANONICAL_JSON_PATH = join(
|
|
34
28
|
dirname(fileURLToPath(import.meta.url)),
|
|
35
|
-
'..',
|
|
36
|
-
'..',
|
|
37
|
-
'..',
|
|
38
|
-
'..',
|
|
39
|
-
'scripts',
|
|
40
|
-
'utils',
|
|
41
29
|
'knip-canonical.json'
|
|
42
30
|
)
|
|
43
31
|
|
|
@@ -166,7 +154,7 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
|
|
|
166
154
|
|
|
167
155
|
if (!deepEqualOxlintCanonical(actual, expected)) {
|
|
168
156
|
failures.push(
|
|
169
|
-
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/
|
|
157
|
+
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js-lint/fix/tooling/oxlint-canonical.json)`
|
|
170
158
|
)
|
|
171
159
|
}
|
|
172
160
|
}
|
|
@@ -396,7 +384,7 @@ async function checkKnipConfig(passFn, failFn) {
|
|
|
396
384
|
return
|
|
397
385
|
}
|
|
398
386
|
await copyFile(KNIP_CANONICAL_JSON_PATH, 'knip.json')
|
|
399
|
-
passFn('knip.json створено з канонічного npm/
|
|
387
|
+
passFn('knip.json створено з канонічного npm/rules/js-lint/fix/tooling/knip-canonical.json (js-lint.mdc)')
|
|
400
388
|
}
|
|
401
389
|
|
|
402
390
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Збирає `oxlint-canonical.json` з `oxlint-canonical-skeleton.json` (без поля rules) та списку
|
|
3
3
|
* правил у `oxlint-rules.tsv` (колонки: ім’я правила, TAB, severity: deny | off | error).
|
|
4
4
|
*
|
|
5
|
-
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./
|
|
5
|
+
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/fix/tooling/rebuild-oxlint-canonical.mjs`,
|
|
6
6
|
* потім скопіюй оновлений канон у корінь споживача як `.oxlintrc.json` за потреби.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -25,7 +25,7 @@ version: '1.23'
|
|
|
25
25
|
|
|
26
26
|
У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
27
27
|
|
|
28
|
-
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/
|
|
28
|
+
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js-lint/fix/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Оновити канон можна з репозиторію пакета або скопіювавши файл після **`bun ./rules/js-lint/fix/tooling/rebuild-oxlint-canonical.mjs`** (джерело правил — **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
|
|
29
29
|
|
|
30
30
|
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
31
31
|
|
|
@@ -62,7 +62,7 @@ version: '1.23'
|
|
|
62
62
|
|
|
63
63
|
Перевірку невикористаних залежностей і експортів виконує **knip** (заміна `depcheck`). Викликається у скрипті `lint-js` і в CI разом з oxlint/eslint/jscpd — окремий крок у CI не потрібен.
|
|
64
64
|
|
|
65
|
-
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/
|
|
65
|
+
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js-lint/fix/tooling/knip-canonical.json`](./fix/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `depcheck`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
|
|
66
66
|
|
|
67
67
|
Якщо `knip.json` відсутній — `npx @nitra/cursor check js-lint` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
68
68
|
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
findUnsafeMssqlInListUnparsedInText,
|
|
23
23
|
findUnsafeMssqlInListMissingEmptyGuardInText,
|
|
24
24
|
isMssqlScanSourceFile
|
|
25
|
-
} from '
|
|
25
|
+
} from './mssql-pool-scan.mjs'
|
|
26
26
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
27
27
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
normalizeSnippet,
|
|
31
31
|
offsetToLine,
|
|
32
32
|
walkAstWithAncestors
|
|
33
|
-
} from '
|
|
33
|
+
} from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
34
34
|
|
|
35
35
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
36
36
|
const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
offsetToLine,
|
|
19
19
|
requireCallModule,
|
|
20
20
|
walkAstWithAncestors
|
|
21
|
-
} from '
|
|
21
|
+
} from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
22
22
|
|
|
23
23
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
24
24
|
const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
|
|
30
30
|
* бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
|
|
31
31
|
*/
|
|
32
|
-
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '
|
|
32
|
+
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
33
33
|
|
|
34
34
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
35
35
|
const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
|
|
@@ -40,22 +40,22 @@ import {
|
|
|
40
40
|
findBunyanImportsInText,
|
|
41
41
|
isBunyanScanSourceFile,
|
|
42
42
|
shouldSkipFileForBunyanScan
|
|
43
|
-
} from '
|
|
44
|
-
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '
|
|
43
|
+
} from './bunyan-imports.mjs'
|
|
44
|
+
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './check-env-scan.mjs'
|
|
45
45
|
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
46
46
|
import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
|
|
47
|
-
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '
|
|
47
|
+
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './conn-file-rules.mjs'
|
|
48
48
|
import {
|
|
49
49
|
findConnFactoryImportsInText,
|
|
50
50
|
isConnImportsScanSourceFile,
|
|
51
51
|
isInsideConnDir,
|
|
52
52
|
resolveConnDirFromPackageJson
|
|
53
|
-
} from '
|
|
53
|
+
} from './conn-imports-scan.mjs'
|
|
54
54
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
55
55
|
import {
|
|
56
56
|
findPromiseSetTimeoutInText,
|
|
57
57
|
isPromiseSetTimeoutScanSourceFile
|
|
58
|
-
} from '
|
|
58
|
+
} from './promise-settimeout-scan.mjs'
|
|
59
59
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
60
60
|
import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
|
|
61
61
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Парсимо через oxc-parser; коли файл не парситься — повертаємо порожні результати, щоб
|
|
15
15
|
* не змішувати помилки синтаксису з порушеннями цього правила.
|
|
16
16
|
*/
|
|
17
|
-
import { parseProgramOrNull } from '
|
|
17
|
+
import { parseProgramOrNull } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
18
18
|
|
|
19
19
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
20
20
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* використовується. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
17
17
|
* треба полагодити синтаксис.
|
|
18
18
|
*/
|
|
19
|
-
import { langFromPath, normalizeSnippet, offsetToLine } from '
|
|
19
|
+
import { langFromPath, normalizeSnippet, offsetToLine } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
20
20
|
import { parseSync } from 'oxc-parser'
|
|
21
21
|
|
|
22
22
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається
|
|
13
13
|
* порожній результат (як інші сканери — спочатку треба полагодити синтаксис).
|
|
14
14
|
*/
|
|
15
|
-
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '
|
|
15
|
+
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
16
16
|
|
|
17
17
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
18
18
|
|
package/rules/k8s/k8s.mdc
CHANGED
|
@@ -375,7 +375,7 @@ images:
|
|
|
375
375
|
|
|
376
376
|
Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
|
|
377
377
|
|
|
378
|
-
**`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — **`
|
|
378
|
+
**`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — Rego-пакет **`abie.health_check_policy`** + **abie.mdc**.
|
|
379
379
|
|
|
380
380
|
За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
|
|
381
381
|
|
|
@@ -453,6 +453,11 @@ export function findTestFrameworkImport(content, virtualPath) {
|
|
|
453
453
|
* Класифікує опублікований файл як test/fixture, якщо хоча б одна з ознак:
|
|
454
454
|
* (1) у шляху є каталог із `TEST_DIR_NAMES`; (2) basename відповідає
|
|
455
455
|
* `TEST_FILE_PATTERNS`; (3) для JS/TS-розширень — імпорт test-фреймворку.
|
|
456
|
+
*
|
|
457
|
+
* Carve-out: для шляху `rules/<rule-name>/...` сегмент `<rule-name>` (індекс 1)
|
|
458
|
+
* — це ім'я правила, а не каталог. Зокрема, правило з id `test` (або `tests`)
|
|
459
|
+
* описує конвенцію розміщення тестів і саме по собі не є test-fixture'ом.
|
|
460
|
+
* Подальші сегменти (наприклад, `rules/<r>/fix/<c>/tests/`) продовжують перевірятись.
|
|
456
461
|
* @param {string} relPath posix-шлях відносно `npm/`
|
|
457
462
|
* @returns {Promise<string | null>} причина порушення або `null`
|
|
458
463
|
*/
|
|
@@ -460,7 +465,12 @@ export async function classifyPublishedFileAsTest(relPath) {
|
|
|
460
465
|
const segments = relPath.split('/')
|
|
461
466
|
const base = segments.at(-1)
|
|
462
467
|
const dirs = segments.slice(0, -1)
|
|
463
|
-
const testDir = dirs.find(seg =>
|
|
468
|
+
const testDir = dirs.find((seg, idx) => {
|
|
469
|
+
if (idx === 1 && dirs[0] === 'rules') {
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
472
|
+
return TEST_DIR_NAMES.has(seg.toLowerCase())
|
|
473
|
+
})
|
|
464
474
|
if (testDir) return `test-style каталог "${testDir}/"`
|
|
465
475
|
if (TEST_FILE_PATTERNS.some(re => re.test(base))) return `test-style ім'я файлу`
|
|
466
476
|
if (JS_LIKE_EXT_RE.test(base)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
якщо у проекті є хоча б один файл `*.test.mjs`
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє, що всі `*.test.mjs` лежать у каталозі `tests/` (а не поряд із джерельним файлом).
|
|
3
|
+
*
|
|
4
|
+
* Конвенція (test.mdc): `dir/foo.mjs` → тест у `dir/tests/foo.test.mjs`.
|
|
5
|
+
* `*_test.rego` виключені: Rego unit-тести живуть поряд із полісі (OPA community convention).
|
|
6
|
+
*
|
|
7
|
+
* Пропускає: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv` (через `walkDir`)
|
|
8
|
+
* і шляхи з `.n-cursor.json:ignore`.
|
|
9
|
+
*/
|
|
10
|
+
import { basename, dirname, relative } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
13
|
+
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
14
|
+
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
15
|
+
|
|
16
|
+
const TESTS_DIR_NAME = 'tests'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Чи файл є JS-тестом (`*.test.mjs`).
|
|
20
|
+
* @param {string} absPath абсолютний шлях
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isTestFile(absPath) {
|
|
24
|
+
return basename(absPath).endsWith('.test.mjs')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Перевіряє, чи лежить тест у каталозі з іменем `tests`.
|
|
29
|
+
* @param {string} absPath абсолютний шлях до тесту
|
|
30
|
+
* @returns {boolean} `true`, якщо басенейм батьківської директорії — `tests`
|
|
31
|
+
*/
|
|
32
|
+
function isInsideTestsDir(absPath) {
|
|
33
|
+
return basename(dirname(absPath)) === TESTS_DIR_NAME
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Перевіряє розміщення тестових файлів у каталозі `tests/` (test.mdc).
|
|
38
|
+
* @returns {Promise<number>} 0 — всі тести у `tests/`, 1 — є порушення
|
|
39
|
+
*/
|
|
40
|
+
export async function check() {
|
|
41
|
+
const reporter = createCheckReporter()
|
|
42
|
+
const { pass, fail } = reporter
|
|
43
|
+
|
|
44
|
+
const cwd = process.cwd()
|
|
45
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
46
|
+
|
|
47
|
+
/** @type {string[]} */
|
|
48
|
+
const offenders = []
|
|
49
|
+
let totalTests = 0
|
|
50
|
+
|
|
51
|
+
await walkDir(
|
|
52
|
+
cwd,
|
|
53
|
+
absPath => {
|
|
54
|
+
if (!isTestFile(absPath)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
totalTests++
|
|
58
|
+
if (!isInsideTestsDir(absPath)) {
|
|
59
|
+
offenders.push(relative(cwd, absPath))
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
ignorePaths
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (offenders.length === 0) {
|
|
66
|
+
pass(`Всі ${totalTests} файлів *.test.mjs у каталозі tests/ (test.mdc)`)
|
|
67
|
+
return reporter.getExitCode()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const offenderPath of offenders) {
|
|
71
|
+
const parentDir = dirname(offenderPath)
|
|
72
|
+
const base = basename(offenderPath)
|
|
73
|
+
fail(`${offenderPath}: тест має лежати у tests/ — перенеси у ${parentDir}/${TESTS_DIR_NAME}/${base} (test.mdc)`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return reporter.getExitCode()
|
|
77
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: JS-тести (*.test.mjs) живуть у каталозі tests/ поряд із джерельним файлом, а не безпосередньо в тій же директорії
|
|
3
|
+
version: '1.1'
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Конвенція розміщення тестів
|
|
8
|
+
|
|
9
|
+
JS-тести у пакеті лежать у **піддиректорії `tests/`** поряд із кодом, який тестується, а **не безпосередньо біля джерельного файлу**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
rules/foo/fix/bar/
|
|
13
|
+
├── check.mjs ← JS-джерело
|
|
14
|
+
└── tests/
|
|
15
|
+
└── check.test.mjs ← тест check.mjs
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Чому так:**
|
|
19
|
+
|
|
20
|
+
- Каталог-з-кодом залишається чистим: продакшен-модулі, политики, темплейти не миготять серед тестових файлів.
|
|
21
|
+
- Один погляд на дерево показує, що піде у npm tarball (все, крім `tests/` і `__fixtures__/` — їх ловить `package.json#files` через негативні globs).
|
|
22
|
+
- Якщо тест переростає у мульти-файловий — він уже у власному каталозі, без хаотичного розкидання.
|
|
23
|
+
|
|
24
|
+
**Виняток — Rego unit-тести (`*_test.rego`):** лишаються **поряд із полісі-файлом** відповідно до загальноприйнятого OPA/Conftest community-патерну. `conftest verify -p <dir>` рекурсивно підхоплює їх незалежно від місця в каталозі.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
rules/foo/policy/package_json/
|
|
28
|
+
├── package_json.rego ← Rego-правило
|
|
29
|
+
├── package_json_test.rego ← Rego unit-тести (поряд, не у tests/)
|
|
30
|
+
└── target.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Спеціальні випадки:**
|
|
34
|
+
|
|
35
|
+
- **Integration-тести на рівні пакета** — у `<root>/tests/` (наприклад `npm/tests/`). Це не «біля файлу», а виокремлений каталог для тестів, що покривають весь пакет.
|
|
36
|
+
- **Fixtures**: `__fixtures__/` (для shared) і `fixtures/` (для rule-specific) також живуть **усередині `tests/`**: `tests/__fixtures__/...` і `tests/fixtures/...`.
|
|
37
|
+
- **Test helpers** (`test-helpers.mjs`) можуть лишатися у `scripts/utils/` як shared infra — конвенція стосується файлів `*.test.mjs`.
|
|
38
|
+
|
|
39
|
+
**Гарантії tarball-чистоти** через `package.json#files`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
"files": [
|
|
43
|
+
"...",
|
|
44
|
+
"!**/*.test.mjs",
|
|
45
|
+
"!**/*_test.rego",
|
|
46
|
+
"!**/__fixtures__/**",
|
|
47
|
+
"!**/fixtures/**",
|
|
48
|
+
"!**/test-helpers.mjs"
|
|
49
|
+
]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Recursive globs ловлять файли всередині `tests/` так само, як ловили б їх біля джерела.
|
|
53
|
+
|
|
54
|
+
## Що перевіряє правило
|
|
55
|
+
|
|
56
|
+
`npx @nitra/cursor check test` (concern `location`) проходить деревом пакета й перевіряє: **кожен `*.test.mjs` файл лежить усередині каталогу з ім'ям `tests`** (точне співпадіння басенейму батьківської директорії). Файли поза цим каталогом репортуються з порадою куди їх перенести.
|
|
57
|
+
|
|
58
|
+
`*_test.rego` перевіркою **не охоплюються** — вони не переміщуються.
|
|
59
|
+
|
|
60
|
+
Пропускаються: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`, шляхи з `.n-cursor.json:ignore`.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* натомість використовуй `mode` з `defineConfig(({ mode }) => ...)`.
|
|
16
16
|
*
|
|
17
17
|
* Заборонені явні value-імпорти з `vue` у джерелах пакета — сканування `.vue`/`.ts`/`.js` тощо
|
|
18
|
-
* через **oxc-parser** (`module.staticImports`; див.
|
|
18
|
+
* через **oxc-parser** (`module.staticImports`; див. `./vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
|
|
19
19
|
*
|
|
20
20
|
* Окремо в `.vue` SFC заборонено імпорти Node-нативних модулів — `node:*` префікс або bare-ім’я
|
|
21
21
|
* вбудованого модуля Node (`fs`, `path`, `timers/promises` тощо). Vue SFC виконується у браузері,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
findForbiddenVueImportsInSourceFile,
|
|
32
32
|
isVueImportScanSourceFile,
|
|
33
33
|
shouldSkipFileForVueImportScan
|
|
34
|
-
} from '
|
|
34
|
+
} from './vue-forbidden-imports.mjs'
|
|
35
35
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
36
36
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
37
37
|
import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
|
package/rules/vue/vue.mdc
CHANGED
|
@@ -341,4 +341,4 @@ import path from 'node:path'
|
|
|
341
341
|
|
|
342
342
|
## Перевірка
|
|
343
343
|
|
|
344
|
-
`npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/
|
|
344
|
+
`npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/rules/vue/fix/packages/vue-forbidden-imports.mjs`).
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -16,13 +16,13 @@ import { existsSync } from 'node:fs'
|
|
|
16
16
|
import { readdir, readFile } from 'node:fs/promises'
|
|
17
17
|
import { basename, join, relative } from 'node:path'
|
|
18
18
|
|
|
19
|
-
import { textHasBunSqlImport } from '
|
|
19
|
+
import { textHasBunSqlImport } from '../rules/js-bun-db/fix/safety/bun-sql-scan.mjs'
|
|
20
20
|
import {
|
|
21
21
|
isGqlScanSourceFile,
|
|
22
22
|
shouldSkipFileForGqlScan,
|
|
23
23
|
sourceFileHasGqlTaggedTemplate
|
|
24
|
-
} from '
|
|
25
|
-
import { contentForVueImportScan } from '
|
|
24
|
+
} from '../rules/graphql/fix/tooling/graphql-gql-scan.mjs'
|
|
25
|
+
import { contentForVueImportScan } from '../rules/vue/fix/packages/vue-forbidden-imports.mjs'
|
|
26
26
|
|
|
27
27
|
/** Порядок автододавання правил відповідно до `rules/<rule>/auto.md`. */
|
|
28
28
|
export const AUTO_RULE_ORDER = Object.freeze([
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* розпізнавання типових вузлів (функцій, `*.join(...)`),
|
|
6
6
|
* робота з `TemplateLiteral` (текст quasis, контекст SQL-списку).
|
|
7
7
|
*
|
|
8
|
-
* Використовується
|
|
9
|
-
*
|
|
8
|
+
* Використовується сканерами у `rules/<rule>/fix/...` (наприклад,
|
|
9
|
+
* `rules/js-bun-db/fix/safety/bun-sql-scan.mjs`, `rules/js-mssql/fix/deps/mssql-pool-scan.mjs`,
|
|
10
|
+
* `rules/js-run/fix/runtime/*-scan.mjs`) для уникнення дублювання boilerplate.
|
|
10
11
|
*/
|
|
11
12
|
import { parseSync } from 'oxc-parser'
|
|
12
13
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard-механізм: атомарний lock + dedup для важких команд.
|
|
3
|
+
* Алгоритм: mkdirSync-based lock, перевірка живості PID, sha256-dedup з TTL.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'node:fs'
|
|
6
|
+
import * as path from 'node:path'
|
|
7
|
+
import * as os from 'node:os'
|
|
8
|
+
import { worktreeFingerprint } from './worktree-fingerprint.mjs'
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
ttl: 600_000,
|
|
12
|
+
staleThreshold: 1_800_000,
|
|
13
|
+
waitTimeout: 1_200_000,
|
|
14
|
+
pollInterval: 1_500,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isAlive(pid) {
|
|
18
|
+
try { process.kill(pid, 0); return true } catch { return false }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise(r => setTimeout(r, ms))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeRelease(lockDir) {
|
|
26
|
+
return () => fs.rmSync(lockDir, { recursive: true, force: true })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{exitCode:number, fingerprint:string|null, finishedAt:number}} result
|
|
31
|
+
* @param {string|null} fingerprint
|
|
32
|
+
* @param {number} ttl
|
|
33
|
+
*/
|
|
34
|
+
export function shouldDedup(result, fingerprint, ttl) {
|
|
35
|
+
if (result.exitCode !== 0) return false
|
|
36
|
+
if (fingerprint === null || result.fingerprint !== fingerprint) return false
|
|
37
|
+
if (Date.now() - result.finishedAt >= ttl) return false
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} key
|
|
43
|
+
* @param {() => Promise<number>} runFn
|
|
44
|
+
* @param {{ttl?:number, staleThreshold?:number, waitTimeout?:number, pollInterval?:number, cacheDir?:string}} [opts]
|
|
45
|
+
* @returns {Promise<number>}
|
|
46
|
+
*/
|
|
47
|
+
export async function withLock(key, runFn, opts = {}) {
|
|
48
|
+
const { ttl, staleThreshold, waitTimeout, pollInterval } = { ...DEFAULTS, ...opts }
|
|
49
|
+
const getFingerprint = opts.getFingerprint ?? worktreeFingerprint
|
|
50
|
+
const cacheDir = opts.cacheDir ?? path.join(process.cwd(), 'node_modules/.cache/n-cursor', key)
|
|
51
|
+
const lockDir = path.join(cacheDir, 'lock')
|
|
52
|
+
const ownerFile = path.join(lockDir, 'owner.json')
|
|
53
|
+
const resultFile = path.join(cacheDir, 'result.json')
|
|
54
|
+
const release = makeRelease(lockDir)
|
|
55
|
+
|
|
56
|
+
const fingerprint = getFingerprint()
|
|
57
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
58
|
+
|
|
59
|
+
const loopStart = Date.now()
|
|
60
|
+
let locked = false
|
|
61
|
+
|
|
62
|
+
// eslint-disable-next-line no-constant-condition
|
|
63
|
+
while (true) {
|
|
64
|
+
if (Date.now() - loopStart >= waitTimeout) {
|
|
65
|
+
console.error(`⚠️ ${key}: чекав ${waitTimeout / 60_000} хв — запускаю без локу`)
|
|
66
|
+
return await runFn()
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(lockDir)
|
|
70
|
+
fs.writeFileSync(ownerFile, JSON.stringify({ pid: process.pid, host: os.hostname(), startedAt: Date.now(), fingerprint }))
|
|
71
|
+
locked = true
|
|
72
|
+
break
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code !== 'EEXIST') throw error
|
|
75
|
+
let owner
|
|
76
|
+
try { owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8')) } catch {
|
|
77
|
+
fs.rmSync(lockDir, { recursive: true, force: true })
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const stale = (Date.now() - owner.startedAt > staleThreshold) ||
|
|
81
|
+
(os.hostname() === owner.host && !isAlive(owner.pid))
|
|
82
|
+
if (stale) {
|
|
83
|
+
console.error(`🧹 ${key}: знайдено застарілий лок — очищаю`)
|
|
84
|
+
fs.rmSync(lockDir, { recursive: true, force: true })
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
console.error(`⏳ ${key}: чекаю, лок тримає pid ${owner.pid}…`)
|
|
88
|
+
await sleep(pollInterval)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.error(`🔒 ${key}: лок взято`)
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const raw = fs.readFileSync(resultFile, 'utf8')
|
|
96
|
+
const result = JSON.parse(raw)
|
|
97
|
+
if (shouldDedup(result, fingerprint, ttl)) {
|
|
98
|
+
const elapsed = Math.round((Date.now() - result.finishedAt) / 1000)
|
|
99
|
+
console.error(`♻️ ${key}: те саме дерево, ${elapsed}с тому — пропускаю`)
|
|
100
|
+
release()
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
} catch { /* result.json не існує або пошкоджений */ }
|
|
104
|
+
|
|
105
|
+
const onSignal = () => { release(); process.exit(130) }
|
|
106
|
+
process.once('SIGINT', onSignal)
|
|
107
|
+
process.once('SIGTERM', onSignal)
|
|
108
|
+
|
|
109
|
+
let code
|
|
110
|
+
try {
|
|
111
|
+
code = await runFn()
|
|
112
|
+
fs.writeFileSync(resultFile, JSON.stringify({ finishedAt: Date.now(), exitCode: code, fingerprint }))
|
|
113
|
+
} finally {
|
|
114
|
+
process.off('SIGINT', onSignal)
|
|
115
|
+
process.off('SIGTERM', onSignal)
|
|
116
|
+
release()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return code
|
|
120
|
+
}
|
|
@@ -11,7 +11,7 @@ import { dirname, join, relative } from 'node:path'
|
|
|
11
11
|
const TRAILING_SLASH_RE = /\/$/
|
|
12
12
|
const LEADING_DOTSLASH_RE = /^\.\//
|
|
13
13
|
|
|
14
|
-
/** Glob-ігнор для workspace-патернів із `*` (узгоджено з `package-manifest.mjs`). */
|
|
14
|
+
/** Glob-ігнор для workspace-патернів із `*` (узгоджено з `rules/changelog/fix/consistency/package-manifest.mjs`). */
|
|
15
15
|
export const WORKSPACE_GLOB_IGNORE = Object.freeze(['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**'])
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprint поточного стану git-робочого дерева.
|
|
3
|
+
* Повертає sha256-hex (64 символи) або null, якщо не в git-репо.
|
|
4
|
+
* @param {typeof import('child_process').spawnSync} spawn
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
export function worktreeFingerprint(spawn = spawnSync) {
|
|
10
|
+
/** @param {string[]} args */
|
|
11
|
+
function git(args) {
|
|
12
|
+
const r = spawn('git', args, { encoding: 'utf8' })
|
|
13
|
+
if (r.status !== 0 || r.error) throw new Error(`git ${args[0]} failed`)
|
|
14
|
+
return r.stdout
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const commitHash = git(['rev-parse', 'HEAD']).trim()
|
|
19
|
+
const diffText = git(['diff', 'HEAD'])
|
|
20
|
+
const untrackedRaw = git(['ls-files', '--others', '--exclude-standard'])
|
|
21
|
+
const untrackedFiles = untrackedRaw.split('\n').filter(Boolean)
|
|
22
|
+
const pairs = untrackedFiles
|
|
23
|
+
.map(f => `${f}:${git(['hash-object', f]).trim()}`)
|
|
24
|
+
.sort()
|
|
25
|
+
const raw = [commitHash, diffText, ...pairs].join('\n')
|
|
26
|
+
return createHash('sha256').update(raw).digest('hex')
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|