@7n/rules-lang-js 0.2.0

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 ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-07-19
4
+
5
+ ### Added
6
+
7
+ - Перший реліз: EcosystemProvider npm/bun для taze-оркестратора `@7n/rules` (extension-point `taze`, контракт `@7n/rules/plugin-api`) — фаза 5a spec lang-plugins-extraction: JS-екосистема стала таким самим плагіном, як Rust/Python, ядро — двигун без мовної специфіки. Бекап package.json воркспейсів, bump через `bunx taze -w -r latest` + `bun install`, детермінований `collectTazeDiff` (semver caret-класифікація), CLI `n-rules taze diff` — через handler плагіна. Автодетект — за кореневим `package.json`
8
+
9
+ All notable changes to this project will be documented in this file.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@7n/rules-lang-js",
3
+ "version": "0.2.0",
4
+ "description": "Плагін @7n/rules: JS/npm/bun-екосистема — taze-провайдер (package.json, bunx taze)",
5
+ "keywords": [
6
+ "javascript",
7
+ "bun",
8
+ "taze",
9
+ "n-rules-plugin"
10
+ ],
11
+ "homepage": "https://github.com/nitra/7n-rules#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/nitra/7n-rules/issues"
14
+ },
15
+ "license": "ISC",
16
+ "author": "v@nitra.ai",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/nitra/7n-rules.git",
20
+ "directory": "plugins/lang-js"
21
+ },
22
+ "type": "module",
23
+ "files": [
24
+ "taze",
25
+ "skills",
26
+ "CHANGELOG.md",
27
+ "!**/*.test.mjs",
28
+ "!**/*_test.rego",
29
+ "!**/fixtures/**",
30
+ "!**/tests/**"
31
+ ],
32
+ "n-rules": {
33
+ "capabilities": [
34
+ "lang:js"
35
+ ],
36
+ "contributes": {
37
+ "rules": false,
38
+ "handlers": {
39
+ "taze": "./taze/provider.mjs"
40
+ }
41
+ }
42
+ },
43
+ "peerDependencies": {
44
+ "@7n/rules": ">=1.24.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "engines": {
50
+ "bun": ">=1.3",
51
+ "node": ">=24"
52
+ }
53
+ }
@@ -0,0 +1,67 @@
1
+ ## npm/bun-гілка (`@7n/rules-lang-js`)
2
+
3
+ Екосистемна гілка JavaScript/npm/bun для taze — виконує кроки 1–8 скелета.
4
+
5
+ ### Детекція і передумови
6
+
7
+ Кореневий `package.json`. Потрібні `bun` і `bunx`; чисте робоче дерево (без незакомічених змін у `package.json` / `bun.lock` / `node_modules`).
8
+
9
+ ### Крок 1 — стартовий стан
10
+
11
+ ```bash
12
+ cp package.json package.json.taze-bak
13
+ cp bun.lock bun.lock.taze-bak
14
+ ```
15
+
16
+ (У monorepo — також усі `*/package.json` воркспейсів. Файли тимчасові, видаляються в кінці.)
17
+
18
+ ### Крок 2 — оновлення
19
+
20
+ ```bash
21
+ bunx taze -w -r latest
22
+ bun install
23
+ ```
24
+
25
+ - `-w` — записати нові версії у `package.json`.
26
+ - `-r` — рекурсивно по всіх воркспейсах.
27
+ - `latest` — піднімати навіть major.
28
+
29
+ ### Крок 3 — major-оновлення
30
+
31
+ > **Не порівнюй `package.json` вручну.** Класифікацію semver несе CLI — детерміновано, по всіх воркспейсах.
32
+
33
+ ```bash
34
+ n-rules taze diff
35
+ ```
36
+
37
+ Друкує компактний JSON: `{ "major": [{workspace, pkg, from, to}], "minorPatch": <N>, "totalChanged": <N> }`. `major` — залежності зі зміною найлівішої ненульової компоненти semver (`1.x→2.x`, `0.4.x→0.5.x`, `0.0.3→0.0.4`); саме вони йдуть у кроки 4–6. Покриває **прямі** залежності `package.json` (root + воркспейси); транзитивні (`bun.lock`) — за потреби окремо.
38
+
39
+ ### Крок 4 — breaking changes
40
+
41
+ CHANGELOG/Releases репозиторію модуля (поле `repository` у `node_modules/<name>/package.json`); якщо неінформативно — `diff -r` між кешованою старою версією (`~/.bun/install/cache/<name>@<old-version>/`) і новою (`node_modules/<name>/`) по `dist/` / `*.d.ts` / entry-points з `exports`.
42
+
43
+ ### Крок 5 — сумісність з кодом
44
+
45
+ ```bash
46
+ rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
47
+ ```
48
+
49
+ ### Крок 6 — перевірки після правок
50
+
51
+ ```bash
52
+ npx @7n/rules lint
53
+ bun run typecheck # якщо є
54
+ bun test # якщо є
55
+ ```
56
+
57
+ ### Крок 7 — прибирання
58
+
59
+ ```bash
60
+ rm package.json.taze-bak bun.lock.taze-bak
61
+ ```
62
+
63
+ (І решту бекапів воркспейсів, якщо створювались.)
64
+
65
+ ### Крок 8 — звіт
66
+
67
+ Окрема секція **npm/bun-пакети** (оновлено / major / зрефакторено / потребує ручного втручання), у **Стан перевірок** — `lint` / `typecheck` / `test`.
package/taze/diff.mjs ADDED
@@ -0,0 +1,110 @@
1
+ /** @see ./docs/diff.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+
6
+ import { isBreaking, parseVersion } from '@7n/rules/plugin-api'
7
+ import { getMonorepoPackageRootDirs } from '@7n/rules/scripts/lib/workspaces.mjs'
8
+
9
+ /** Поля package.json із залежностями, які порівнюємо. */
10
+ const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
11
+
12
+ /** Дефолтний суфікс бекапу, який створює крок 1 скіла (`cp package.json package.json.taze-bak`). */
13
+ const DEFAULT_BACKUP_SUFFIX = '.taze-bak'
14
+
15
+ /**
16
+ * Порівнює два package.json-обʼєкти й повертає зміни залежностей.
17
+ * @param {object} oldPkg розпарсений старий package.json (бекап)
18
+ * @param {object} newPkg розпарсений новий package.json
19
+ * @param {string} workspace мітка воркспейсу (`.` для кореня)
20
+ * @returns {{major: Array<{workspace:string, pkg:string, from:string, to:string}>, minorPatch:number}} зміни
21
+ */
22
+ export function diffPackageJson(oldPkg, newPkg, workspace) {
23
+ const major = []
24
+ let minorPatch = 0
25
+ for (const field of DEP_FIELDS) {
26
+ const oldDeps = oldPkg?.[field]
27
+ const newDeps = newPkg?.[field]
28
+ if (!oldDeps || !newDeps) continue
29
+ for (const [pkg, from] of Object.entries(oldDeps)) {
30
+ const to = newDeps[pkg]
31
+ if (to === undefined || to === from) continue
32
+ const fromV = parseVersion(from)
33
+ const toV = parseVersion(to)
34
+ if (fromV && toV && isBreaking(fromV, toV)) {
35
+ major.push({ workspace, pkg, from, to })
36
+ } else {
37
+ minorPatch += 1
38
+ }
39
+ }
40
+ }
41
+ return { major, minorPatch }
42
+ }
43
+
44
+ /**
45
+ * Читає JSON-файл або повертає null, якщо файл відсутній / невалідний.
46
+ * @param {string} path абсолютний шлях
47
+ * @returns {Promise<object|null>} розпарсений обʼєкт або null
48
+ */
49
+ async function readJsonOrNull(path) {
50
+ if (!existsSync(path)) return null
51
+ try {
52
+ return JSON.parse(await readFile(path, 'utf8'))
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Збирає diff по всьому монорепо: для кожного воркспейсу порівнює
60
+ * `<ws>/package.json` з `<ws>/package.json<backupSuffix>`.
61
+ * @param {string} cwd корінь репозиторію
62
+ * @param {string} [backupSuffix] суфікс бекап-файлу
63
+ * @returns {Promise<{major: Array<{workspace:string, pkg:string, from:string, to:string}>, minorPatch:number, totalChanged:number, comparedWorkspaces:number}>} агрегований diff
64
+ */
65
+ export async function collectTazeDiff(cwd, backupSuffix = DEFAULT_BACKUP_SUFFIX) {
66
+ const roots = await getMonorepoPackageRootDirs(cwd)
67
+ const major = []
68
+ let minorPatch = 0
69
+ let comparedWorkspaces = 0
70
+ for (const ws of roots) {
71
+ const dir = join(cwd, ws)
72
+ const oldPkg = await readJsonOrNull(join(dir, `package.json${backupSuffix}`))
73
+ const newPkg = await readJsonOrNull(join(dir, 'package.json'))
74
+ if (!oldPkg || !newPkg) continue
75
+ comparedWorkspaces += 1
76
+ const res = diffPackageJson(oldPkg, newPkg, ws)
77
+ major.push(...res.major)
78
+ minorPatch += res.minorPatch
79
+ }
80
+ return { major, minorPatch, totalChanged: major.length + minorPatch, comparedWorkspaces }
81
+ }
82
+
83
+ const USAGE = 'Usage: n-rules taze diff [--backup-suffix <suffix>]'
84
+
85
+ /**
86
+ * CLI: `n-rules taze diff` друкує компактний JSON зі списком major-оновлень і
87
+ * лічбою minor/patch. Read-only.
88
+ * @param {string[]} args аргументи після `taze`
89
+ * @param {string} [cwd] корінь репозиторію (ін'єкція для тестів)
90
+ * @returns {Promise<number>} exit code
91
+ */
92
+ export async function runTazeCli(args, cwd = process.cwd()) {
93
+ if (args[0] !== 'diff') {
94
+ console.error(USAGE)
95
+ return 1
96
+ }
97
+ const flagAt = args.indexOf('--backup-suffix')
98
+ const backupSuffix = flagAt === -1 ? DEFAULT_BACKUP_SUFFIX : args[flagAt + 1]
99
+ if (!backupSuffix) {
100
+ console.error(USAGE)
101
+ return 1
102
+ }
103
+ const diff = await collectTazeDiff(cwd, backupSuffix)
104
+ if (diff.comparedWorkspaces === 0) {
105
+ console.error(`✗ Не знайдено жодного package.json${backupSuffix} — спершу зроби бекап (крок 1 скіла).`)
106
+ return 1
107
+ }
108
+ process.stdout.write(`${JSON.stringify(diff)}\n`)
109
+ return 0
110
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: JS Module
3
+ title: diff.mjs
4
+ resource: plugins/lang-js/taze/diff.mjs
5
+ docgen:
6
+ crc: e9789318
7
+ ---
8
+
9
+ ## Огляд
10
+
11
+ Детермінований semver-diff npm/bun-гілки taze: порівнює package.json кожного воркспейсу з `.taze-bak`-бекапом і класифікує зміни залежностей на major vs minor/patch за caret-правилом (`isBreaking`/`parseVersion` з `@7n/rules/plugin-api`).
12
+
13
+ ## Поведінка
14
+
15
+ - **diffPackageJson** — порівнює два розпарсені package.json по dependencies/devDependencies/peerDependencies/optionalDependencies; не-semver специфікатори (workspace:*, git-url) ігноруються.
16
+ - **collectTazeDiff** — агрегує diff по root + воркспейсах монорепо (`getMonorepoPackageRootDirs` ядра); воркспейс без бекапу пропускається.
17
+ - **runTazeCli** — CLI-обгортка (`n-rules taze diff`): друкує компактний JSON `{ major, minorPatch, totalChanged }`.
18
+
19
+ ## Гарантії поведінки
20
+
21
+ - Read-only: лише читає файли, нічого не пише.
22
+ - Відсутній або невалідний бекап/маніфест — пропуск без винятку.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: JS Module
3
+ title: provider.mjs
4
+ resource: plugins/lang-js/taze/provider.mjs
5
+ docgen:
6
+ crc: 8749ccfb
7
+ ---
8
+
9
+ ## Огляд
10
+
11
+ EcosystemProvider npm/bun для taze-оркестратора ядра (фаза 5a spec lang-plugins-extraction: ядро — двигун без мовної специфіки, JS-екосистема — такий самий плагін, як Rust/Python). Контракт `@7n/rules/plugin-api`, реєструється маніфестом `n-rules.contributes.handlers.taze`. Реекспортує `runTazeCli` для CLI `n-rules taze diff` ядра.
12
+
13
+ ## Поведінка
14
+
15
+ - **buildDependencyPrompt** — промпт ОДНОГО ізольованого виклику раннера для одного major-запису (кроки 4-6 SKILL.md; без кроків 1-3/7/8 — їх виконує оркестратор).
16
+ - **backupWorkspacePackageFiles** / **cleanupWorkspaceBackups** — бекап і прибирання package.json кожного воркспейсу монорепо (`.taze-bak`).
17
+ - **jsProvider** — detect за кореневим package.json; available перевіряє bun; bump — `bunx taze -w -r latest` + `bun install` (провал кидає з exit-кодом і stderr); diff — `collectTazeDiff` з мапінгом workspace → manifest (контракт порту); cleanup — прибирання бекапів воркспейсів.
18
+
19
+ ## Гарантії поведінки
20
+
21
+ - Виконує файлові операції (бекапи) і запускає зовнішні команди (`bunx`, `bun`) — НЕ read-only.
22
+ - Без кореневого package.json detect повертає порожньо — тиша у звіті.
@@ -0,0 +1,130 @@
1
+ /** @see ./docs/provider.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { copyFile, rm } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+
6
+ import { getMonorepoPackageRootDirs } from '@7n/rules/scripts/lib/workspaces.mjs'
7
+
8
+ import { collectTazeDiff } from './diff.mjs'
9
+
10
+ // CLI `n-rules taze diff` ядра резолвить цей handler-модуль і кличе runTazeCli.
11
+ export { runTazeCli } from './diff.mjs'
12
+
13
+ /** Суфікс бекапу — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
14
+ const BACKUP_SUFFIX = '.taze-bak'
15
+
16
+ /**
17
+ * Промпт ОДНОГО ітеративного виклику для npm/bun-пакета (кроки 4-6 SKILL.md)
18
+ * для ОДНОГО major-запису. Кроки 1-3/7/8 виконує оркестратор ядра
19
+ * детерміновано, без LLM.
20
+ * @param {{manifest: string, pkg: string, from: string, to: string}} entry запис major-diff (з `collectTazeDiff`, workspace → manifest)
21
+ * @returns {string} готовий промпт
22
+ */
23
+ export function buildDependencyPrompt({ manifest, pkg, from, to }) {
24
+ return [
25
+ '# Major-оновлення одного пакета: перевірка сумісності й рефакторинг',
26
+ '',
27
+ `Пакет \`${pkg}\` у воркспейсі \`${manifest}\`: **${from} → ${to}** — вже застосовано в package.json/bun.lock (кроки 1-3 виконано детерміновано, без тебе). Твоя задача — лише breaking-changes-перевірка й, за потреби, рефакторинг.`,
28
+ '',
29
+ '## Кроки',
30
+ `1. Зібрати breaking changes цього оновлення: CHANGELOG/Releases репозиторію модуля (поле \`repository\` у \`node_modules/${pkg}/package.json\`), або git/diff між закешованою старою версією (\`~/.bun/install/cache/${pkg}@<стара-версія>/\`) і новою (\`node_modules/${pkg}/\`).`,
31
+ `2. Знайти використання зачепленого API в коді проєкту (\`rg -n\` по імпортах/викликах \`${pkg}\`).`,
32
+ '3. Сумісно — нічого не робити. Несумісно — застосувати міграцію (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом).',
33
+ '4. Якщо були правки — запусти `npx @7n/rules lint`, typecheck/test якщо є в проєкті.',
34
+ '5. Нетривіальна/неоднозначна міграція — не вгадуй, залиш TODO-коментар із посиланням на CHANGELOG.',
35
+ '',
36
+ 'У відповіді одним абзацом підсумуй: сумісно / зрефакторено (які файли) / TODO (чому).'
37
+ ].join('\n')
38
+ }
39
+
40
+ /**
41
+ * Бекапить package.json кожного воркспейсу (крок 1 SKILL.md) — потрібно для
42
+ * класифікації major/minor через `collectTazeDiff` після bump-у.
43
+ * @param {string} cwd корінь репо
44
+ * @param {{ getMonorepoPackageRootDirs?: (cwd: string) => Promise<string[]>, copyFile?: (src: string, dest: string) => Promise<void> }} [deps] інжекти
45
+ * @returns {Promise<string[]>} відносні шляхи воркспейсів, що мали package.json
46
+ */
47
+ export async function backupWorkspacePackageFiles(cwd, deps = {}) {
48
+ const getRoots = deps.getMonorepoPackageRootDirs ?? getMonorepoPackageRootDirs
49
+ const copy = deps.copyFile ?? copyFile
50
+ const roots = await getRoots(cwd)
51
+ const backedUp = []
52
+ for (const ws of roots) {
53
+ const pkgPath = join(cwd, ws, 'package.json')
54
+ if (!existsSync(pkgPath)) continue
55
+ await copy(pkgPath, `${pkgPath}${BACKUP_SUFFIX}`)
56
+ backedUp.push(ws)
57
+ }
58
+ return backedUp
59
+ }
60
+
61
+ /**
62
+ * Прибирає бекапи package.json усіх воркспейсів (крок 7 SKILL.md).
63
+ * @param {string} cwd корінь репо
64
+ * @param {{ getMonorepoPackageRootDirs?: (cwd: string) => Promise<string[]>, rm?: (path: string, opts?: object) => Promise<void> }} [deps] інжекти
65
+ * @returns {Promise<void>}
66
+ */
67
+ export async function cleanupWorkspaceBackups(cwd, deps = {}) {
68
+ const getRoots = deps.getMonorepoPackageRootDirs ?? getMonorepoPackageRootDirs
69
+ const remove = deps.rm ?? rm
70
+ for (const ws of await getRoots(cwd)) {
71
+ await remove(join(cwd, ws, `package.json${BACKUP_SUFFIX}`), { force: true })
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Виконує детерміновану команду npm-гілки, кидає з exit-кодом+stderr при провалі.
77
+ * @param {string} cmd бінарник
78
+ * @param {string[]} args аргументи
79
+ * @param {string} cwd робочий каталог
80
+ * @param {import('@7n/rules/plugin-api').SpawnFn} spawnFn spawnSync-сумісний виклик
81
+ * @returns {void}
82
+ */
83
+ function runCommand(cmd, args, cwd, spawnFn) {
84
+ const result = spawnFn(cmd, args, { cwd, encoding: 'utf8' })
85
+ if (result.status !== 0) {
86
+ throw new Error(`${cmd} ${args.join(' ')} → exit ${result.status}: ${result.stderr || result.stdout}`)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * EcosystemProvider npm/bun для taze-оркестратора ядра (фаза 5a spec
92
+ * lang-plugins-extraction: ядро — двигун без мовної специфіки, JS-екосистема —
93
+ * такий самий плагін, як Rust/Python). Контракт `@7n/rules/plugin-api`,
94
+ * реєструється маніфестом (`n-rules.contributes.handlers.taze`).
95
+ * `diff.major` записи мапляться workspace → manifest (контракт порту).
96
+ * @type {import('@7n/rules/plugin-api').EcosystemProvider}
97
+ */
98
+ export const jsProvider = {
99
+ id: 'js-bun',
100
+ title: 'npm/bun-пакети',
101
+ manifestNoun: 'package.json',
102
+ skillSection: 'npm/bun-гілкою SKILL.md',
103
+ detect: cwd => (existsSync(join(cwd, 'package.json')) ? ['package.json'] : []),
104
+ available: spawnFn =>
105
+ spawnFn('bun', ['--version'], { encoding: 'utf8' }).status === 0
106
+ ? { ok: true, reason: null }
107
+ : { ok: false, reason: '`bun` не встановлено (https://bun.sh) — npm/bun-гілка потребує bun/bunx' },
108
+ backup: async (cwd, manifests, deps) => {
109
+ await backupWorkspacePackageFiles(cwd, deps)
110
+ },
111
+ bump: (cwd, manifests, { spawnFn, log }) => {
112
+ log('⬆️ bunx taze -w -r latest...')
113
+ runCommand('bunx', ['taze', '-w', '-r', 'latest'], cwd, spawnFn)
114
+ log('📥 bun install...')
115
+ runCommand('bun', ['install'], cwd, spawnFn)
116
+ return Promise.resolve()
117
+ },
118
+ diff: async cwd => {
119
+ const diff = await collectTazeDiff(cwd)
120
+ return {
121
+ major: diff.major.map(({ workspace, pkg, from, to }) => ({ manifest: workspace, pkg, from, to })),
122
+ minorPatch: diff.minorPatch,
123
+ totalChanged: diff.totalChanged
124
+ }
125
+ },
126
+ promptFor: buildDependencyPrompt,
127
+ cleanup: (cwd, manifests, deps) => cleanupWorkspaceBackups(cwd, deps)
128
+ }
129
+
130
+ export default jsProvider