@nitra/cursor 1.39.1 → 1.40.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/bin/n-cursor.js +8 -5
- package/package.json +1 -1
- package/rules/ga/js/lint.mjs +12 -0
- package/rules/ga/meta.json +1 -1
- package/rules/js-lint/js/lint.mjs +52 -0
- package/rules/js-lint/js-lint.mdc +1 -1
- package/rules/js-lint/meta.json +1 -1
- package/rules/js-lint-ci/fix.mjs +19 -0
- package/rules/js-lint-ci/js/lint.mjs +20 -0
- package/rules/js-lint-ci/js-lint-ci.mdc +12 -0
- package/rules/js-lint-ci/meta.json +1 -0
- package/rules/npm-module/js/rule_meta.mjs +10 -1
- package/rules/rego/js/lint.mjs +12 -0
- package/rules/rego/meta.json +1 -1
- package/rules/security/js/lint.mjs +18 -0
- package/rules/security/meta.json +1 -1
- package/rules/style-lint/js/lint.mjs +34 -0
- package/rules/style-lint/meta.json +1 -1
- package/rules/text/js/lint.mjs +12 -0
- package/rules/text/meta.json +1 -1
- package/schemas/rule-meta.json +1 -0
- package/scripts/lib/changed-files.mjs +30 -0
- package/scripts/lib/rule-meta.mjs +12 -0
- package/scripts/lint-cli.mjs +83 -0
- package/rules/js-lint/js/data/tooling/oxlint-canonical-skeleton.json +0 -27
- package/rules/js-lint/js/data/tooling/oxlint-rules.tsv +0 -359
- package/rules/js-lint/lib/rebuild-oxlint-canonical.mjs +0 -29
- package/scripts/lib/run-lint-cli.mjs +0 -116
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.40.1] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- js-lint: oxlint-canonical.json тепер source-of-truth — прибрано генерацію (rebuild-oxlint-canonical.mjs, oxlint-canonical-skeleton.json, oxlint-rules.tsv)
|
|
8
|
+
|
|
9
|
+
## [1.40.0] - 2026-05-31
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- lint: розділення на `n-cursor lint` (quick, по змінених файлах) і `n-cursor lint-ci` (повний, по всьому репо) — data-driven за полем `meta.json.lint` (quick/ci); виконавець кроку — `js/lint.mjs` правила; jscpd+knip винесено в нове правило `js-lint-ci` (фаза ci)
|
|
14
|
+
|
|
3
15
|
## [1.39.1] - 2026-05-31
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -105,7 +105,7 @@ import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
|
|
|
105
105
|
import { runSkillsCli } from '../scripts/skills-cli.mjs'
|
|
106
106
|
import { runWorktreeCli } from '../scripts/worktree-cli.mjs'
|
|
107
107
|
import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
|
|
108
|
-
import {
|
|
108
|
+
import { runLint } from '../scripts/lint-cli.mjs'
|
|
109
109
|
import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
|
|
110
110
|
|
|
111
111
|
const PACKAGE_NAME = '@nitra/cursor'
|
|
@@ -1464,9 +1464,12 @@ try {
|
|
|
1464
1464
|
break
|
|
1465
1465
|
}
|
|
1466
1466
|
case 'lint': {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1467
|
+
process.exitCode = await runLint({ ci: false })
|
|
1468
|
+
|
|
1469
|
+
break
|
|
1470
|
+
}
|
|
1471
|
+
case 'lint-ci': {
|
|
1472
|
+
process.exitCode = await runLint({ ci: true })
|
|
1470
1473
|
|
|
1471
1474
|
break
|
|
1472
1475
|
}
|
|
@@ -1540,7 +1543,7 @@ try {
|
|
|
1540
1543
|
default: {
|
|
1541
1544
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1542
1545
|
console.error(
|
|
1543
|
-
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree`
|
|
1546
|
+
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree, lint-ci`
|
|
1544
1547
|
)
|
|
1545
1548
|
process.exitCode = 1
|
|
1546
1549
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ci-крок ga: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
|
|
3
|
+
*/
|
|
4
|
+
import { runLintGaCli } from '../lint/lint.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
|
|
8
|
+
* @returns {Promise<number>} exit code
|
|
9
|
+
*/
|
|
10
|
+
export async function lint(_files) {
|
|
11
|
+
return runLintGaCli()
|
|
12
|
+
}
|
package/rules/ga/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": ".github/workflows/**" } }
|
|
1
|
+
{ "auto": { "glob": ".github/workflows/**" }, "lint": "ci" }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick-крок lint правила js-lint: oxlint + eslint (з автофіксом).
|
|
3
|
+
*
|
|
4
|
+
* Викликається lint-оркестратором (`n-cursor lint` / `lint-ci`):
|
|
5
|
+
* - `files` = масив змінених файлів (quick) → лінтимо лише js-подібні з них;
|
|
6
|
+
* - `files` = undefined (ci) → лінтимо весь проєкт.
|
|
7
|
+
* Крос-файлові jscpd/knip — окреме правило js-lint-ci (фаза ci).
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
|
|
11
|
+
const JS_EXT_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Лишає лише js-подібні файли зі списку.
|
|
15
|
+
* @param {string[]} files список шляхів
|
|
16
|
+
* @returns {string[]} підмножина js-подібних
|
|
17
|
+
*/
|
|
18
|
+
export function filterJsFiles(files) {
|
|
19
|
+
return files.filter(f => JS_EXT_RE.test(f))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string[]} args аргументи інструмента (бінар через bunx)
|
|
24
|
+
* @param {string} cwd корінь
|
|
25
|
+
* @returns {number} exit code
|
|
26
|
+
*/
|
|
27
|
+
function run(args, cwd) {
|
|
28
|
+
const r = spawnSync('bunx', args, { cwd, stdio: 'inherit' })
|
|
29
|
+
return typeof r.status === 'number' ? r.status : 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Запускає oxlint+eslint з автофіксом.
|
|
34
|
+
* @param {string[] | undefined} files quick: лише ці файли; undefined: весь проєкт
|
|
35
|
+
* @param {string} [cwd] корінь репо
|
|
36
|
+
* @returns {Promise<number>} 0 — OK, ≠0 — порушення
|
|
37
|
+
*/
|
|
38
|
+
export function lint(files, cwd = process.cwd()) {
|
|
39
|
+
let oxArgs = ['oxlint', '--fix']
|
|
40
|
+
let esArgs = ['eslint', '--fix']
|
|
41
|
+
if (files === undefined) {
|
|
42
|
+
esArgs.push('.')
|
|
43
|
+
} else {
|
|
44
|
+
const js = filterJsFiles(files)
|
|
45
|
+
if (js.length === 0) return Promise.resolve(0)
|
|
46
|
+
oxArgs = ['oxlint', '--fix', ...js]
|
|
47
|
+
esArgs = ['eslint', '--fix', ...js]
|
|
48
|
+
}
|
|
49
|
+
const ox = run(oxArgs, cwd)
|
|
50
|
+
if (ox !== 0) return Promise.resolve(ox)
|
|
51
|
+
return Promise.resolve(run(esArgs, cwd))
|
|
52
|
+
}
|
|
@@ -25,7 +25,7 @@ version: '1.26'
|
|
|
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/rules/js-lint/js/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-и дозволені.
|
|
28
|
+
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js-lint/js/data/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-и дозволені. Канон **`oxlint-canonical.json`** — source-of-truth, редагується напряму; у споживачі оновлюється копіюванням файлу з репозиторію пакета. Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
|
|
29
29
|
|
|
30
30
|
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
31
31
|
|
package/rules/js-lint/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } }
|
|
1
|
+
{ "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "quick" }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
|
|
2
|
+
import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Запускає правило: applies → JS-concerns → policy → mdc-refs (через runStandardRule).
|
|
6
|
+
* Library mode: викликається CLI orchestration через `import + run(ctx)`.
|
|
7
|
+
* @param {import('../../scripts/lib/run-standard-rule.mjs').RuleContext} [ctx] контекст прогону (walkCache тощо)
|
|
8
|
+
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
9
|
+
*/
|
|
10
|
+
export function run(ctx) {
|
|
11
|
+
return runStandardRule(import.meta.dirname, ctx)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isRunAsCli(import.meta.url)) {
|
|
15
|
+
// Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
|
|
16
|
+
// (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
|
|
17
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
|
|
18
|
+
process.exit(await runRuleCli(import.meta.dirname))
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ci-крок: jscpd (детектор клонів) + knip (невикористані експорти).
|
|
3
|
+
*
|
|
4
|
+
* Крос-файлові аналізатори — працюють лише по всьому репо, тож `files` ігнорується
|
|
5
|
+
* (викликається лише у `lint-ci` з undefined). Per-file режиму ці інструменти не мають.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string[] | undefined} _files ігнорується (крос-файловий аналіз)
|
|
11
|
+
* @param {string} [cwd] корінь репо
|
|
12
|
+
* @returns {Promise<number>} 0 — OK, ≠0 — порушення
|
|
13
|
+
*/
|
|
14
|
+
export function lint(_files, cwd = process.cwd()) {
|
|
15
|
+
const jscpd = spawnSync('bunx', ['jscpd', '.'], { cwd, stdio: 'inherit' })
|
|
16
|
+
const jc = typeof jscpd.status === 'number' ? jscpd.status : 1
|
|
17
|
+
if (jc !== 0) return Promise.resolve(jc)
|
|
18
|
+
const knip = spawnSync('bunx', ['knip', '--no-config-hints'], { cwd, stdio: 'inherit' })
|
|
19
|
+
return Promise.resolve(typeof knip.status === 'number' ? knip.status : 1)
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Крос-файловий ci-етап js-lint — jscpd (детектор клонів) і knip (невикористані експорти). Лише у lint-ci, по всьому репо.
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# js-lint-ci — крос-файловий ci-етап
|
|
8
|
+
|
|
9
|
+
`jscpd` і `knip` аналізують увесь граф проєкту, тож мають сенс лише у повному прогоні
|
|
10
|
+
`npx @nitra/cursor lint-ci` (не у швидкому `lint` по змінених файлах). Per-file режиму нема.
|
|
11
|
+
|
|
12
|
+
Швидкий етап js-lint (oxlint/eslint) — у правилі `js-lint` (`lint: quick`).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "lint": "ci" }
|
|
@@ -12,7 +12,7 @@ import { existsSync, readdirSync } from 'node:fs'
|
|
|
12
12
|
import { join } from 'node:path'
|
|
13
13
|
|
|
14
14
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
15
|
-
import { parseRuleAutoSpec, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
|
|
15
|
+
import { parseRuleAutoSpec, parseRuleLintPhase, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
|
|
16
16
|
import { RULE_PREDICATES } from '../../../scripts/lib/rule-predicates.mjs'
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -54,6 +54,15 @@ export function check(cwd = process.cwd()) {
|
|
|
54
54
|
ruleOk = false
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
if (raw.lint !== undefined) {
|
|
58
|
+
if (parseRuleLintPhase(raw.lint) === null) {
|
|
59
|
+
reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`)
|
|
60
|
+
ruleOk = false
|
|
61
|
+
} else if (!existsSync(join(ruleDir, 'js', 'lint.mjs'))) {
|
|
62
|
+
reporter.fail(`rules/${id}: lint:"${raw.lint}" але немає js/lint.mjs`)
|
|
63
|
+
ruleOk = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
57
66
|
if (ruleOk) {
|
|
58
67
|
reporter.pass(`rules/${id}: meta.json валідний`)
|
|
59
68
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ci-крок rego: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
|
|
3
|
+
*/
|
|
4
|
+
import { runLintRego } from '../lint/lint.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
|
|
8
|
+
* @returns {Promise<number>} exit code
|
|
9
|
+
*/
|
|
10
|
+
export async function lint(_files) {
|
|
11
|
+
return runLintRego()
|
|
12
|
+
}
|
package/rules/rego/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": "**/*.rego" } }
|
|
1
|
+
{ "auto": { "glob": "**/*.rego" }, "lint": "ci" }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ci-крок security: trufflehog filesystem скан усього репо (per-file немає).
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string[] | undefined} _files ігнорується
|
|
8
|
+
* @param {string} [cwd] корінь
|
|
9
|
+
* @returns {Promise<number>} exit code
|
|
10
|
+
*/
|
|
11
|
+
export function lint(_files, cwd = process.cwd()) {
|
|
12
|
+
const r = spawnSync(
|
|
13
|
+
'trufflehog',
|
|
14
|
+
['filesystem', '.', '--no-update', '--exclude-paths', '.trufflehog-exclude', '--results=verified,unknown', '--fail'],
|
|
15
|
+
{ cwd, stdio: 'inherit' }
|
|
16
|
+
)
|
|
17
|
+
return Promise.resolve(typeof r.status === 'number' ? r.status : 1)
|
|
18
|
+
}
|
package/rules/security/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": "завжди" }
|
|
1
|
+
{ "auto": "завжди", "lint": "ci" }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick-крок lint правила style-lint: stylelint --fix по css/scss/vue.
|
|
3
|
+
*
|
|
4
|
+
* `files` (quick) → лише style-файли з них; undefined (ci) → весь glob `**\/*.{css,scss,vue}`.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const STYLE_EXT_RE = /\.(?:css|scss|vue)$/u
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string[]} files список шляхів
|
|
12
|
+
* @returns {string[]} лише css/scss/vue
|
|
13
|
+
*/
|
|
14
|
+
export function filterStyleFiles(files) {
|
|
15
|
+
return files.filter(f => STYLE_EXT_RE.test(f))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string[] | undefined} files quick: ці файли; undefined: весь проєкт
|
|
20
|
+
* @param {string} [cwd] корінь
|
|
21
|
+
* @returns {Promise<number>} exit code
|
|
22
|
+
*/
|
|
23
|
+
export function lint(files, cwd = process.cwd()) {
|
|
24
|
+
const args = ['stylelint', '--fix']
|
|
25
|
+
if (files === undefined) {
|
|
26
|
+
args.push('**/*.{css,scss,vue}')
|
|
27
|
+
} else {
|
|
28
|
+
const style = filterStyleFiles(files)
|
|
29
|
+
if (style.length === 0) return Promise.resolve(0)
|
|
30
|
+
args.push(...style)
|
|
31
|
+
}
|
|
32
|
+
const r = spawnSync('npx', args, { cwd, stdio: 'inherit' })
|
|
33
|
+
return Promise.resolve(typeof r.status === 'number' ? r.status : 1)
|
|
34
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": ["**/*.css", "**/*.vue"] } }
|
|
1
|
+
{ "auto": { "glob": ["**/*.css", "**/*.vue"] }, "lint": "quick" }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ci-крок text: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується).
|
|
3
|
+
*/
|
|
4
|
+
import { runLintTextCli } from '../lint/lint.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
|
|
8
|
+
* @returns {Promise<number>} exit code
|
|
9
|
+
*/
|
|
10
|
+
export async function lint(_files) {
|
|
11
|
+
return runLintTextCli()
|
|
12
|
+
}
|
package/rules/text/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": "завжди" }
|
|
1
|
+
{ "auto": "завжди", "lint": "ci" }
|
package/schemas/rule-meta.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
+
"lint": { "type": "string", "enum": ["quick", "ci"], "description": "Фаза lint-кроку: quick (по змінених, у lint і lint-ci) або ci (лише lint-ci)." },
|
|
9
10
|
"auto": {
|
|
10
11
|
"description": "Умова автоактивації правила: \"завжди\", масив id правил-залежностей, glob, або іменований предикат.",
|
|
11
12
|
"oneOf": [
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Збір змінених файлів для quick-режиму lint-оркестратора.
|
|
3
|
+
*
|
|
4
|
+
* Quick лінтить лише те, що змінено в робочому дереві: tracked-modified + staged
|
|
5
|
+
* (`git diff HEAD`) і нові untracked (`git ls-files --others --exclude-standard`).
|
|
6
|
+
* Видалені файли не повертаються. Поза git-репо або при помилці git — порожній список.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string[]} args аргументи git
|
|
12
|
+
* @param {string} cwd корінь
|
|
13
|
+
* @returns {string[]} непорожні рядки stdout або [] при помилці
|
|
14
|
+
*/
|
|
15
|
+
function gitLines(args, cwd) {
|
|
16
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf8' })
|
|
17
|
+
if (r.status !== 0 || r.error) return []
|
|
18
|
+
return r.stdout.split('\n').map(s => s.trim()).filter(Boolean)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Relative-posix список змінених + untracked файлів робочого дерева.
|
|
23
|
+
* @param {string} [cwd] корінь репо
|
|
24
|
+
* @returns {string[]} унікальні шляхи (без видалених)
|
|
25
|
+
*/
|
|
26
|
+
export function collectChangedFiles(cwd = process.cwd()) {
|
|
27
|
+
const modified = gitLines(['diff', 'HEAD', '--name-only', '--diff-filter=ACMR'], cwd)
|
|
28
|
+
const untracked = gitLines(['ls-files', '--others', '--exclude-standard'], cwd)
|
|
29
|
+
return [...new Set([...modified, ...untracked])]
|
|
30
|
+
}
|
|
@@ -48,6 +48,18 @@ export function parseRuleAutoSpec(value) {
|
|
|
48
48
|
return null
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Допустимі фази lint. */
|
|
52
|
+
const LINT_PHASES = new Set(['quick', 'ci'])
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Нормалізує значення `meta.json.lint` у фазу lint.
|
|
56
|
+
* @param {unknown} value значення поля `lint`
|
|
57
|
+
* @returns {'quick' | 'ci' | null} фаза або `null` (відсутнє/невалідне = не lint-крок)
|
|
58
|
+
*/
|
|
59
|
+
export function parseRuleLintPhase(value) {
|
|
60
|
+
return typeof value === 'string' && LINT_PHASES.has(value) ? /** @type {'quick'|'ci'} */ (value) : null
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* Читає й парсить `meta.json` одного правила.
|
|
53
65
|
* @param {string} ruleDir абсолютний шлях до каталогу правила
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Оркестратор `n-cursor lint` (quick) / `n-cursor lint-ci` (full).
|
|
3
|
+
*
|
|
4
|
+
* Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`quick`|`ci`),
|
|
5
|
+
* послідовно (заборона паралельного eslint) викликає `rules/<id>/js/lint.mjs`:
|
|
6
|
+
* - quick: `lint(changedFiles)` — лише змінені файли (git diff HEAD + untracked);
|
|
7
|
+
* - ci: `lint(undefined)` — весь проєкт.
|
|
8
|
+
* Порядок правил — алфавітний; ci-набір = quick ∪ ci. Fail-fast: перший ненульовий код спиняє.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { cwd as processCwd } from 'node:process'
|
|
14
|
+
|
|
15
|
+
import { parseRuleLintPhase, readRuleMetaRaw } from './lib/rule-meta.mjs'
|
|
16
|
+
import { collectChangedFiles } from './lib/changed-files.mjs'
|
|
17
|
+
|
|
18
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
19
|
+
const RULES_DIR = join(PACKAGE_ROOT, 'rules')
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Вибирає id правил для фази, алфавітно.
|
|
23
|
+
* @param {Record<string, {lint?: unknown}>} metaById мапа id → meta-обʼєкт
|
|
24
|
+
* @param {'quick'|'ci'} phase цільова фаза (quick → лише quick; ci → quick+ci)
|
|
25
|
+
* @returns {string[]} відсортовані id
|
|
26
|
+
*/
|
|
27
|
+
export function selectLintRules(metaById, phase) {
|
|
28
|
+
const out = []
|
|
29
|
+
for (const [id, raw] of Object.entries(metaById)) {
|
|
30
|
+
const p = parseRuleLintPhase(raw?.lint)
|
|
31
|
+
if (p === 'quick' || (phase === 'ci' && p === 'ci')) out.push(id)
|
|
32
|
+
}
|
|
33
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Зчитує meta всіх правил пакета.
|
|
38
|
+
* @param {string} rulesDir каталог rules
|
|
39
|
+
* @returns {Record<string, Record<string, unknown>>} id → meta
|
|
40
|
+
*/
|
|
41
|
+
function readAllMeta(rulesDir) {
|
|
42
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
43
|
+
const out = {}
|
|
44
|
+
if (!existsSync(rulesDir)) return out
|
|
45
|
+
for (const e of readdirSync(rulesDir, { withFileTypes: true })) {
|
|
46
|
+
if (!e.isDirectory() || e.name.startsWith('.')) continue
|
|
47
|
+
const raw = readRuleMetaRaw(join(rulesDir, e.name))
|
|
48
|
+
if (raw) out[e.name] = raw
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Запускає lint-оркестрацію.
|
|
55
|
+
* @param {{ ci?: boolean, cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
|
|
56
|
+
* @returns {Promise<number>} exit code
|
|
57
|
+
*/
|
|
58
|
+
export async function runLint(opts = {}) {
|
|
59
|
+
const ci = opts.ci === true
|
|
60
|
+
const cwd = opts.cwd ?? processCwd()
|
|
61
|
+
const rulesDir = opts.rulesDir ?? RULES_DIR
|
|
62
|
+
const log = opts.log ?? (s => process.stdout.write(s))
|
|
63
|
+
|
|
64
|
+
const changed = ci ? undefined : collectChangedFiles(cwd)
|
|
65
|
+
if (!ci && changed.length === 0) {
|
|
66
|
+
log('\nℹ️ lint: немає змінених файлів — нічого перевіряти.\n')
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ids = selectLintRules(readAllMeta(rulesDir), ci ? 'ci' : 'quick')
|
|
71
|
+
for (const id of ids) {
|
|
72
|
+
const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
|
|
73
|
+
if (!existsSync(lintPath)) {
|
|
74
|
+
log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
// eslint-disable-next-line no-unsanitized/method -- шлях з discovered rule dir
|
|
78
|
+
const mod = await import(lintPath)
|
|
79
|
+
const code = await mod.lint(changed, cwd)
|
|
80
|
+
if (code !== 0) return code
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
-
"plugins": ["unicorn", "oxc", "import", "jsdoc", "promise", "node", "vue"],
|
|
4
|
-
"jsPlugins": ["@e18e/eslint-plugin"],
|
|
5
|
-
"categories": {},
|
|
6
|
-
"rules": {},
|
|
7
|
-
"settings": {
|
|
8
|
-
"next": {
|
|
9
|
-
"rootDir": []
|
|
10
|
-
},
|
|
11
|
-
"jsdoc": {
|
|
12
|
-
"ignorePrivate": false,
|
|
13
|
-
"ignoreInternal": false,
|
|
14
|
-
"ignoreReplacesDocs": true,
|
|
15
|
-
"overrideReplacesDocs": true,
|
|
16
|
-
"augmentsExtendsReplacesDocs": false,
|
|
17
|
-
"implementsReplacesDocs": false,
|
|
18
|
-
"exemptDestructuredRootsFromChecks": false,
|
|
19
|
-
"tagNamePreference": {}
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"env": {
|
|
23
|
-
"builtin": true
|
|
24
|
-
},
|
|
25
|
-
"globals": {},
|
|
26
|
-
"ignorePatterns": ["**/schema.graphql", "**/auto-imports.d.ts"]
|
|
27
|
-
}
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
e18e/prefer-includes error
|
|
2
|
-
array-callback-return deny
|
|
3
|
-
no-new-wrappers deny
|
|
4
|
-
import/no-named-as-default deny
|
|
5
|
-
oxc/uninvoked-array-callback deny
|
|
6
|
-
typescript/no-non-null-assertion deny
|
|
7
|
-
unicorn/prefer-date-now deny
|
|
8
|
-
no-obj-calls deny
|
|
9
|
-
default-case-last deny
|
|
10
|
-
import/no-webpack-loader-syntax deny
|
|
11
|
-
promise/no-promise-in-callback deny
|
|
12
|
-
typescript/no-unnecessary-type-constraint deny
|
|
13
|
-
unicorn/prefer-dom-node-remove deny
|
|
14
|
-
for-direction deny
|
|
15
|
-
no-prototype-builtins deny
|
|
16
|
-
typescript/no-useless-empty-export deny
|
|
17
|
-
unicorn/prefer-includes deny
|
|
18
|
-
guard-for-in deny
|
|
19
|
-
no-restricted-globals deny
|
|
20
|
-
promise/prefer-await-to-then off
|
|
21
|
-
typescript/prefer-as-const deny
|
|
22
|
-
unicorn/prefer-math-trunc deny
|
|
23
|
-
no-self-assign deny
|
|
24
|
-
max-params off
|
|
25
|
-
react/button-has-type deny
|
|
26
|
-
typescript/prefer-function-type deny
|
|
27
|
-
unicorn/prefer-native-coercion-functions deny
|
|
28
|
-
no-extra-label deny
|
|
29
|
-
no-shadow-restricted-names deny
|
|
30
|
-
react/iframe-missing-sandbox deny
|
|
31
|
-
typescript/prefer-ts-expect-error deny
|
|
32
|
-
unicorn/prefer-number-properties off
|
|
33
|
-
no-labels deny
|
|
34
|
-
no-ternary off
|
|
35
|
-
react/jsx-key deny
|
|
36
|
-
unicorn/consistent-empty-array-spread deny
|
|
37
|
-
unicorn/prefer-query-selector deny
|
|
38
|
-
no-object-constructor deny
|
|
39
|
-
react/jsx-no-script-url deny
|
|
40
|
-
unicorn/empty-brace-spaces deny
|
|
41
|
-
unicorn/prefer-set-has deny
|
|
42
|
-
no-array-constructor deny
|
|
43
|
-
react/jsx-no-useless-fragment deny
|
|
44
|
-
unicorn/explicit-length-check off
|
|
45
|
-
unicorn/prefer-string-replace-all deny
|
|
46
|
-
no-bitwise deny
|
|
47
|
-
no-unsafe-optional-chaining deny
|
|
48
|
-
react/no-children-prop deny
|
|
49
|
-
unicorn/no-abusive-eslint-disable deny
|
|
50
|
-
unicorn/prefer-string-trim-start-end deny
|
|
51
|
-
no-class-assign deny
|
|
52
|
-
no-unused-private-class-members deny
|
|
53
|
-
react/no-direct-mutation-state deny
|
|
54
|
-
unicorn/no-array-reduce deny
|
|
55
|
-
unicorn/require-array-join-separator deny
|
|
56
|
-
no-console off
|
|
57
|
-
no-useless-concat deny
|
|
58
|
-
react/no-render-return-value deny
|
|
59
|
-
unicorn/no-console-spaces deny
|
|
60
|
-
unicorn/text-encoding-identifier-case deny
|
|
61
|
-
no-constant-condition deny
|
|
62
|
-
no-useless-rename deny
|
|
63
|
-
react/no-unescaped-entities deny
|
|
64
|
-
unicorn/no-hex-escape deny
|
|
65
|
-
no-control-regex deny
|
|
66
|
-
no-with deny
|
|
67
|
-
react/react-in-jsx-scope deny
|
|
68
|
-
unicorn/no-length-as-slice-end deny
|
|
69
|
-
no-div-regex deny
|
|
70
|
-
prefer-numeric-literals deny
|
|
71
|
-
react/self-closing-comp deny
|
|
72
|
-
unicorn/no-negation-in-equality-check deny
|
|
73
|
-
no-dupe-keys deny
|
|
74
|
-
prefer-rest-params deny
|
|
75
|
-
unicorn/no-new-buffer deny
|
|
76
|
-
no-empty-character-class deny
|
|
77
|
-
require-await deny
|
|
78
|
-
node/no-exports-assign deny
|
|
79
|
-
unicorn/no-process-exit deny
|
|
80
|
-
no-empty-static-block deny
|
|
81
|
-
sort-keys off
|
|
82
|
-
oxc/bad-array-method-on-arguments deny
|
|
83
|
-
unicorn/no-thenable deny
|
|
84
|
-
typescript/ban-ts-comment deny
|
|
85
|
-
no-eval deny
|
|
86
|
-
unicode-bom deny
|
|
87
|
-
jsdoc/check-access deny
|
|
88
|
-
oxc/bad-comparison-sequence deny
|
|
89
|
-
unicorn/no-unnecessary-await deny
|
|
90
|
-
typescript/consistent-generic-constructors deny
|
|
91
|
-
no-extra-boolean-cast deny
|
|
92
|
-
vars-on-top deny
|
|
93
|
-
jsdoc/empty-tags deny
|
|
94
|
-
oxc/bad-replace-all-arg deny
|
|
95
|
-
unicorn/no-useless-fallback-in-spread deny
|
|
96
|
-
no-global-assign deny
|
|
97
|
-
jsdoc/require-param deny
|
|
98
|
-
oxc/erasing-op deny
|
|
99
|
-
unicorn/no-useless-spread deny
|
|
100
|
-
typescript/no-confusing-non-null-assertion deny
|
|
101
|
-
no-invalid-regexp deny
|
|
102
|
-
import/no-namespace off
|
|
103
|
-
jsdoc/require-param-type deny
|
|
104
|
-
oxc/no-accumulating-spread deny
|
|
105
|
-
typescript/no-empty-interface deny
|
|
106
|
-
unicorn/no-zero-fractions deny
|
|
107
|
-
import/namespace deny
|
|
108
|
-
jsdoc/require-property-name deny
|
|
109
|
-
no-label-var deny
|
|
110
|
-
typescript/no-extra-non-null-assertion deny
|
|
111
|
-
oxc/no-barrel-file deny
|
|
112
|
-
unicorn/prefer-spread deny
|
|
113
|
-
no-negated-condition deny
|
|
114
|
-
import/no-cycle deny
|
|
115
|
-
jsdoc/require-returns-description deny
|
|
116
|
-
typescript/no-misused-new deny
|
|
117
|
-
oxc/no-optional-chaining off
|
|
118
|
-
unicorn/prefer-array-flat-map deny
|
|
119
|
-
no-new-native-nonconstructor deny
|
|
120
|
-
import/no-dynamic-require deny
|
|
121
|
-
oxc/only-used-in-recursion deny
|
|
122
|
-
typescript/no-non-null-asserted-optional-chain deny
|
|
123
|
-
unicorn/prefer-code-point deny
|
|
124
|
-
default-case off
|
|
125
|
-
no-nonoctal-decimal-escape deny
|
|
126
|
-
import/no-self-import deny
|
|
127
|
-
promise/catch-or-return deny
|
|
128
|
-
typescript/no-this-alias deny
|
|
129
|
-
unicorn/prefer-dom-node-dataset deny
|
|
130
|
-
eqeqeq deny
|
|
131
|
-
no-proto deny
|
|
132
|
-
promise/no-new-statics deny
|
|
133
|
-
typescript/no-unsafe-function-type deny
|
|
134
|
-
unicorn/prefer-event-target deny
|
|
135
|
-
no-regex-spaces deny
|
|
136
|
-
promise/prefer-await-to-callbacks off
|
|
137
|
-
typescript/no-wrapper-object-types deny
|
|
138
|
-
unicorn/prefer-math-min-max deny
|
|
139
|
-
max-lines off
|
|
140
|
-
no-script-url deny
|
|
141
|
-
promise/valid-params deny
|
|
142
|
-
typescript/prefer-for-of deny
|
|
143
|
-
unicorn/prefer-modern-math-apis deny
|
|
144
|
-
no-useless-call deny
|
|
145
|
-
no-setter-return deny
|
|
146
|
-
typescript/prefer-namespace-keyword deny
|
|
147
|
-
unicorn/prefer-node-protocol deny
|
|
148
|
-
no-nested-ternary off
|
|
149
|
-
no-template-curly-in-string deny
|
|
150
|
-
react/jsx-curly-brace-presence deny
|
|
151
|
-
unicorn/catch-error-name deny
|
|
152
|
-
unicorn/prefer-prototype-methods deny
|
|
153
|
-
no-throw-literal deny
|
|
154
|
-
react/jsx-no-duplicate-props deny
|
|
155
|
-
unicorn/consistent-function-scoping deny
|
|
156
|
-
unicorn/prefer-regexp-test deny
|
|
157
|
-
no-alert deny
|
|
158
|
-
no-unexpected-multiline deny
|
|
159
|
-
react/jsx-no-undef deny
|
|
160
|
-
unicorn/escape-case deny
|
|
161
|
-
unicorn/prefer-string-raw deny
|
|
162
|
-
no-await-in-loop off
|
|
163
|
-
no-unsafe-negation deny
|
|
164
|
-
react/no-array-index-key deny
|
|
165
|
-
unicorn/new-for-builtins deny
|
|
166
|
-
unicorn/prefer-string-starts-ends-with deny
|
|
167
|
-
no-case-declarations deny
|
|
168
|
-
no-unused-labels deny
|
|
169
|
-
react/no-danger deny
|
|
170
|
-
unicorn/no-array-for-each deny
|
|
171
|
-
unicorn/prefer-type-error deny
|
|
172
|
-
no-cond-assign deny
|
|
173
|
-
no-useless-catch deny
|
|
174
|
-
react/no-is-mounted deny
|
|
175
|
-
unicorn/no-await-in-promise-methods deny
|
|
176
|
-
unicorn/switch-case-braces deny
|
|
177
|
-
no-constant-binary-expression deny
|
|
178
|
-
no-useless-escape deny
|
|
179
|
-
react/no-string-refs deny
|
|
180
|
-
unicorn/no-empty-file deny
|
|
181
|
-
no-void deny
|
|
182
|
-
unicorn/no-invalid-remove-event-listener deny
|
|
183
|
-
react/prefer-es6-class deny
|
|
184
|
-
no-delete-var deny
|
|
185
|
-
prefer-exponentiation-operator deny
|
|
186
|
-
react/rules-of-hooks deny
|
|
187
|
-
unicorn/no-magic-array-flat-depth deny
|
|
188
|
-
no-dupe-else-if deny
|
|
189
|
-
prefer-object-spread deny
|
|
190
|
-
react/void-dom-elements-no-children deny
|
|
191
|
-
unicorn/no-new-array deny
|
|
192
|
-
no-else-return deny
|
|
193
|
-
radix off
|
|
194
|
-
unicorn/no-object-as-default-parameter deny
|
|
195
|
-
no-empty-pattern deny
|
|
196
|
-
sort-imports off
|
|
197
|
-
oxc/approx-constant deny
|
|
198
|
-
typescript/array-type deny
|
|
199
|
-
unicorn/no-static-only-class deny
|
|
200
|
-
no-eq-null deny
|
|
201
|
-
symbol-description deny
|
|
202
|
-
oxc/bad-char-at-comparison deny
|
|
203
|
-
typescript/ban-types deny
|
|
204
|
-
unicorn/no-typeof-undefined deny
|
|
205
|
-
no-extend-native deny
|
|
206
|
-
valid-typeof deny
|
|
207
|
-
jsdoc/check-tag-names deny
|
|
208
|
-
oxc/bad-object-literal-comparison deny
|
|
209
|
-
typescript/consistent-type-definitions deny
|
|
210
|
-
unicorn/no-unreadable-iife deny
|
|
211
|
-
no-func-assign deny
|
|
212
|
-
import/default deny
|
|
213
|
-
jsdoc/no-defaults deny
|
|
214
|
-
oxc/double-comparisons deny
|
|
215
|
-
typescript/no-inferrable-types deny
|
|
216
|
-
unicorn/no-useless-promise-resolve-reject deny
|
|
217
|
-
no-inner-declarations deny
|
|
218
|
-
import/no-named-default deny
|
|
219
|
-
oxc/missing-throw deny
|
|
220
|
-
jsdoc/require-param-name deny
|
|
221
|
-
typescript/no-dynamic-delete deny
|
|
222
|
-
unicorn/no-useless-undefined deny
|
|
223
|
-
no-iterator deny
|
|
224
|
-
jsdoc/require-property-description deny
|
|
225
|
-
oxc/no-async-endpoint-handlers deny
|
|
226
|
-
typescript/no-explicit-any deny
|
|
227
|
-
unicorn/numeric-separators-style off
|
|
228
|
-
no-magic-numbers off
|
|
229
|
-
import/no-commonjs off
|
|
230
|
-
jsdoc/require-returns deny
|
|
231
|
-
typescript/no-import-type-side-effects deny
|
|
232
|
-
unicorn/prefer-array-flat deny
|
|
233
|
-
no-new-func deny
|
|
234
|
-
import/no-duplicates deny
|
|
235
|
-
jsdoc/require-yields deny
|
|
236
|
-
oxc/number-arg-out-of-range deny
|
|
237
|
-
typescript/no-non-null-asserted-nullish-coalescing deny
|
|
238
|
-
unicorn/prefer-blob-reading-methods deny
|
|
239
|
-
no-new deny
|
|
240
|
-
import/no-named-as-default-member off
|
|
241
|
-
promise/avoid-new deny
|
|
242
|
-
typescript/no-require-imports deny
|
|
243
|
-
unicorn/prefer-dom-node-append deny
|
|
244
|
-
default-param-last deny
|
|
245
|
-
no-plusplus off
|
|
246
|
-
import/unambiguous off
|
|
247
|
-
promise/no-callback-in-promise deny
|
|
248
|
-
typescript/no-unsafe-declaration-merging deny
|
|
249
|
-
unicorn/prefer-dom-node-text-content deny
|
|
250
|
-
func-names off
|
|
251
|
-
no-redeclare deny
|
|
252
|
-
promise/param-names deny
|
|
253
|
-
typescript/no-var-requires deny
|
|
254
|
-
unicorn/prefer-logical-operator-over-ternary deny
|
|
255
|
-
max-classes-per-file deny
|
|
256
|
-
no-return-assign deny
|
|
257
|
-
promise/spec-only deny
|
|
258
|
-
typescript/prefer-enum-initializers deny
|
|
259
|
-
unicorn/prefer-modern-dom-apis deny
|
|
260
|
-
new-cap off
|
|
261
|
-
no-self-compare deny
|
|
262
|
-
react/checked-requires-onchange-or-readonly deny
|
|
263
|
-
typescript/prefer-literal-enum-member deny
|
|
264
|
-
unicorn/prefer-negative-index deny
|
|
265
|
-
no-multi-assign deny
|
|
266
|
-
no-sparse-arrays deny
|
|
267
|
-
typescript/triple-slash-reference deny
|
|
268
|
-
react/jsx-boolean-value deny
|
|
269
|
-
unicorn/prefer-optional-catch-binding deny
|
|
270
|
-
no-lone-blocks deny
|
|
271
|
-
no-this-before-super deny
|
|
272
|
-
react/jsx-no-comment-textnodes deny
|
|
273
|
-
unicorn/consistent-existence-index-check deny
|
|
274
|
-
unicorn/prefer-reflect-apply deny
|
|
275
|
-
no-duplicate-imports deny
|
|
276
|
-
no-undefined off
|
|
277
|
-
react/jsx-no-target-blank deny
|
|
278
|
-
unicorn/prefer-set-size deny
|
|
279
|
-
unicorn/error-message deny
|
|
280
|
-
no-async-promise-executor deny
|
|
281
|
-
no-unsafe-finally deny
|
|
282
|
-
react/jsx-props-no-spread-multi deny
|
|
283
|
-
unicorn/filename-case off
|
|
284
|
-
unicorn/prefer-string-slice deny
|
|
285
|
-
no-caller deny
|
|
286
|
-
no-unused-expressions deny
|
|
287
|
-
react/no-danger-with-children deny
|
|
288
|
-
unicorn/no-anonymous-default-export off
|
|
289
|
-
unicorn/prefer-structured-clone deny
|
|
290
|
-
no-compare-neg-zero deny
|
|
291
|
-
no-unused-vars deny
|
|
292
|
-
react/no-find-dom-node deny
|
|
293
|
-
unicorn/no-await-expression-member deny
|
|
294
|
-
unicorn/require-number-to-fixed-digits-argument deny
|
|
295
|
-
no-const-assign deny
|
|
296
|
-
no-useless-constructor deny
|
|
297
|
-
react/no-set-state deny
|
|
298
|
-
unicorn/no-document-cookie deny
|
|
299
|
-
unicorn/throw-new-error deny
|
|
300
|
-
no-constructor-return deny
|
|
301
|
-
no-var deny
|
|
302
|
-
react/no-unknown-property deny
|
|
303
|
-
unicorn/no-instanceof-array deny
|
|
304
|
-
no-debugger deny
|
|
305
|
-
prefer-promise-reject-errors deny
|
|
306
|
-
unicorn/no-lonely-if deny
|
|
307
|
-
no-dupe-class-members deny
|
|
308
|
-
prefer-object-has-own deny
|
|
309
|
-
react/style-prop-object deny
|
|
310
|
-
unicorn/no-nested-ternary off
|
|
311
|
-
no-duplicate-case deny
|
|
312
|
-
prefer-spread deny
|
|
313
|
-
unicorn/no-null off
|
|
314
|
-
no-empty-function deny
|
|
315
|
-
require-yield deny
|
|
316
|
-
node/no-new-require deny
|
|
317
|
-
typescript/adjacent-overload-signatures deny
|
|
318
|
-
unicorn/no-single-promise-in-promise-methods deny
|
|
319
|
-
no-empty deny
|
|
320
|
-
sort-vars deny
|
|
321
|
-
oxc/bad-bitwise-operator deny
|
|
322
|
-
typescript/ban-tslint-comment deny
|
|
323
|
-
unicorn/no-this-assignment deny
|
|
324
|
-
no-ex-assign deny
|
|
325
|
-
use-isnan deny
|
|
326
|
-
jsdoc/check-property-names deny
|
|
327
|
-
oxc/bad-min-max-func deny
|
|
328
|
-
typescript/consistent-indexed-object-style deny
|
|
329
|
-
unicorn/no-unreadable-array-destructuring deny
|
|
330
|
-
no-fallthrough deny
|
|
331
|
-
yoda deny
|
|
332
|
-
jsdoc/implements-on-classes deny
|
|
333
|
-
oxc/const-comparisons deny
|
|
334
|
-
unicorn/no-useless-length-check deny
|
|
335
|
-
typescript/explicit-function-return-type deny
|
|
336
|
-
no-import-assign deny
|
|
337
|
-
import/first deny
|
|
338
|
-
jsdoc/require-param-description deny
|
|
339
|
-
typescript/no-duplicate-enum-values deny
|
|
340
|
-
oxc/misrefactored-assign-op deny
|
|
341
|
-
unicorn/no-useless-switch-case deny
|
|
342
|
-
no-irregular-whitespace deny
|
|
343
|
-
import/max-dependencies off
|
|
344
|
-
jsdoc/require-property deny
|
|
345
|
-
oxc/no-async-await off
|
|
346
|
-
typescript/no-empty-object-type deny
|
|
347
|
-
unicorn/number-literal-case deny
|
|
348
|
-
no-loss-of-precision deny
|
|
349
|
-
import/no-amd deny
|
|
350
|
-
jsdoc/require-property-type deny
|
|
351
|
-
oxc/no-const-enum deny
|
|
352
|
-
typescript/no-extraneous-class deny
|
|
353
|
-
unicorn/prefer-add-event-listener deny
|
|
354
|
-
no-multi-str deny
|
|
355
|
-
import/no-default-export off
|
|
356
|
-
jsdoc/require-returns-type deny
|
|
357
|
-
oxc/no-rest-spread-properties off
|
|
358
|
-
typescript/no-namespace deny
|
|
359
|
-
unicorn/prefer-array-some deny
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Збирає `oxlint-canonical.json` з `oxlint-canonical-skeleton.json` (без поля rules) та списку
|
|
3
|
-
* правил у `oxlint-rules.tsv` (колонки: ім’я правила, TAB, severity: deny | off | error).
|
|
4
|
-
*
|
|
5
|
-
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/lib/rebuild-oxlint-canonical.mjs`,
|
|
6
|
-
* потім скопіюй оновлений канон у корінь споживача як `.oxlintrc.json` за потреби.
|
|
7
|
-
*/
|
|
8
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
9
|
-
import { dirname, join } from 'node:path'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
11
|
-
|
|
12
|
-
const dir = join(dirname(fileURLToPath(import.meta.url)), '..', 'js', 'data', 'tooling')
|
|
13
|
-
const rules = {}
|
|
14
|
-
for (const line of readFileSync(join(dir, 'oxlint-rules.tsv'), 'utf8').split('\n')) {
|
|
15
|
-
const t = line.trim()
|
|
16
|
-
if (!t) {
|
|
17
|
-
continue
|
|
18
|
-
}
|
|
19
|
-
const i = t.indexOf('\t')
|
|
20
|
-
if (i === -1) {
|
|
21
|
-
throw new Error(`oxlint-rules.tsv: очікується TAB між ключем і значенням: ${t}`)
|
|
22
|
-
}
|
|
23
|
-
rules[t.slice(0, i)] = t.slice(i + 1)
|
|
24
|
-
}
|
|
25
|
-
const skeleton = JSON.parse(readFileSync(join(dir, 'oxlint-canonical-skeleton.json'), 'utf8'))
|
|
26
|
-
skeleton.rules = rules
|
|
27
|
-
const out = join(dir, 'oxlint-canonical.json')
|
|
28
|
-
writeFileSync(out, `${JSON.stringify(skeleton, null, 2)}\n`)
|
|
29
|
-
console.log(`wrote ${out} (${Object.keys(rules).length} rules)`)
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor lint` — оркестратор лінт-ланцюжка з вимірюванням часу на кожен крок.
|
|
3
|
-
*
|
|
4
|
-
* Замість агрегатора `bun run lint-ga && bun run lint-js && ... && oxfmt .` у кореневому
|
|
5
|
-
* `package.json` (де child-процеси анонімні і час кожного не видно), цей оркестратор:
|
|
6
|
-
*
|
|
7
|
-
* - читає `scripts` з кореневого `package.json`,
|
|
8
|
-
* - бере **присутні** ключі з фіксованого списку `LINT_SCRIPTS` (відсутні мовчки пропускає),
|
|
9
|
-
* - послідовно запускає `bun run <script>`,
|
|
10
|
-
* - заміряє час кожного,
|
|
11
|
-
* - **fail-fast**: при першому ненульовому exit-коді зупиняється, друкує таблицю
|
|
12
|
-
* лише по виконаних і повертає той самий код,
|
|
13
|
-
* - друкує підсумкову таблицю `⏱ Lint timing` і повертає 0, якщо все ОК.
|
|
14
|
-
*
|
|
15
|
-
* Список + порядок зумисне фіксований: збігається з канонічним ланцюжком, що його раніше
|
|
16
|
-
* тримав root `package.json`. Динамічний discovery (`scripts/^lint-/`) дав би непередбачуваний
|
|
17
|
-
* порядок і небажану інтерпретацію власних `lint-*` користувача.
|
|
18
|
-
*
|
|
19
|
-
* `oxfmt` — окремий рядок поза префіксом `lint-`, ставиться в кінець (як було у `lint`).
|
|
20
|
-
*/
|
|
21
|
-
import { spawnSync as defaultSpawnSync } from 'node:child_process'
|
|
22
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
23
|
-
import { join } from 'node:path'
|
|
24
|
-
|
|
25
|
-
import { formatTimingSummary } from './timing-summary.mjs'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Імена npm-скриптів, які `n-cursor lint` запускає **по черзі**, якщо вони є у root `package.json`.
|
|
29
|
-
* Порядок дзеркалить попередній агрегатор `lint`: cheap-checks першими, формат — в кінці.
|
|
30
|
-
*/
|
|
31
|
-
export const LINT_SCRIPTS = /** @type {const} */ ([
|
|
32
|
-
'lint-ga',
|
|
33
|
-
'lint-js',
|
|
34
|
-
'lint-rego',
|
|
35
|
-
'lint-style',
|
|
36
|
-
'lint-text',
|
|
37
|
-
'lint-security',
|
|
38
|
-
'oxfmt'
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Читає `scripts` з `package.json` у заданій теці. Повертає `null`, якщо файла немає, JSON
|
|
43
|
-
* некоректний або поля `scripts` нема. Не кидає — викликач сам вирішує, що робити.
|
|
44
|
-
* @param {string} root абсолютний шлях до теки з `package.json`
|
|
45
|
-
* @returns {Record<string, string> | null} мапа scripts або null
|
|
46
|
-
*/
|
|
47
|
-
function readRootScripts(root) {
|
|
48
|
-
const packageJsonPath = join(root, 'package.json')
|
|
49
|
-
if (!existsSync(packageJsonPath)) {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
54
|
-
const scripts = parsed?.scripts
|
|
55
|
-
if (!scripts || typeof scripts !== 'object') {
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
|
-
return /** @type {Record<string, string>} */ (scripts)
|
|
59
|
-
} catch {
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @typedef {{
|
|
66
|
-
* cwd?: string,
|
|
67
|
-
* spawnSyncFn?: typeof defaultSpawnSync,
|
|
68
|
-
* now?: () => number,
|
|
69
|
-
* log?: (text: string) => void,
|
|
70
|
-
* logError?: (text: string) => void
|
|
71
|
-
* }} RunLintCliOptions
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Виконує лінт-ланцюжок з вимірюванням часу. Повертає exit-код, не кидає винятків (для прямого
|
|
76
|
-
* присвоєння у `process.exitCode`).
|
|
77
|
-
* @param {RunLintCliOptions} [options] DI для тестів (підміняємо spawn / fs / clock)
|
|
78
|
-
* @returns {number} 0 = успіх, ненульовий = code першого впалого скрипта, або 1 при структурних проблемах
|
|
79
|
-
*/
|
|
80
|
-
export function runLintCli(options = {}) {
|
|
81
|
-
const root = options.cwd ?? process.cwd()
|
|
82
|
-
const spawnSync = options.spawnSyncFn ?? defaultSpawnSync
|
|
83
|
-
const now = options.now ?? Date.now
|
|
84
|
-
const log = options.log ?? (text => process.stdout.write(text))
|
|
85
|
-
const logError = options.logError ?? (text => process.stderr.write(text))
|
|
86
|
-
|
|
87
|
-
const scripts = readRootScripts(root)
|
|
88
|
-
if (scripts === null) {
|
|
89
|
-
logError(`❌ n-cursor lint: не знайдено package.json або поля "scripts" у ${root}\n`)
|
|
90
|
-
return 1
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const present = LINT_SCRIPTS.filter(name => typeof scripts[name] === 'string' && scripts[name].length > 0)
|
|
94
|
-
if (present.length === 0) {
|
|
95
|
-
log('\nℹ️ n-cursor lint: у package.json немає жодного з lint-* / oxfmt скриптів — нічого запускати.\n')
|
|
96
|
-
return 0
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** @type {{ id: string, ms: number, ok: boolean }[]} */
|
|
100
|
-
const timings = []
|
|
101
|
-
let failedCode = 0
|
|
102
|
-
for (const name of present) {
|
|
103
|
-
const startedAt = now()
|
|
104
|
-
const result = spawnSync('bun', ['run', name], { stdio: 'inherit', cwd: root })
|
|
105
|
-
const code = typeof result.status === 'number' ? result.status : 1
|
|
106
|
-
const ok = code === 0
|
|
107
|
-
timings.push({ id: name, ms: now() - startedAt, ok })
|
|
108
|
-
if (!ok) {
|
|
109
|
-
failedCode = code === 0 ? 1 : code
|
|
110
|
-
break
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
log(formatTimingSummary('Lint timing', timings))
|
|
115
|
-
return failedCode
|
|
116
|
-
}
|