@7n/rules-lang-js 0.3.1 → 0.4.1
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 +12 -0
- package/package.json +14 -4
- package/rules/bun/bunfig/bunfig.mdc +17 -0
- package/rules/bun/bunfig/bunfig.rego +29 -0
- package/rules/bun/bunfig/concern.json +9 -0
- package/rules/bun/bunfig/template/bunfig.toml.snippet.toml +2 -0
- package/rules/bun/docs/index.md +11 -0
- package/rules/bun/layout/concern.json +16 -0
- package/rules/bun/layout/docs/fix-layout.md +29 -0
- package/rules/bun/layout/docs/main.md +34 -0
- package/rules/bun/layout/fix-layout.mjs +63 -0
- package/rules/bun/layout/layout.mdc +60 -0
- package/rules/bun/layout/main.mjs +53 -0
- package/rules/bun/licensee/concern.json +7 -0
- package/rules/bun/licensee/docs/fix-licensee.md +27 -0
- package/rules/bun/licensee/docs/index.md +12 -0
- package/rules/bun/licensee/docs/main.md +30 -0
- package/rules/bun/licensee/fix-licensee.mjs +31 -0
- package/rules/bun/licensee/main.mjs +68 -0
- package/rules/bun/lint-surface/concern.json +3 -0
- package/rules/bun/lint-surface/lint-surface.mdc +13 -0
- package/rules/bun/main.json +1 -0
- package/rules/bun/main.mdc +11 -0
- package/rules/bun/package_json/concern.json +9 -0
- package/rules/bun/package_json/docs/fix-package_json.md +28 -0
- package/rules/bun/package_json/docs/index.md +9 -0
- package/rules/bun/package_json/fix-package_json.mjs +311 -0
- package/rules/bun/package_json/package_json.mdc +14 -0
- package/rules/bun/package_json/package_json.rego +64 -0
- package/rules/bun/package_json/template/package.json.deny.json +4 -0
- package/rules/js/check/check.mdc +26 -0
- package/rules/js/check/concern.json +19 -0
- package/rules/js/check/docs/eslint-config.md +56 -0
- package/rules/js/check/docs/fix-check.md +48 -0
- package/rules/js/check/docs/index.md +13 -0
- package/rules/js/check/docs/main.md +48 -0
- package/rules/js/check/eslint-config.mjs +262 -0
- package/rules/js/check/fix-check.mjs +82 -0
- package/rules/js/check/main.mjs +310 -0
- package/rules/js/dep-policy/concern.json +4 -0
- package/rules/js/dep-policy/dep-policy.mdc +36 -0
- package/rules/js/dep-policy/docs/main.md +35 -0
- package/rules/js/dep-policy/main.mjs +99 -0
- package/rules/js/docs/index.md +11 -0
- package/rules/js/eslint/concern.json +8 -0
- package/rules/js/eslint/docs/fix-eslint.md +50 -0
- package/rules/js/eslint/docs/fix-worker.md +31 -0
- package/rules/js/eslint/docs/index.md +11 -0
- package/rules/js/eslint/docs/main.md +35 -0
- package/rules/js/eslint/fix-eslint.mjs +160 -0
- package/rules/js/eslint/fix-worker.mjs +139 -0
- package/rules/js/eslint/main.mjs +115 -0
- package/rules/js/file-extensions/concern.json +3 -0
- package/rules/js/file-extensions/file-extensions.mdc +12 -0
- package/rules/js/jscpd_config/concern.json +11 -0
- package/rules/js/jscpd_config/docs/fix-jscpd_config.md +25 -0
- package/rules/js/jscpd_config/docs/index.md +9 -0
- package/rules/js/jscpd_config/fix-jscpd_config.mjs +3 -0
- package/rules/js/jscpd_config/jscpd_config.mdc +42 -0
- package/rules/js/jscpd_config/jscpd_config.rego +44 -0
- package/rules/js/jscpd_config/template/.jscpd.json.snippet.json +7 -0
- package/rules/js/jscpd_duplicates/concern.json +7 -0
- package/rules/js/jscpd_duplicates/docs/main.md +29 -0
- package/rules/js/jscpd_duplicates/main.mjs +69 -0
- package/rules/js/knip/concern.json +7 -0
- package/rules/js/knip/docs/main.md +31 -0
- package/rules/js/knip/knip.mdc +15 -0
- package/rules/js/knip/main.mjs +89 -0
- package/rules/js/lint-findings/concern.json +3 -0
- package/rules/js/lint-findings/docs/main.md +40 -0
- package/rules/js/lint-findings/main.mjs +125 -0
- package/rules/js/main.json +1 -0
- package/rules/js/main.mdc +18 -0
- package/rules/js/package_json/concern.json +9 -0
- package/rules/js/package_json/docs/fix-package_json.md +25 -0
- package/rules/js/package_json/docs/index.md +9 -0
- package/rules/js/package_json/fix-package_json.mjs +3 -0
- package/rules/js/package_json/package_json.mdc +15 -0
- package/rules/js/package_json/package_json.rego +142 -0
- package/rules/js/package_json/template/package.json.snippet.json +6 -0
- package/rules/js/tooling/concern.json +3 -0
- package/rules/js/tooling/data/tooling/knip-canonical.json +30 -0
- package/rules/js/tooling/data/tooling/oxlint-canonical.json +400 -0
- package/rules/js/tooling/docs/main.md +53 -0
- package/rules/js/tooling/main.mjs +183 -0
- package/rules/js/utils_imports/concern.json +4 -0
- package/rules/js/utils_imports/docs/main.md +50 -0
- package/rules/js/utils_imports/main.mjs +185 -0
- package/rules/js/utils_imports/utils_imports.mdc +15 -0
- package/rules/js/vscode_extensions/concern.json +11 -0
- package/rules/js/vscode_extensions/docs/fix-vscode_extensions.md +24 -0
- package/rules/js/vscode_extensions/docs/index.md +11 -0
- package/rules/js/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/js/vscode_extensions/template/extensions.json.snippet.json +6 -0
- package/rules/js/vscode_extensions/vscode_extensions.mdc +11 -0
- package/rules/js/vscode_extensions/vscode_extensions.rego +12 -0
- package/rules/js-bun-db/connection/concern.json +3 -0
- package/rules/js-bun-db/connection/connection.mdc +42 -0
- package/rules/js-bun-db/docs/index.md +11 -0
- package/rules/js-bun-db/lib/bun-sql-scan.mjs +1047 -0
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +63 -0
- package/rules/js-bun-db/lib/docs/index.md +11 -0
- package/rules/js-bun-db/main.json +1 -0
- package/rules/js-bun-db/main.mdc +8 -0
- package/rules/js-bun-db/package_json/concern.json +9 -0
- package/rules/js-bun-db/package_json/package_json.mdc +31 -0
- package/rules/js-bun-db/package_json/package_json.rego +15 -0
- package/rules/js-bun-db/package_json/template/package.json.deny.json +6 -0
- package/rules/js-bun-db/pg_format_identifiers/concern.json +3 -0
- package/rules/js-bun-db/pg_format_identifiers/pg_format_identifiers.mdc +104 -0
- package/rules/js-bun-db/safety/concern.json +4 -0
- package/rules/js-bun-db/safety/docs/main.md +34 -0
- package/rules/js-bun-db/safety/main.mjs +430 -0
- package/rules/js-bun-db/safety/safety.mdc +458 -0
- package/rules/js-bun-redis/docs/index.md +11 -0
- package/rules/js-bun-redis/imports/concern.json +4 -0
- package/rules/js-bun-redis/imports/docs/main.md +36 -0
- package/rules/js-bun-redis/imports/imports.mdc +47 -0
- package/rules/js-bun-redis/imports/main.mjs +88 -0
- package/rules/js-bun-redis/lib/docs/index.md +11 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +227 -0
- package/rules/js-bun-redis/lib/redis-imports.mjs +130 -0
- package/rules/js-bun-redis/main.json +1 -0
- package/rules/js-bun-redis/main.mdc +8 -0
- package/rules/js-bun-redis/package_json/concern.json +9 -0
- package/rules/js-bun-redis/package_json/package_json.mdc +11 -0
- package/rules/js-bun-redis/package_json/package_json.rego +15 -0
- package/rules/js-bun-redis/package_json/template/package.json.deny.json +12 -0
- package/rules/js-mssql/deps/concern.json +4 -0
- package/rules/js-mssql/deps/docs/main.md +33 -0
- package/rules/js-mssql/deps/main.mjs +297 -0
- package/rules/js-mssql/docs/index.md +11 -0
- package/rules/js-mssql/lib/docs/index.md +11 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +380 -0
- package/rules/js-mssql/lib/mssql-pool-scan.mjs +610 -0
- package/rules/js-mssql/main.json +1 -0
- package/rules/js-mssql/main.mdc +144 -0
- package/rules/js-mssql/mssql-tvp/concern.json +3 -0
- package/rules/js-mssql/mssql-tvp/mssql-tvp.mdc +77 -0
- package/rules/js-mssql/package_json/concern.json +9 -0
- package/rules/js-mssql/package_json/package_json.mdc +9 -0
- package/rules/js-mssql/package_json/package_json.rego +57 -0
- package/rules/js-run/configmap/concern.json +9 -0
- package/rules/js-run/configmap/configmap.mdc +37 -0
- package/rules/js-run/configmap/configmap.rego +21 -0
- package/rules/js-run/configmap/template/configmap.yaml.contains.yml +4 -0
- package/rules/js-run/docs/index.md +11 -0
- package/rules/js-run/jsconfig/concern.json +9 -0
- package/rules/js-run/jsconfig/docs/fix-jsconfig.md +28 -0
- package/rules/js-run/jsconfig/docs/index.md +9 -0
- package/rules/js-run/jsconfig/fix-jsconfig.mjs +119 -0
- package/rules/js-run/jsconfig/jsconfig.mdc +48 -0
- package/rules/js-run/jsconfig/jsconfig.rego +59 -0
- package/rules/js-run/jsconfig/template/jsconfig.json.snippet.json +10 -0
- package/rules/js-run/lib/bunyan-imports.mjs +98 -0
- package/rules/js-run/lib/check-env-scan.mjs +338 -0
- package/rules/js-run/lib/conn-file-rules.mjs +214 -0
- package/rules/js-run/lib/conn-imports-scan.mjs +154 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +121 -0
- package/rules/js-run/lib/docs/check-env-scan.md +438 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +304 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +208 -0
- package/rules/js-run/lib/docs/index.md +16 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +334 -0
- package/rules/js-run/lib/docs/temporal-scan.md +29 -0
- package/rules/js-run/lib/promise-settimeout-scan.mjs +128 -0
- package/rules/js-run/lib/temporal-scan.mjs +52 -0
- package/rules/js-run/main.json +1 -0
- package/rules/js-run/main.mdc +16 -0
- package/rules/js-run/package_json/concern.json +9 -0
- package/rules/js-run/package_json/package_json.mdc +44 -0
- package/rules/js-run/package_json/package_json.rego +37 -0
- package/rules/js-run/package_json/template/package.json.deny.json +22 -0
- package/rules/js-run/project-structure/concern.json +3 -0
- package/rules/js-run/project-structure/project-structure.mdc +11 -0
- package/rules/js-run/runtime/concern.json +12 -0
- package/rules/js-run/runtime/docs/fix-runtime.md +27 -0
- package/rules/js-run/runtime/docs/main.md +35 -0
- package/rules/js-run/runtime/fix-runtime.mjs +46 -0
- package/rules/js-run/runtime/main.mjs +496 -0
- package/rules/js-run/runtime/runtime.mdc +184 -0
- package/rules/js-run/scope/concern.json +3 -0
- package/rules/js-run/scope/scope.mdc +11 -0
- package/rules/npm-module/docs/index.md +11 -0
- package/rules/npm-module/emit_types_config/concern.json +9 -0
- package/rules/npm-module/emit_types_config/docs/fix-emit_types_config.md +25 -0
- package/rules/npm-module/emit_types_config/docs/index.md +9 -0
- package/rules/npm-module/emit_types_config/emit_types_config.mdc +43 -0
- package/rules/npm-module/emit_types_config/emit_types_config.rego +28 -0
- package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +5 -0
- package/rules/npm-module/emit_types_config/template/tsconfig.emit-types.json.snippet.json +9 -0
- package/rules/npm-module/header_doc_pointer/concern.json +5 -0
- package/rules/npm-module/header_doc_pointer/docs/main.md +40 -0
- package/rules/npm-module/header_doc_pointer/header_doc_pointer.mdc +18 -0
- package/rules/npm-module/header_doc_pointer/main.mjs +131 -0
- package/rules/npm-module/main.json +1 -0
- package/rules/npm-module/main.mdc +36 -0
- package/rules/npm-module/npm_package_json/concern.json +9 -0
- package/rules/npm-module/npm_package_json/docs/fix-npm_package_json.md +26 -0
- package/rules/npm-module/npm_package_json/docs/index.md +9 -0
- package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +5 -0
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +57 -0
- package/rules/npm-module/npm_package_json/npm_package_json.rego +73 -0
- package/rules/npm-module/npm_package_json/template/package.json.snippet.json +1 -0
- package/rules/npm-module/package_structure/concern.json +5 -0
- package/rules/npm-module/package_structure/docs/main.md +37 -0
- package/rules/npm-module/package_structure/main.mjs +448 -0
- package/rules/npm-module/package_structure/package_structure.mdc +63 -0
- package/rules/npm-module/root_package_json/concern.json +9 -0
- package/rules/npm-module/root_package_json/docs/fix-root_package_json.md +25 -0
- package/rules/npm-module/root_package_json/docs/index.md +9 -0
- package/rules/npm-module/root_package_json/fix-root_package_json.mjs +5 -0
- package/rules/npm-module/root_package_json/root_package_json.mdc +41 -0
- package/rules/npm-module/root_package_json/root_package_json.rego +28 -0
- package/rules/npm-module/root_package_json/template/package.json.snippet.json +1 -0
- package/rules/npm-module/rule_meta/concern.json +8 -0
- package/rules/npm-module/rule_meta/docs/main.md +35 -0
- package/rules/npm-module/rule_meta/main.mjs +119 -0
- package/rules/npm-module/rule_meta/rule_meta.mdc +11 -0
- package/rules/npm-module/skill_meta/concern.json +5 -0
- package/rules/npm-module/skill_meta/docs/main.md +149 -0
- package/rules/npm-module/skill_meta/main.mjs +91 -0
- package/rules/npm-module/skill_meta/skill_meta.mdc +11 -0
- package/rules/tool-surface/docs/index.md +11 -0
- package/rules/tool-surface/main.json +6 -0
- package/rules/tool-surface/main.mdc +72 -0
- package/rules/vue/composition-api/composition-api.mdc +82 -0
- package/rules/vue/composition-api/concern.json +3 -0
- package/rules/vue/docs/index.md +11 -0
- package/rules/vue/lib/docs/index.md +11 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +265 -0
- package/rules/vue/lib/vue-forbidden-imports.mjs +240 -0
- package/rules/vue/main.json +1 -0
- package/rules/vue/main.mdc +18 -0
- package/rules/vue/nheader-layout/concern.json +3 -0
- package/rules/vue/nheader-layout/nheader-layout.mdc +171 -0
- package/rules/vue/package_json/concern.json +9 -0
- package/rules/vue/package_json/package_json.mdc +30 -0
- package/rules/vue/package_json/package_json.rego +140 -0
- package/rules/vue/packages/concern.json +6 -0
- package/rules/vue/packages/docs/index.md +11 -0
- package/rules/vue/packages/docs/main.md +35 -0
- package/rules/vue/packages/main.mjs +575 -0
- package/rules/vue/packages/packages.mdc +56 -0
- package/rules/vue/quasar-ui/concern.json +3 -0
- package/rules/vue/quasar-ui/quasar-ui.mdc +32 -0
- package/rules/vue/structure/concern.json +3 -0
- package/rules/vue/structure/structure.mdc +101 -0
- package/rules/vue/testing/concern.json +3 -0
- package/rules/vue/testing/testing.mdc +40 -0
- package/rules/vue/tfm-translations/concern.json +7 -0
- package/rules/vue/tfm-translations/docs/main.md +29 -0
- package/rules/vue/tfm-translations/main.mjs +55 -0
- package/rules/vue/tfm-translations/tfm-translations.mdc +32 -0
- package/rules/vue/vite-config/concern.json +3 -0
- package/rules/vue/vite-config/vite-config.mdc +153 -0
- package/rules/vue/vite-env/concern.json +3 -0
- package/rules/vue/vite-env/vite-env.mdc +61 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** @see ./docs/eslint-config.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Детекція воркспейс-типів (node vs vue) і чистий планувальник scaffold/merge
|
|
5
|
+
* для `eslint.config.js` consumer-репо. Використовується двома споживачами:
|
|
6
|
+
* - detector `main.mjs` (read-only): перевірка, що кожен vue-воркспейс
|
|
7
|
+
* присутній у `vue: [...]` аргументах getConfig — інакше .vue файли не
|
|
8
|
+
* парсяться eslint-ом;
|
|
9
|
+
* - T0 `fix-check.mjs`: детермінований scaffold відсутнього конфігу або
|
|
10
|
+
* хірургічний merge наявного (додати vue-воркспейси, прибрати їх із
|
|
11
|
+
* node-списку, доставити ignores) — БЕЗ повного перезапису файлу.
|
|
12
|
+
*
|
|
13
|
+
* Merge fail-safe: якщо структура конфігу не розпізнається regex-ами
|
|
14
|
+
* (немає `getConfig({`), планувальник повертає null і файл не чіпається.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { readFile } from 'node:fs/promises'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
import { globby } from 'globby'
|
|
21
|
+
|
|
22
|
+
/** Відсутній eslint.config.{js,mjs} — T0 скаффолдить із детектованих типів. */
|
|
23
|
+
export const ESLINT_CONFIG_MISSING = 'eslint-config-missing'
|
|
24
|
+
/** У ignores немає `**\/auto-imports.d.ts` — T0 дописує в наявний масив. */
|
|
25
|
+
export const ESLINT_CONFIG_IGNORES = 'eslint-config-ignores'
|
|
26
|
+
/** Vue-воркспейс відсутній у `vue: [...]` getConfig — .vue файли не парсяться. */
|
|
27
|
+
export const ESLINT_CONFIG_VUE_WORKSPACE = 'eslint-config-vue-workspace'
|
|
28
|
+
|
|
29
|
+
export const AUTO_IMPORTS_IGNORE = '**/auto-imports.d.ts'
|
|
30
|
+
|
|
31
|
+
const VUE_LIST_RE = /\bvue\s*:\s*\[([^\]]*)\]/u
|
|
32
|
+
const NODE_LIST_RE = /\bnode\s*:\s*\[([^\]]*)\]/u
|
|
33
|
+
const GET_CONFIG_OBJ_RE = /getConfig\(\s*\{/u
|
|
34
|
+
const IGNORES_OPEN_RE = /\bignores\s*:\s*\[/u
|
|
35
|
+
const STRING_ENTRY_RE = /'([^']*)'|"([^"]*)"/gu
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} p шлях воркспейсу з package.json або запис зі списку getConfig
|
|
39
|
+
* @returns {string} канонічна форма без `./` на початку і `/` в кінці
|
|
40
|
+
*/
|
|
41
|
+
function normalizeWs(p) {
|
|
42
|
+
let s = p.startsWith('./') ? p.slice(2) : p
|
|
43
|
+
while (s.endsWith('/')) s = s.slice(0, -1)
|
|
44
|
+
return s || '.'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} abs абсолютний шлях до JSON-файлу
|
|
49
|
+
* @returns {Promise<object|null>} розпарсений об'єкт або null (немає/битий)
|
|
50
|
+
*/
|
|
51
|
+
async function readJsonOrNull(abs) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(await readFile(abs, 'utf8'))
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Розгортає записи `workspaces` root package.json у список наявних директорій
|
|
61
|
+
* (glob-записи типу `packages/*` — через globby onlyDirectories).
|
|
62
|
+
* @param {string} cwd корінь репозиторію
|
|
63
|
+
* @param {unknown[]} patterns поле workspaces
|
|
64
|
+
* @returns {Promise<string[]>} posix-relative директорії воркспейсів, що існують
|
|
65
|
+
*/
|
|
66
|
+
async function expandWorkspaces(cwd, patterns) {
|
|
67
|
+
const dirs = []
|
|
68
|
+
for (const p of patterns) {
|
|
69
|
+
if (typeof p !== 'string' || p.length === 0) continue
|
|
70
|
+
const norm = normalizeWs(p)
|
|
71
|
+
if (norm.includes('*')) {
|
|
72
|
+
dirs.push(...(await globby(norm, { cwd, onlyDirectories: true, gitignore: false })))
|
|
73
|
+
} else if (existsSync(join(cwd, norm))) {
|
|
74
|
+
dirs.push(norm)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(dirs.map(d => normalizeWs(d)))]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Чи є директорія Vue-кодом: `vue`/`nuxt` у deps її package.json (дешева
|
|
82
|
+
* перевірка першою) або наявність бодай одного `.vue` файлу.
|
|
83
|
+
* @param {string} cwd корінь репозиторію
|
|
84
|
+
* @param {string} ws posix-relative директорія воркспейсу (або `.` для кореня)
|
|
85
|
+
* @returns {Promise<boolean>} true — vue-воркспейс
|
|
86
|
+
*/
|
|
87
|
+
async function isVueWorkspace(cwd, ws) {
|
|
88
|
+
const dir = ws === '.' ? cwd : join(cwd, ws)
|
|
89
|
+
const pkg = await readJsonOrNull(join(dir, 'package.json'))
|
|
90
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
91
|
+
if ('vue' in deps || 'nuxt' in deps) return true
|
|
92
|
+
const vueFiles = await globby('**/*.vue', {
|
|
93
|
+
cwd: dir,
|
|
94
|
+
gitignore: false,
|
|
95
|
+
ignore: ['**/node_modules/**', '**/dist/**'],
|
|
96
|
+
deep: 8
|
|
97
|
+
})
|
|
98
|
+
return vueFiles.length > 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Класифікує репозиторій за воркспейс-типами для аргументів getConfig.
|
|
103
|
+
* Монорепо (workspaces у root package.json) — кожен воркспейс окремо;
|
|
104
|
+
* без workspaces — сам корінь як `.`.
|
|
105
|
+
* @param {string} cwd корінь репозиторію
|
|
106
|
+
* @returns {Promise<{ node: string[], vue: string[] }>} директорії за типами
|
|
107
|
+
*/
|
|
108
|
+
export async function detectWorkspaceTypes(cwd) {
|
|
109
|
+
const pkg = await readJsonOrNull(join(cwd, 'package.json'))
|
|
110
|
+
const wsField = Array.isArray(pkg?.workspaces) ? pkg.workspaces : []
|
|
111
|
+
const dirs = await expandWorkspaces(cwd, wsField)
|
|
112
|
+
|
|
113
|
+
if (dirs.length === 0) {
|
|
114
|
+
const rootIsVue = await isVueWorkspace(cwd, '.')
|
|
115
|
+
return rootIsVue ? { node: [], vue: ['.'] } : { node: ['.'], vue: [] }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @type {{ node: string[], vue: string[] }} */
|
|
119
|
+
const types = { node: [], vue: [] }
|
|
120
|
+
for (const ws of dirs) {
|
|
121
|
+
if (await isVueWorkspace(cwd, ws)) {
|
|
122
|
+
types.vue.push(ws)
|
|
123
|
+
} else {
|
|
124
|
+
types.node.push(ws)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return types
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Записи string-літералів усередині вмісту списку (`vue: [...]`/`node: [...]`).
|
|
132
|
+
* @param {string|undefined} inner текст між дужками списку
|
|
133
|
+
* @returns {string[]} нормалізовані записи списку
|
|
134
|
+
*/
|
|
135
|
+
function listEntries(inner) {
|
|
136
|
+
if (inner === undefined) return []
|
|
137
|
+
return inner
|
|
138
|
+
.matchAll(STRING_ENTRY_RE)
|
|
139
|
+
.map(m => normalizeWs(m[1] ?? m[2]))
|
|
140
|
+
.toArray()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Vue-записи з тексту конфігу — для перевірки detector-ом.
|
|
145
|
+
* @param {string} raw вміст eslint.config
|
|
146
|
+
* @returns {string[]} нормалізовані записи `vue: [...]`
|
|
147
|
+
*/
|
|
148
|
+
export function parseVueList(raw) {
|
|
149
|
+
return listEntries(raw.match(VUE_LIST_RE)?.[1])
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} ws запис для вставки у список
|
|
154
|
+
* @returns {string} одинарно-квотований літерал
|
|
155
|
+
*/
|
|
156
|
+
function quote(ws) {
|
|
157
|
+
return `'${ws}'`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Повний шаблон eslint.config.js для scaffold (файл відсутній). Включає лише
|
|
162
|
+
* непорожні типи; порядок ключів — node, vue (стиль власного конфігу репо).
|
|
163
|
+
* @param {{ node: string[], vue: string[] }} types детектовані воркспейс-типи
|
|
164
|
+
* @returns {string} вміст файлу
|
|
165
|
+
*/
|
|
166
|
+
export function renderEslintConfigScaffold(types) {
|
|
167
|
+
const args = []
|
|
168
|
+
if (types.node.length > 0) args.push(` node: [${types.node.map(w => quote(w)).join(', ')}]`)
|
|
169
|
+
if (types.vue.length > 0) args.push(` vue: [${types.vue.map(w => quote(w)).join(', ')}]`)
|
|
170
|
+
return [
|
|
171
|
+
"import { getConfig } from '@nitra/eslint-config'",
|
|
172
|
+
'',
|
|
173
|
+
'export default [',
|
|
174
|
+
' {',
|
|
175
|
+
` ignores: ['${AUTO_IMPORTS_IGNORE}']`,
|
|
176
|
+
' },',
|
|
177
|
+
' ...getConfig({',
|
|
178
|
+
args.join(',\n'),
|
|
179
|
+
' })',
|
|
180
|
+
']',
|
|
181
|
+
''
|
|
182
|
+
].join('\n')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Хірургічний merge наявного конфігу під детектовані типи:
|
|
187
|
+
* 1) відсутній `**\/auto-imports.d.ts` → вставка в перший `ignores: [`;
|
|
188
|
+
* 2) vue-воркспейси поза `vue: [...]` → вставка у список (або нова властивість
|
|
189
|
+
* одразу після `getConfig({`);
|
|
190
|
+
* 3) ті самі воркспейси у `node: [...]` → вилучення звідти.
|
|
191
|
+
* Решта файлу (кастомні ignores, overrides, коментарі) — недоторкана.
|
|
192
|
+
* @param {string} raw вміст eslint.config
|
|
193
|
+
* @param {{ node: string[], vue: string[] }} types детектовані воркспейс-типи
|
|
194
|
+
* @returns {string} новий вміст (=== raw, якщо merge неможливий/не потрібен)
|
|
195
|
+
*/
|
|
196
|
+
export function mergeEslintConfig(raw, types) {
|
|
197
|
+
let out = raw
|
|
198
|
+
|
|
199
|
+
if (!out.includes(AUTO_IMPORTS_IGNORE) && IGNORES_OPEN_RE.test(out)) {
|
|
200
|
+
out = out.replace(IGNORES_OPEN_RE, m => `${m}'${AUTO_IMPORTS_IGNORE}', `)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const missingVue = types.vue.filter(ws => !parseVueList(out).includes(ws))
|
|
204
|
+
if (missingVue.length > 0) {
|
|
205
|
+
const inserted = missingVue.map(w => quote(w)).join(', ')
|
|
206
|
+
const vueMatch = out.match(VUE_LIST_RE)
|
|
207
|
+
if (vueMatch) {
|
|
208
|
+
const rest = vueMatch[1].trim().length > 0 ? `, ${vueMatch[1]}` : vueMatch[1]
|
|
209
|
+
out = out.replace(VUE_LIST_RE, () => `vue: [${inserted}${rest}]`)
|
|
210
|
+
} else if (GET_CONFIG_OBJ_RE.test(out)) {
|
|
211
|
+
out = out.replace(GET_CONFIG_OBJ_RE, m => `${m}\n vue: [${inserted}],`)
|
|
212
|
+
}
|
|
213
|
+
// без getConfig({ — merge неможливий, лишаємо як є (fail-safe)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const nodeMatch = out.match(NODE_LIST_RE)
|
|
217
|
+
if (nodeMatch) {
|
|
218
|
+
const entries = listEntries(nodeMatch[1])
|
|
219
|
+
const kept = entries.filter(e => !types.vue.includes(e))
|
|
220
|
+
if (kept.length !== entries.length) {
|
|
221
|
+
out = out.replace(NODE_LIST_RE, () => `node: [${kept.map(w => quote(w)).join(', ')}]`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return out
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* План детермінованого фіксу eslint.config для T0: scaffold відсутнього файлу
|
|
230
|
+
* або merge наявного. Ідемпотентний — повторний виклик на виправленому дереві
|
|
231
|
+
* повертає null.
|
|
232
|
+
* @param {string} cwd корінь репозиторію
|
|
233
|
+
* @returns {Promise<{ path: string, content: string, message: string }|null>} запис для виконання або null
|
|
234
|
+
*/
|
|
235
|
+
export async function planEslintConfigFix(cwd) {
|
|
236
|
+
const types = await detectWorkspaceTypes(cwd)
|
|
237
|
+
|
|
238
|
+
const existing = ['eslint.config.js', 'eslint.config.mjs'].find(f => existsSync(join(cwd, f)))
|
|
239
|
+
if (!existing) {
|
|
240
|
+
const summary = [
|
|
241
|
+
types.node.length > 0 ? `node: [${types.node.join(', ')}]` : '',
|
|
242
|
+
types.vue.length > 0 ? `vue: [${types.vue.join(', ')}]` : ''
|
|
243
|
+
]
|
|
244
|
+
.filter(Boolean)
|
|
245
|
+
.join(', ')
|
|
246
|
+
return {
|
|
247
|
+
path: join(cwd, 'eslint.config.js'),
|
|
248
|
+
content: renderEslintConfigScaffold(types),
|
|
249
|
+
message: `створено eslint.config.js (${summary})`
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const abs = join(cwd, existing)
|
|
254
|
+
const raw = await readFile(abs, 'utf8')
|
|
255
|
+
const merged = mergeEslintConfig(raw, types)
|
|
256
|
+
if (merged === raw) return null
|
|
257
|
+
return {
|
|
258
|
+
path: abs,
|
|
259
|
+
content: merged,
|
|
260
|
+
message: `${existing}: merge під воркспейс-типи (vue: [${types.vue.join(', ')}])`
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** @see ./docs/fix-check.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* T0-autofix для `js/check` — детермінований scaffold/merge `eslint.config.js`
|
|
5
|
+
* і `.oxlintrc.json`. Раніше обидва йшли у LLM-ладдер, який переписував конфіги
|
|
6
|
+
* цілком: для eslint.config.js це вгадувало воркспейс-типи (інцидент: у
|
|
7
|
+
* vue-монорепо записано `getConfig({ node: ['npm'] })` — eslint перестав
|
|
8
|
+
* обробляти .vue файли), а для .oxlintrc.json — 15 КБ канону, який дешевій
|
|
9
|
+
* моделі не відтворити byte-perfect (verify fail), а дорожчій — не встигнути
|
|
10
|
+
* за один rung-таймаут.
|
|
11
|
+
*
|
|
12
|
+
* Джерело правди для eslint.config.js — `planEslintConfigFix` (детекція типів
|
|
13
|
+
* із workspaces root package.json + vue-залежність/.vue-файли), наявний конфіг
|
|
14
|
+
* оновлюється точковим merge хірургічно, без повного перезапису.
|
|
15
|
+
*
|
|
16
|
+
* Джерело правди для .oxlintrc.json — `planOxlintrcFix`, що дзеркалить
|
|
17
|
+
* `verifyOxlintRcAgainstCanonical`: відсутній файл копіює канон, наявний —
|
|
18
|
+
* доповнюється до канону без втрати project-specific розширень (зайві
|
|
19
|
+
* `rules`-ключі й `ignorePatterns` зберігаються).
|
|
20
|
+
*
|
|
21
|
+
* Решта порушень js/check (engines, workflows) — поза цим T0, стандартний шлях
|
|
22
|
+
* (ladder/manual).
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync } from 'node:fs'
|
|
25
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
26
|
+
import { join } from 'node:path'
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
ESLINT_CONFIG_IGNORES,
|
|
30
|
+
ESLINT_CONFIG_MISSING,
|
|
31
|
+
ESLINT_CONFIG_VUE_WORKSPACE,
|
|
32
|
+
planEslintConfigFix
|
|
33
|
+
} from './eslint-config.mjs'
|
|
34
|
+
import { OXLINT_CANONICAL_JSON_PATH, OXLINTRC_DRIFT, OXLINTRC_MISSING, planOxlintrcFix } from '../tooling/main.mjs'
|
|
35
|
+
|
|
36
|
+
const ESLINT_CONFIG_REASONS = new Set([ESLINT_CONFIG_MISSING, ESLINT_CONFIG_IGNORES, ESLINT_CONFIG_VUE_WORKSPACE])
|
|
37
|
+
const OXLINTRC_REASONS = new Set([OXLINTRC_MISSING, OXLINTRC_DRIFT])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Читає `.oxlintrc.json`, якщо є й валідний JSON; інакше `null` (трактується
|
|
41
|
+
* як відсутній файл — `planOxlintrcFix` будує з канону).
|
|
42
|
+
* @param {string} oxPath абсолютний шлях до `.oxlintrc.json`
|
|
43
|
+
* @returns {Promise<unknown>} розпарсений вміст або `null`
|
|
44
|
+
*/
|
|
45
|
+
async function readOxlintrcOrNull(oxPath) {
|
|
46
|
+
if (!existsSync(oxPath)) return null
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(oxPath, 'utf8'))
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
55
|
+
export const patterns = [
|
|
56
|
+
{
|
|
57
|
+
id: 'js-check-eslint-config',
|
|
58
|
+
test: violations => violations.some(v => ESLINT_CONFIG_REASONS.has(v.reason)),
|
|
59
|
+
apply: async (violations, ctx) => {
|
|
60
|
+
const plan = await planEslintConfigFix(ctx.cwd)
|
|
61
|
+
if (plan === null) return { touchedFiles: [] }
|
|
62
|
+
ctx.recordWrite?.(plan.path)
|
|
63
|
+
await writeFile(plan.path, plan.content, 'utf8')
|
|
64
|
+
return { touchedFiles: [plan.path], message: plan.message }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'js-check-oxlintrc',
|
|
69
|
+
test: violations => violations.some(v => OXLINTRC_REASONS.has(v.reason)),
|
|
70
|
+
apply: async (violations, ctx) => {
|
|
71
|
+
const oxPath = join(ctx.cwd, '.oxlintrc.json')
|
|
72
|
+
const [actual, canonicalRaw] = await Promise.all([
|
|
73
|
+
readOxlintrcOrNull(oxPath),
|
|
74
|
+
readFile(OXLINT_CANONICAL_JSON_PATH, 'utf8')
|
|
75
|
+
])
|
|
76
|
+
const merged = planOxlintrcFix(actual, JSON.parse(canonicalRaw))
|
|
77
|
+
ctx.recordWrite?.(oxPath)
|
|
78
|
+
await writeFile(oxPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
|
|
79
|
+
return { touchedFiles: [oxPath], message: '.oxlintrc.json: T0 merge до канону oxlint (@7n/rules)' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/** @see ./docs/tooling.md */
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { copyFile, readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
KNIP_CANONICAL_JSON_PATH,
|
|
10
|
+
OXLINT_CANONICAL_JSON_PATH,
|
|
11
|
+
OXLINTRC_DRIFT,
|
|
12
|
+
OXLINTRC_MISSING,
|
|
13
|
+
verifyOxlintRcAgainstCanonical
|
|
14
|
+
} from '../tooling/main.mjs'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
AUTO_IMPORTS_IGNORE,
|
|
18
|
+
ESLINT_CONFIG_IGNORES,
|
|
19
|
+
ESLINT_CONFIG_MISSING,
|
|
20
|
+
ESLINT_CONFIG_VUE_WORKSPACE,
|
|
21
|
+
detectWorkspaceTypes,
|
|
22
|
+
parseVueList
|
|
23
|
+
} from './eslint-config.mjs'
|
|
24
|
+
|
|
25
|
+
const NON_DIGITS_RE = /\D+/u
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Перевіряє ESLint flat config файл.
|
|
29
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
30
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
31
|
+
* @param {string} cwd корінь репозиторію
|
|
32
|
+
*/
|
|
33
|
+
async function checkEslintConfig(passFn, failFn, cwd) {
|
|
34
|
+
let eslintPath
|
|
35
|
+
if (existsSync(join(cwd, 'eslint.config.js'))) {
|
|
36
|
+
eslintPath = 'eslint.config.js'
|
|
37
|
+
passFn('eslint.config.js існує')
|
|
38
|
+
} else if (existsSync(join(cwd, 'eslint.config.mjs'))) {
|
|
39
|
+
eslintPath = 'eslint.config.mjs'
|
|
40
|
+
passFn('eslint.config.mjs існує')
|
|
41
|
+
} else {
|
|
42
|
+
failFn('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js.mdc)', ESLINT_CONFIG_MISSING)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const eslintRaw = await readFile(join(cwd, eslintPath), 'utf8')
|
|
46
|
+
const checks = [
|
|
47
|
+
{
|
|
48
|
+
needle: 'getConfig',
|
|
49
|
+
ok: `${eslintPath}: містить getConfig`,
|
|
50
|
+
err: `${eslintPath}: потрібен виклик getConfig (js.mdc)`
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
needle: '@nitra/eslint-config',
|
|
54
|
+
ok: `${eslintPath}: імпорт @nitra/eslint-config`,
|
|
55
|
+
err: `${eslintPath}: імпортуй getConfig з @nitra/eslint-config`
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
needle: AUTO_IMPORTS_IGNORE,
|
|
59
|
+
ok: `${eslintPath}: ignores містить ${AUTO_IMPORTS_IGNORE}`,
|
|
60
|
+
err: `${eslintPath}: додай у ignores запис ${AUTO_IMPORTS_IGNORE} (js.mdc)`,
|
|
61
|
+
reason: ESLINT_CONFIG_IGNORES
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
for (const { needle, ok, err, reason } of checks) {
|
|
65
|
+
if (eslintRaw.includes(needle)) {
|
|
66
|
+
passFn(ok)
|
|
67
|
+
} else {
|
|
68
|
+
failFn(err, reason)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await checkEslintWorkspaceTypes(eslintRaw, eslintPath, passFn, failFn, cwd)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Кожен vue-воркспейс (детекція: `workspaces` root package.json + vue-залежність
|
|
76
|
+
* або `.vue` файли) має бути у `vue: [...]` аргументах getConfig — інакше eslint
|
|
77
|
+
* не підключає vue-parser і всі `.vue` файли воркспейсу не парсяться. Ловить
|
|
78
|
+
* хибний scaffold типу `getConfig({ node: ['npm'] })` у vue-монорепо.
|
|
79
|
+
* @param {string} eslintRaw вміст eslint.config
|
|
80
|
+
* @param {string} eslintPath ім'я файлу конфігу для повідомлень
|
|
81
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
82
|
+
* @param {(msg: string, reason?: string) => void} failFn callback при помилці
|
|
83
|
+
* @param {string} cwd корінь репозиторію
|
|
84
|
+
*/
|
|
85
|
+
async function checkEslintWorkspaceTypes(eslintRaw, eslintPath, passFn, failFn, cwd) {
|
|
86
|
+
const types = await detectWorkspaceTypes(cwd)
|
|
87
|
+
if (types.vue.length === 0) return
|
|
88
|
+
const vueEntries = parseVueList(eslintRaw)
|
|
89
|
+
for (const ws of types.vue) {
|
|
90
|
+
if (vueEntries.includes(ws)) {
|
|
91
|
+
passFn(`${eslintPath}: vue-воркспейс '${ws}' у vue: [...]`)
|
|
92
|
+
} else {
|
|
93
|
+
failFn(
|
|
94
|
+
`${eslintPath}: воркспейс '${ws}' містить Vue-код, але відсутній у vue: [...] getConfig — ` +
|
|
95
|
+
'.vue файли не парсяться (js.mdc)',
|
|
96
|
+
ESLINT_CONFIG_VUE_WORKSPACE
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Перевірки `prettier` / `@nitra/prettier-config` у залежностях (text.mdc) і
|
|
103
|
+
// `@nitra/eslint-config ≥ 3.10.0` тепер у Rego: відповідно
|
|
104
|
+
// `npm/policy/text/package_json/` і `npm/policy/js_lint/package_json/`. Тут
|
|
105
|
+
// лишилася лише workspace-ітерація для `type: "module"` і engines, бо js_lint
|
|
106
|
+
// Rego запускається лише на кореневому `package.json`.
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Перевіряє, що package.json має `"type": "module"`.
|
|
110
|
+
* @param {string} label шлях або назва пакета для повідомлень
|
|
111
|
+
* @param {{ type?: string }} pkg parsed package.json
|
|
112
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
113
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
114
|
+
*/
|
|
115
|
+
function checkPackageJsonTypeModule(label, pkg, passFn, failFn) {
|
|
116
|
+
if (pkg.type === 'module') {
|
|
117
|
+
passFn(`${label}: "type": "module"`)
|
|
118
|
+
} else {
|
|
119
|
+
failFn(`${label}: має містити "type": "module" (js.mdc)`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `"type": "module"`, `engines.node >= 24` і `engines.bun >= 1.3` у кожному workspace `package.json`.
|
|
125
|
+
* @param {unknown[]} workspaces поле workspaces з package.json
|
|
126
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
127
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
128
|
+
* @param {string} cwd корінь репозиторію
|
|
129
|
+
*/
|
|
130
|
+
async function checkWorkspacePackages(workspaces, passFn, failFn, cwd) {
|
|
131
|
+
for (const ws of workspaces) {
|
|
132
|
+
const wsPkgRel = `${ws}/package.json`
|
|
133
|
+
const wsPkgAbs = join(cwd, wsPkgRel)
|
|
134
|
+
if (existsSync(wsPkgAbs)) {
|
|
135
|
+
const wsPkg = JSON.parse(await readFile(wsPkgAbs, 'utf8'))
|
|
136
|
+
checkPackageJsonTypeModule(wsPkgRel, wsPkg, passFn, failFn)
|
|
137
|
+
checkEnginesNode(wsPkgRel, wsPkg, passFn, failFn)
|
|
138
|
+
checkEnginesBun(wsPkgRel, wsPkg, passFn, failFn)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* engines.node >= 24.
|
|
145
|
+
* @param {string} label шлях або назва пакета для повідомлень
|
|
146
|
+
* @param {{ engines?: { node?: string } }} pkg розпарсений package.json
|
|
147
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
148
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
149
|
+
*/
|
|
150
|
+
function checkEnginesNode(label, pkg, passFn, failFn) {
|
|
151
|
+
const nodeEngine = pkg.engines?.node
|
|
152
|
+
if (nodeEngine) {
|
|
153
|
+
const firstNumeric = String(nodeEngine).split(NON_DIGITS_RE).find(Boolean)
|
|
154
|
+
if (firstNumeric && Number(firstNumeric) >= 24) {
|
|
155
|
+
passFn(`${label}: engines.node "${nodeEngine}"`)
|
|
156
|
+
} else {
|
|
157
|
+
failFn(`${label}: engines.node "${nodeEngine}" — має бути >=24`)
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
failFn(`${label} не містить engines.node — додай: "engines": { "node": ">=24" }`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* engines.bun >= 1.3.
|
|
166
|
+
* @param {string} label шлях або назва пакета для повідомлень
|
|
167
|
+
* @param {{ engines?: { bun?: string } }} pkg розпарсений package.json
|
|
168
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
169
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
170
|
+
*/
|
|
171
|
+
function checkEnginesBun(label, pkg, passFn, failFn) {
|
|
172
|
+
const bunEngine = pkg.engines?.bun
|
|
173
|
+
if (bunEngine) {
|
|
174
|
+
const [major, minor] = String(bunEngine).split(NON_DIGITS_RE).filter(Boolean).map(Number)
|
|
175
|
+
if (Number.isFinite(major) && Number.isFinite(minor) && (major > 1 || (major === 1 && minor >= 3))) {
|
|
176
|
+
passFn(`${label}: engines.bun "${bunEngine}"`)
|
|
177
|
+
} else {
|
|
178
|
+
failFn(`${label}: engines.bun "${bunEngine}" — має бути >=1.3`)
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
failFn(`${label} не містить engines.bun — додай: "engines": { "bun": ">=1.3" }`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Workspace-ітерація: для кожного workspace `package.json` перевіряємо
|
|
187
|
+
* `type: "module"` і `engines.{node,bun}`. Кореневий `package.json` ці поля
|
|
188
|
+
* валідує `npm/policy/js_lint/package_json/`; lint-js скрипт і `@nitra/eslint-config`
|
|
189
|
+
* — теж у Rego.
|
|
190
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
191
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
192
|
+
* @param {string} cwd корінь репозиторію
|
|
193
|
+
*/
|
|
194
|
+
async function checkPackageJsonJsLint(passFn, failFn, cwd) {
|
|
195
|
+
const pkgPath = join(cwd, 'package.json')
|
|
196
|
+
if (!existsSync(pkgPath)) return
|
|
197
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
198
|
+
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : []
|
|
199
|
+
await checkWorkspacePackages(workspaces, passFn, failFn, cwd)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Перевіряє .oxlintrc.json.
|
|
204
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
205
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
206
|
+
* @param {string} cwd корінь репозиторію
|
|
207
|
+
*/
|
|
208
|
+
async function checkOxlintRc(passFn, failFn, cwd) {
|
|
209
|
+
const oxPath = join(cwd, '.oxlintrc.json')
|
|
210
|
+
if (!existsSync(oxPath)) {
|
|
211
|
+
failFn('.oxlintrc.json не існує — додай конфіг oxlint (js.mdc)', OXLINTRC_MISSING)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
let oxCfg
|
|
215
|
+
try {
|
|
216
|
+
oxCfg = JSON.parse(await readFile(oxPath, 'utf8'))
|
|
217
|
+
} catch {
|
|
218
|
+
failFn('.oxlintrc.json не є валідним JSON')
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
passFn('.oxlintrc.json існує')
|
|
222
|
+
let canonical
|
|
223
|
+
try {
|
|
224
|
+
canonical = JSON.parse(await readFile(OXLINT_CANONICAL_JSON_PATH, 'utf8'))
|
|
225
|
+
} catch {
|
|
226
|
+
failFn('внутрішня помилка: не вдалося прочитати канон oxlint з пакета @7n/rules')
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
const oxV = verifyOxlintRcAgainstCanonical(oxCfg, canonical)
|
|
230
|
+
if (oxV.ok) {
|
|
231
|
+
passFn('.oxlintrc.json збігається з каноном oxlint (@7n/rules)')
|
|
232
|
+
} else {
|
|
233
|
+
for (const msg of oxV.failures) {
|
|
234
|
+
failFn(msg, OXLINTRC_DRIFT)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Cross-file перевірка: `lint.yml` (якщо існує) не дублює лінт JS-кроки з `lint-js.yml`.
|
|
241
|
+
* Existence і структуру `lint-js.yml` валідує mixin-концерн `js/lint_js_yml`
|
|
242
|
+
* плагіна `@7n/rules-ci-github` (провайдер-специфіка — поза ядром).
|
|
243
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
244
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
245
|
+
* @param {string} cwd корінь репозиторію
|
|
246
|
+
*/
|
|
247
|
+
async function checkLintJsWorkflows(passFn, failFn, cwd) {
|
|
248
|
+
// Existence/структуру lint-js.yml вимагає провайдер-плагін @7n/rules-ci-github
|
|
249
|
+
// (mixin js/lint_js_yml, policy required) — ядро провайдер-агностичне і тут
|
|
250
|
+
// перевіряє лише крос-файловий дубляж, коли GitHub-workflow уже існують.
|
|
251
|
+
const lintYmlPath = join(cwd, '.github/workflows/lint.yml')
|
|
252
|
+
if (existsSync(lintYmlPath)) {
|
|
253
|
+
const lintYml = await readFile(lintYmlPath, 'utf8')
|
|
254
|
+
if (lintYml.includes('bunx oxlint') && lintYml.includes('bunx eslint') && lintYml.includes('jscpd')) {
|
|
255
|
+
failFn('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js.mdc)')
|
|
256
|
+
} else {
|
|
257
|
+
passFn('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Перевіряє наявність `knip.json` у корені проєкту. Якщо файл відсутній —
|
|
264
|
+
* копіює канонічний `knip-canonical.json` з пакета `@7n/rules` як стартовий
|
|
265
|
+
* baseline; зміст подальших модифікацій локально не валідується (`entry` /
|
|
266
|
+
* `project` / `ignore` / `ignoreDependencies` / `ignoreBinaries` дозволені
|
|
267
|
+
* будь-які; це side effect — описано у js.mdc).
|
|
268
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
269
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
270
|
+
* @param {string} cwd корінь репозиторію
|
|
271
|
+
*/
|
|
272
|
+
async function checkKnipConfig(passFn, failFn, cwd) {
|
|
273
|
+
const knipPath = join(cwd, 'knip.json')
|
|
274
|
+
if (existsSync(knipPath)) {
|
|
275
|
+
passFn('knip.json існує')
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
if (!existsSync(KNIP_CANONICAL_JSON_PATH)) {
|
|
279
|
+
failFn(
|
|
280
|
+
`knip.json відсутній, і канонічний шаблон у пакеті не знайдено (${KNIP_CANONICAL_JSON_PATH}) — ` +
|
|
281
|
+
'перевстанови @7n/rules'
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
await copyFile(KNIP_CANONICAL_JSON_PATH, knipPath)
|
|
286
|
+
passFn('knip.json створено з канонічного npm/rules/js/tooling/data/tooling/knip-canonical.json (js.mdc)')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Перевіряє відповідність проєкту правилам js.mdc
|
|
291
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
|
|
292
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} перелік порушень
|
|
293
|
+
*/
|
|
294
|
+
export async function lint(ctx) {
|
|
295
|
+
const cwd = ctx.cwd
|
|
296
|
+
const reporter = createViolationReporter(ctx)
|
|
297
|
+
const { pass, fail } = reporter
|
|
298
|
+
|
|
299
|
+
await checkEslintConfig(pass, fail, cwd)
|
|
300
|
+
await checkPackageJsonJsLint(pass, fail, cwd)
|
|
301
|
+
await checkOxlintRc(pass, fail, cwd)
|
|
302
|
+
await checkLintJsWorkflows(pass, fail, cwd)
|
|
303
|
+
await checkKnipConfig(pass, fail, cwd)
|
|
304
|
+
|
|
305
|
+
for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
|
|
306
|
+
if (existsSync(join(cwd, dup))) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return reporter.result()
|
|
310
|
+
}
|