@7n/rules-lang-js 0.8.0 → 0.10.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 +23 -0
- package/doc-files/docs/extractors.md +1 -1
- package/doc-files/docs/index.md +1 -0
- package/doc-files/docs/js-facts.md +47 -0
- package/doc-files/docs/units-js.md +1 -2
- package/doc-files/extractors.mjs +135 -12
- package/doc-files/js-facts.mjs +28 -0
- package/doc-files/units-js.mjs +26 -10
- package/package.json +1 -1
- package/rules/bun/package_json/package_json.mdc +3 -1
- package/rules/bun/package_json/package_json.rego +30 -1
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +18 -3
- package/rules/npm-module/npm_package_json/npm_package_json.rego +42 -5
- package/rules/storybook/adopt/docs/index.md +9 -0
- package/rules/storybook/adopt/docs/main.md +46 -0
- package/rules/storybook/adopt/main.mjs +379 -0
- package/rules/storybook/hygiene/concern.json +4 -0
- package/rules/storybook/hygiene/docs/index.md +9 -0
- package/rules/storybook/hygiene/docs/main.md +45 -0
- package/rules/storybook/hygiene/main.mjs +254 -0
- package/rules/storybook/main.json +1 -0
- package/rules/storybook/main.mdc +60 -0
- package/rules/storybook/mocking/concern.json +3 -0
- package/rules/storybook/mocking/mocking.mdc +108 -0
- package/rules/storybook/scaffold/concern.json +5 -0
- package/rules/storybook/scaffold/docs/fix-scaffold.md +29 -0
- package/rules/storybook/scaffold/docs/index.md +10 -0
- package/rules/storybook/scaffold/docs/main.md +36 -0
- package/rules/storybook/scaffold/fix-scaffold.mjs +150 -0
- package/rules/storybook/scaffold/main.mjs +164 -0
- package/rules/storybook/scaffold/template/docs/index.md +10 -0
- package/rules/storybook/scaffold/template/docs/main.md +30 -0
- package/rules/storybook/scaffold/template/docs/preview.md +46 -0
- package/rules/storybook/scaffold/template/main.js +45 -0
- package/rules/storybook/scaffold/template/mocks/docs/gql-sse.md +36 -0
- package/rules/storybook/scaffold/template/mocks/docs/index.md +9 -0
- package/rules/storybook/scaffold/template/mocks/gql-sse.js +25 -0
- package/rules/storybook/scaffold/template/preview.js +47 -0
- package/rules/storybook/scope/concern.json +4 -0
- package/rules/storybook/scope/docs/index.md +9 -0
- package/rules/storybook/scope/docs/main.md +62 -0
- package/rules/storybook/scope/main.mjs +202 -0
- package/rules/storybook/vitest-config/concern.json +8 -0
- package/rules/storybook/vitest-config/docs/fix-vitest-config.md +38 -0
- package/rules/storybook/vitest-config/docs/index.md +10 -0
- package/rules/storybook/vitest-config/docs/main.md +72 -0
- package/rules/storybook/vitest-config/fix-vitest-config.mjs +340 -0
- package/rules/storybook/vitest-config/main.mjs +358 -0
- package/rules/storybook/vitest-config/template/docs/index.md +12 -0
- package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +34 -0
- package/rules/storybook/vitest-config/template/docs/unit-project-entry.md +30 -0
- package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +29 -0
- package/rules/storybook/vitest-config/template/docs/vitest.stryker.config.baseline.md +31 -0
- package/rules/storybook/vitest-config/template/storybook-project-entry.js +22 -0
- package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
- package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +37 -0
- package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
- package/rules/storybook/vitest-config/vitest-config.mdc +33 -0
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Логіка, що ЛИШАЄТЬСЯ у rego (inverse-patterns, не виносяться у template):
|
|
8
8
|
# - форма поля `types` (regex pattern: `./types/index.d.ts` або `./types/<…>.d.ts|.d.mts`);
|
|
9
|
-
# - `devDependencies` мають бути
|
|
9
|
+
# - `devDependencies` мають бути відсутні/порожні АБО належати канонічному
|
|
10
|
+
# Storybook-allowlist із зафіксованою точною версією (канон Storybook, кластер 7
|
|
11
|
+
# Governance: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`).
|
|
12
|
+
# Storybook-devDeps живуть саме у `npm/package.json` консюмер-пакета
|
|
13
|
+
# (а не в кореневому package.json), бо майбутній `isStorybookRoot()` у
|
|
14
|
+
# `@7n/test` читає саме цей файл, щоб визначити Storybook-скоуп пакета.
|
|
15
|
+
# Канон — static map (той самий підхід, що й `allowed_root_test_deps` у
|
|
16
|
+
# `bun.package_json`), НЕ template: це не mandatory-presence дані (більшість
|
|
17
|
+
# npm-пакетів Storybook не має), тож генеричний T0-fix-writer цього
|
|
18
|
+
# concern-а (`createTemplateFixPattern`, deep-merge усього `template.snippet`
|
|
19
|
+
# у target) канонічні devDeps у кожен package.json не домерджує.
|
|
10
20
|
#
|
|
11
21
|
# FS-перевірки (наявність файлу зі шляху `types`, скан tarball на тест-патерни) — у JS.
|
|
12
22
|
package npm_module.npm_package_json
|
|
@@ -20,10 +30,27 @@ types_field_template := concat(" ", [
|
|
|
20
30
|
|
|
21
31
|
dev_deps_template := concat(" ", [
|
|
22
32
|
"npm/package.json: \"devDependencies\" не публікуються користувачам пакета —",
|
|
33
|
+
"дозволені лише канонічні Storybook-пакети (isStorybookRoot(), канон Storybook);",
|
|
23
34
|
"dev-інструментарій перенеси у кореневий package.json, а CLI-тули, які пакет",
|
|
24
35
|
"спавнить через bunx у споживачів, — у \"dependencies\": %v (npm-module.mdc)",
|
|
25
36
|
])
|
|
26
37
|
|
|
38
|
+
storybook_version_template := concat(" ", [
|
|
39
|
+
"npm/package.json: devDependencies.%v = %q не відповідає зафіксованій версії",
|
|
40
|
+
"Storybook-канону %q — вирівняй версію пакета до канону (npm-module.mdc, канон Storybook)",
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
# Канонічні Storybook-devDeps (isStorybookRoot()-маркери, канон Storybook кластер 7):
|
|
44
|
+
# зафіксована точна версія — єдина дозволена версія для кожного пакета. Оновлення —
|
|
45
|
+
# ручна правка цієї map.
|
|
46
|
+
storybook_canon_dev_deps := {
|
|
47
|
+
"storybook": "9.1.10",
|
|
48
|
+
"@storybook/vue3-vite": "9.1.10",
|
|
49
|
+
"@storybook/vue3": "9.1.10",
|
|
50
|
+
"msw": "2.11.3",
|
|
51
|
+
"msw-storybook-addon": "2.0.5",
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
# ── deny: types (regex — лишається в rego) ───────────────────────────────
|
|
28
55
|
|
|
29
56
|
deny contains msg if {
|
|
@@ -57,13 +84,23 @@ deny contains msg if {
|
|
|
57
84
|
msg := sprintf("npm/package.json: масив \"%s\" має містити %q (npm-module.mdc)", [field, required])
|
|
58
85
|
}
|
|
59
86
|
|
|
60
|
-
# ── deny: devDependencies (inverse-pattern
|
|
87
|
+
# ── deny: devDependencies (inverse-pattern + Storybook-allowlist виняток) ─
|
|
88
|
+
|
|
89
|
+
deny contains msg if {
|
|
90
|
+
dev := object.get(input, "devDependencies", {})
|
|
91
|
+
forbidden_names := [n | some n, _ in dev; not n in object.keys(storybook_canon_dev_deps)]
|
|
92
|
+
count(forbidden_names) > 0
|
|
93
|
+
msg := sprintf(dev_deps_template, [concat(", ", sort(forbidden_names))])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ── deny: Storybook-devDep присутній, але версія розходиться з каноном ──
|
|
61
97
|
|
|
62
98
|
deny contains msg if {
|
|
63
99
|
dev := object.get(input, "devDependencies", {})
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
some name, version in dev
|
|
101
|
+
canonical := storybook_canon_dev_deps[name]
|
|
102
|
+
version != canonical
|
|
103
|
+
msg := sprintf(storybook_version_template, [name, version, canonical])
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/adopt/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 640409c7
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 90
|
|
10
|
+
issues: anchor-miss:(mocking.mdc),anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Режим adopt для канону Storybook у Vue-бібліотеках: звіряє пакети з канонічними секціями `main.js`, `preview.js`, `mocks/gql-sse.js`, `package.json#scripts.storybook`, `vitest test.projects` і `vitest.stryker.config`, а для вже наявних ручних `.storybook/` без сліпого перезапису показує діагностику diff по секціях. Автофікс (`--fix-missing`) додає лише повністю відсутні секції, з тим самим рендером, що й T0-фікс concern-ів `scaffold`/`vitest-config`, без втручання в наявні розбіжні файли. Дає CLI-звіт по кожному пакету зі станом `status` і секціями `SECTION`, окремо фіксуючи `diagnosePackage`, `fixMissingSections`, `runAdopt` і `formatReport`; помилки перехоплює fail-safe, тому збій або діагностики, або фіксу для одного пакета переводить лише його в `status: 'broken'`, а решта пакетів продовжують оброблятися.
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- `STATUS` — задає фіксовані стани діагностики секцій Storybook: збіг, розбіжність або відсутність.
|
|
20
|
+
- `SECTION` — задає стабільні назви секцій звіту, щоб adopt-перевірка й `--fix-missing` узгоджено посилалися на ті самі частини конфігурації.
|
|
21
|
+
- `diagnosePackage` — перевіряє пакет по секціях і повертає підсумок: канонічний, з відсутніми файлами або з відмінностями; помилки окремих секцій не валять увесь пакет.
|
|
22
|
+
- `fixMissingSections` — створює лише ті канонічні секції, яких у пакеті немає взагалі, і не перезаписує наявні розбіжні файли.
|
|
23
|
+
- `runAdopt` — проганяє adopt-перевірку по всіх або вибраних пакетах у скоупі, із fail-safe поведінкою: збій одного пакета переходить у `broken`, але інші обробляються далі.
|
|
24
|
+
- `formatReport` — перетворює результати adopt-прогону на лаконічний український звіт для CLI, включно з переліком згенерованого або повідомленням про відсутність пакетів у скоупі.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- STATUS — Статуси однієї секції діагностики (діагностика ≠ lint-violation — тут завжди 4 значення).
|
|
29
|
+
- SECTION — Канонічні назви секцій (стабільні — на них зав'язаний `--fix-missing`-switch).
|
|
30
|
+
- diagnosePackage — Діагностика одного пакета в скоупі по секціях. Ніколи не кидає — збій окремої
|
|
31
|
+
секції (парсинг/IO) відображається як секція `differ`, а не виняток, що впав
|
|
32
|
+
би на весь пакет; лишається на розсуд `diagnosePackage` (circuit breaker рівня
|
|
33
|
+
пакета — тут не потрібен, бо секції вже ізольовані одна від одної).
|
|
34
|
+
- fixMissingSections — Генерує канонічний вміст лише для секцій зі статусом `missing` одного пакета
|
|
35
|
+
(adopt-автофікс НІКОЛИ не чіпає секції зі статусом `differ` — інструкція
|
|
36
|
+
для агента/людини, не сліпий перезапис). Кожна секція фіксується незалежно;
|
|
37
|
+
збій однієї не блокує решту (той самий circuit-breaker принцип, лише на дрібнішому рівні).
|
|
38
|
+
- runAdopt — Adopt-прогін усіх (чи обраних) пакетів у скоупі. Circuit breaker (ADR Кластер 8):
|
|
39
|
+
збій діагностики/фіксу ОДНОГО пакета деградує до `status: 'broken'` для нього —
|
|
40
|
+
решта пакетів обробляються далі, весь прогін ніколи не падає через один зламаний.
|
|
41
|
+
відсутні секції; `rootDirs` — звузити прогін до цих коренів пакетів (порожньо/відсутнє → усі в скоупі)
|
|
42
|
+
- formatReport — Форматує людський звіт по результатах `runAdopt` (українською, для виводу скіла в CLI).
|
|
43
|
+
|
|
44
|
+
## Гарантії поведінки
|
|
45
|
+
|
|
46
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopt-режим канону Storybook (ADR канон-storybook-для-vue-компонентних-бібліотек,
|
|
3
|
+
* Кластер 8; main.mdc). Для пакетів, де ВЖЕ є ручний `.storybook/`, що не збігається з
|
|
4
|
+
* каноном, — діагностика diff по секціях проти канонічних `template/` (main.js,
|
|
5
|
+
* preview.js, mocks/gql-sse.js, package.json#scripts.storybook, vitest test.projects,
|
|
6
|
+
* vitest.stryker.config), БЕЗ сліпого перезапису розбіжних файлів. Автофікс
|
|
7
|
+
* (`--fix-missing`) — лише для секцій, яких немає ВЗАГАЛІ (той самий рендер, що й
|
|
8
|
+
* T0-фікс concern-ів `scaffold`/`vitest-config` — переюз, не дублювання шаблонування).
|
|
9
|
+
*
|
|
10
|
+
* Circuit breaker (ADR): збій діагностики чи фіксу одного пакета деградує до
|
|
11
|
+
* `status: 'broken'` для ЦЬОГО пакета — решта пакетів прогону обробляються далі,
|
|
12
|
+
* увесь прогін ніколи не падає через один зламаний пакет.
|
|
13
|
+
*
|
|
14
|
+
* Викликається зі скіла (`npm/skills/storybook/SKILL.md`, `--adopt`):
|
|
15
|
+
* bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs [--fix-missing] [--cwd <path>] [rootDir...]
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
import { isRunAsCli } from '@7n/rules/scripts/cli-entry.mjs'
|
|
21
|
+
|
|
22
|
+
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
23
|
+
import { MAIN_JS_MARKERS, missingMarkers, PREVIEW_JS_MARKERS, STORYBOOK_SCRIPT } from '../scaffold/main.mjs'
|
|
24
|
+
import { renderMainJs, renderMocksGqlSse, renderPreviewJs } from '../scaffold/fix-scaffold.mjs'
|
|
25
|
+
import { buildFreshVitestConfig, buildStrykerConfig } from '../vitest-config/fix-vitest-config.mjs'
|
|
26
|
+
import {
|
|
27
|
+
BROWSER_KEY_RE,
|
|
28
|
+
CHROMIUM_RE,
|
|
29
|
+
classifyProjects,
|
|
30
|
+
findProperty,
|
|
31
|
+
findTestObject,
|
|
32
|
+
parseModule,
|
|
33
|
+
resolveVitestConfigPath,
|
|
34
|
+
STORIES_RE,
|
|
35
|
+
strykerConfigPathFor
|
|
36
|
+
} from '../vitest-config/main.mjs'
|
|
37
|
+
|
|
38
|
+
/** Статуси однієї секції діагностики (діагностика ≠ lint-violation — тут завжди 4 значення). */
|
|
39
|
+
export const STATUS = Object.freeze({ MATCH: 'match', DIFFER: 'differ', MISSING: 'missing' })
|
|
40
|
+
|
|
41
|
+
/** Канонічні назви секцій (стабільні — на них зав'язаний `--fix-missing`-switch). */
|
|
42
|
+
export const SECTION = Object.freeze({
|
|
43
|
+
MAIN_JS: 'main.js',
|
|
44
|
+
PREVIEW_JS: 'preview.js',
|
|
45
|
+
MOCKS_GQL_SSE: 'mocks/gql-sse.js',
|
|
46
|
+
PACKAGE_SCRIPT: 'package.json#scripts.storybook',
|
|
47
|
+
VITEST_PROJECTS: 'vitest test.projects (unit+storybook)',
|
|
48
|
+
STRYKER_CONFIG: 'vitest.stryker.config'
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* posix-relative шлях файлу пакета від кореня репозиторію (для звіту/сообщений).
|
|
53
|
+
* @param {{ rootDir: string }} entry запис пакета
|
|
54
|
+
* @param {string} suffix шлях файлу відносно кореня пакета
|
|
55
|
+
* @returns {string} відносний шлях від кореня репозиторію
|
|
56
|
+
*/
|
|
57
|
+
function relFileFor(entry, suffix) {
|
|
58
|
+
return entry.rootDir === '.' ? suffix : `${entry.rootDir}/${suffix}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Діагностика файлу, канон якого перевіряється текстовими маркерами (main.js/preview.js).
|
|
63
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
64
|
+
* @param {string} relPath шлях файлу відносно кореня пакета (`.storybook/main.js`)
|
|
65
|
+
* @param {string} sectionName ім'я секції звіту
|
|
66
|
+
* @param {{ token: string, hint: string }[]} markers канонічні маркери файлу
|
|
67
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} одна секція діагностики
|
|
68
|
+
*/
|
|
69
|
+
function diagnoseMarkerFile(entry, relPath, sectionName, markers) {
|
|
70
|
+
const file = relFileFor(entry, relPath)
|
|
71
|
+
const abs = join(entry.absDir, relPath)
|
|
72
|
+
if (!existsSync(abs)) return { name: sectionName, file, status: STATUS.MISSING }
|
|
73
|
+
const content = readFileSync(abs, 'utf8')
|
|
74
|
+
const missing = missingMarkers(content, markers)
|
|
75
|
+
if (missing.length === 0) return { name: sectionName, file, status: STATUS.MATCH }
|
|
76
|
+
return {
|
|
77
|
+
name: sectionName,
|
|
78
|
+
file,
|
|
79
|
+
status: STATUS.DIFFER,
|
|
80
|
+
detail: `бракує: ${missing.map(m => m.hint).join(', ')}`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Діагностика `.storybook/mocks/gql-sse.js` — verbatim-порівняння з канонічним helper-ом
|
|
86
|
+
* (одне джерело істини протоколу graphql-sse, mocking.mdc — не переносити копію в пакет).
|
|
87
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
88
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
89
|
+
*/
|
|
90
|
+
function diagnoseMocksSection(entry) {
|
|
91
|
+
const relPath = '.storybook/mocks/gql-sse.js'
|
|
92
|
+
const file = relFileFor(entry, relPath)
|
|
93
|
+
const abs = join(entry.absDir, relPath)
|
|
94
|
+
if (!existsSync(abs)) return { name: SECTION.MOCKS_GQL_SSE, file, status: STATUS.MISSING }
|
|
95
|
+
const actual = readFileSync(abs, 'utf8')
|
|
96
|
+
const canonical = renderMocksGqlSse()
|
|
97
|
+
if (actual === canonical) return { name: SECTION.MOCKS_GQL_SSE, file, status: STATUS.MATCH }
|
|
98
|
+
return {
|
|
99
|
+
name: SECTION.MOCKS_GQL_SSE,
|
|
100
|
+
file,
|
|
101
|
+
status: STATUS.DIFFER,
|
|
102
|
+
detail: 'вміст відрізняється від канонічного helper-а (mocking.mdc) — одне джерело істини, не дублюй логіку вручну'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Діагностика `package.json#scripts.storybook`.
|
|
108
|
+
* @param {{ pkg: Record<string, unknown>, rootDir: string }} entry запис пакета
|
|
109
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
110
|
+
*/
|
|
111
|
+
function diagnoseScriptSection(entry) {
|
|
112
|
+
const file = relFileFor(entry, 'package.json')
|
|
113
|
+
const current = /** @type {{ scripts?: Record<string, unknown> }} */ (entry.pkg)?.scripts?.storybook
|
|
114
|
+
if (current === undefined) return { name: SECTION.PACKAGE_SCRIPT, file, status: STATUS.MISSING }
|
|
115
|
+
if (current === STORYBOOK_SCRIPT) return { name: SECTION.PACKAGE_SCRIPT, file, status: STATUS.MATCH }
|
|
116
|
+
return {
|
|
117
|
+
name: SECTION.PACKAGE_SCRIPT,
|
|
118
|
+
file,
|
|
119
|
+
status: STATUS.DIFFER,
|
|
120
|
+
detail: `зараз '${current}', канон '${STORYBOOK_SCRIPT}'`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Діагностика `test.projects` наявного vitest-конфіга (unit+storybook, browser-mode маркери).
|
|
126
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
127
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
128
|
+
*/
|
|
129
|
+
function diagnoseVitestProjectsSection(entry) {
|
|
130
|
+
const name = SECTION.VITEST_PROJECTS
|
|
131
|
+
const vitestConfigPath = resolveVitestConfigPath(entry.absDir)
|
|
132
|
+
if (!vitestConfigPath) {
|
|
133
|
+
return { name, file: relFileFor(entry, 'vitest.config.mjs'), status: STATUS.MISSING }
|
|
134
|
+
}
|
|
135
|
+
const file = relFileFor(entry, vitestConfigPath.slice(entry.absDir.length + 1))
|
|
136
|
+
const src = readFileSync(vitestConfigPath, 'utf8')
|
|
137
|
+
|
|
138
|
+
let parsed
|
|
139
|
+
try {
|
|
140
|
+
parsed = parseModule(vitestConfigPath, src)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return { name, file, status: STATUS.DIFFER, detail: `не парситься (${error.message}) — перевір вручну` }
|
|
143
|
+
}
|
|
144
|
+
if (parsed.errors?.length) {
|
|
145
|
+
return { name, file, status: STATUS.DIFFER, detail: 'syntax error — перевір вручну' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const testObj = findTestObject(parsed.program)
|
|
149
|
+
if (!testObj) {
|
|
150
|
+
return { name, file, status: STATUS.DIFFER, detail: 'немає test-блоку (defineConfig({ test: {...} }))' }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const projectsProp = findProperty(testObj, 'projects')
|
|
154
|
+
if (!projectsProp) {
|
|
155
|
+
return { name, file, status: STATUS.MISSING, detail: 'test.projects відсутній' }
|
|
156
|
+
}
|
|
157
|
+
if (projectsProp.value?.type !== 'ArrayExpression') {
|
|
158
|
+
return { name, file, status: STATUS.DIFFER, detail: 'test.projects не статичний масив (spread/змінна)' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { hasUnit, storybookSlice } = classifyProjects(src, projectsProp.value)
|
|
162
|
+
if (!hasUnit || !storybookSlice) {
|
|
163
|
+
const missingParts = [hasUnit ? null : "'unit'", storybookSlice ? null : "'storybook'"].filter(Boolean)
|
|
164
|
+
return { name, file, status: STATUS.DIFFER, detail: `бракує ${missingParts.join(' і ')} у test.projects` }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const missingHints = []
|
|
168
|
+
if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
|
|
169
|
+
if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
|
|
170
|
+
if (!STORIES_RE.test(storybookSlice)) missingHints.push('stories-glob')
|
|
171
|
+
if (missingHints.length > 0) {
|
|
172
|
+
return { name, file, status: STATUS.DIFFER, detail: `storybook-project без: ${missingHints.join(', ')}` }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { name, file, status: STATUS.MATCH }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Діагностика ізольованого `vitest.stryker.config.*` — байтове порівняння з канонічним
|
|
180
|
+
* baseline-рендером (fix-vitest-config.mjs, той самий генератор).
|
|
181
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
182
|
+
* @returns {Promise<{ name: string, file: string, status: string, detail?: string }>} секція діагностики
|
|
183
|
+
*/
|
|
184
|
+
async function diagnoseStrykerSection(entry) {
|
|
185
|
+
const name = SECTION.STRYKER_CONFIG
|
|
186
|
+
const vitestConfigPath = resolveVitestConfigPath(entry.absDir)
|
|
187
|
+
if (!vitestConfigPath) {
|
|
188
|
+
return { name, file: relFileFor(entry, 'vitest.stryker.config.mjs'), status: STATUS.MISSING }
|
|
189
|
+
}
|
|
190
|
+
const strykerPath = strykerConfigPathFor(vitestConfigPath)
|
|
191
|
+
const file = relFileFor(entry, strykerPath.slice(entry.absDir.length + 1))
|
|
192
|
+
if (!existsSync(strykerPath)) {
|
|
193
|
+
return { name, file, status: STATUS.MISSING }
|
|
194
|
+
}
|
|
195
|
+
const actual = readFileSync(strykerPath, 'utf8')
|
|
196
|
+
const canonical = await buildStrykerConfig(entry.absDir)
|
|
197
|
+
if (actual === canonical) return { name, file, status: STATUS.MATCH }
|
|
198
|
+
return {
|
|
199
|
+
name,
|
|
200
|
+
file,
|
|
201
|
+
status: STATUS.DIFFER,
|
|
202
|
+
detail:
|
|
203
|
+
'вміст відрізняється від канонічного baseline (vitest-config.mdc) — @stryker-mutator/vitest-runner крашиться на browser-mode projects, перевір вручну'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Діагностика одного пакета в скоупі по секціях. Ніколи не кидає — збій окремої
|
|
209
|
+
* секції (парсинг/IO) відображається як секція `differ`, а не виняток, що впав
|
|
210
|
+
* би на весь пакет; лишається на розсуд `diagnosePackage` (circuit breaker рівня
|
|
211
|
+
* пакета — тут не потрібен, бо секції вже ізольовані одна від одної).
|
|
212
|
+
* @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
|
|
213
|
+
* @returns {Promise<{ rootDir: string, status: 'canonical'|'missing-files'|'differs', sections: object[] }>} діагностика пакета
|
|
214
|
+
*/
|
|
215
|
+
export async function diagnosePackage(entry) {
|
|
216
|
+
const sections = [
|
|
217
|
+
diagnoseMarkerFile(entry, '.storybook/main.js', SECTION.MAIN_JS, MAIN_JS_MARKERS),
|
|
218
|
+
diagnoseMarkerFile(entry, '.storybook/preview.js', SECTION.PREVIEW_JS, PREVIEW_JS_MARKERS),
|
|
219
|
+
diagnoseMocksSection(entry),
|
|
220
|
+
diagnoseScriptSection(entry),
|
|
221
|
+
diagnoseVitestProjectsSection(entry),
|
|
222
|
+
await diagnoseStrykerSection(entry)
|
|
223
|
+
]
|
|
224
|
+
let status = 'canonical'
|
|
225
|
+
if (sections.some(s => s.status === STATUS.DIFFER)) status = 'differs'
|
|
226
|
+
else if (sections.some(s => s.status === STATUS.MISSING)) status = 'missing-files'
|
|
227
|
+
return { rootDir: entry.rootDir, status, sections }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Генерує канонічний вміст лише для секцій зі статусом `missing` одного пакета
|
|
232
|
+
* (adopt-автофікс НІКОЛИ не чіпає секції зі статусом `differ` — інструкція
|
|
233
|
+
* для агента/людини, не сліпий перезапис). Кожна секція фіксується незалежно;
|
|
234
|
+
* збій однієї не блокує решту (той самий circuit-breaker принцип, лише на дрібнішому рівні).
|
|
235
|
+
* @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
|
|
236
|
+
* @param {object[]} sections секції діагностики пакета (`diagnosePackage().sections`)
|
|
237
|
+
* @returns {Promise<string[]>} абсолютні шляхи записаних файлів
|
|
238
|
+
*/
|
|
239
|
+
export async function fixMissingSections(entry, sections) {
|
|
240
|
+
const written = []
|
|
241
|
+
const writeFileEnsureDir = (absPath, content) => {
|
|
242
|
+
mkdirSync(dirname(absPath), { recursive: true })
|
|
243
|
+
writeFileSync(absPath, content, 'utf8')
|
|
244
|
+
written.push(absPath)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const byName = new Map(sections.map(s => [s.name, s]))
|
|
248
|
+
|
|
249
|
+
if (byName.get(SECTION.MAIN_JS)?.status === STATUS.MISSING) {
|
|
250
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/main.js'), renderMainJs(entry.absDir))
|
|
251
|
+
}
|
|
252
|
+
if (byName.get(SECTION.PREVIEW_JS)?.status === STATUS.MISSING) {
|
|
253
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/preview.js'), renderPreviewJs())
|
|
254
|
+
}
|
|
255
|
+
if (byName.get(SECTION.MOCKS_GQL_SSE)?.status === STATUS.MISSING) {
|
|
256
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/mocks/gql-sse.js'), renderMocksGqlSse())
|
|
257
|
+
}
|
|
258
|
+
if (byName.get(SECTION.PACKAGE_SCRIPT)?.status === STATUS.MISSING) {
|
|
259
|
+
const pkgPath = join(entry.absDir, 'package.json')
|
|
260
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
261
|
+
pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
262
|
+
pkg.scripts.storybook = STORYBOOK_SCRIPT
|
|
263
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
264
|
+
written.push(pkgPath)
|
|
265
|
+
}
|
|
266
|
+
if (byName.get(SECTION.VITEST_PROJECTS)?.status === STATUS.MISSING && !resolveVitestConfigPath(entry.absDir)) {
|
|
267
|
+
// test.projects відсутній ЛИШЕ через відсутність усього vitest-конфіга — augment
|
|
268
|
+
// наявного (без відсутнього test-блоку) свідомо поза adopt-автофіксом: секція вже
|
|
269
|
+
// позначена `differ` (не `missing`) для такого випадку, сюди він не потрапляє.
|
|
270
|
+
const fresh = await buildFreshVitestConfig(entry.absDir)
|
|
271
|
+
writeFileEnsureDir(fresh.path, fresh.content)
|
|
272
|
+
}
|
|
273
|
+
if (byName.get(SECTION.STRYKER_CONFIG)?.status === STATUS.MISSING) {
|
|
274
|
+
const vitestConfigPath = resolveVitestConfigPath(entry.absDir) ?? join(entry.absDir, 'vitest.config.mjs')
|
|
275
|
+
const strykerPath = strykerConfigPathFor(vitestConfigPath)
|
|
276
|
+
writeFileEnsureDir(strykerPath, await buildStrykerConfig(entry.absDir))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return written
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Adopt-прогін усіх (чи обраних) пакетів у скоупі. Circuit breaker (ADR Кластер 8):
|
|
284
|
+
* збій діагностики/фіксу ОДНОГО пакета деградує до `status: 'broken'` для нього —
|
|
285
|
+
* решта пакетів обробляються далі, весь прогін ніколи не падає через один зламаний.
|
|
286
|
+
* @param {string} cwd абсолютний корінь консюмер-репо
|
|
287
|
+
* @param {{ fixMissing?: boolean, rootDirs?: string[] }} [opts] `fixMissing` — генерувати
|
|
288
|
+
* відсутні секції; `rootDirs` — звузити прогін до цих коренів пакетів (порожньо/відсутнє → усі в скоупі)
|
|
289
|
+
* @returns {Promise<{ rootDir: string, status: string, sections?: object[], written?: string[], error?: string }[]>} результати за пакетами
|
|
290
|
+
*/
|
|
291
|
+
export async function runAdopt(cwd, opts = {}) {
|
|
292
|
+
const fixMissing = opts.fixMissing === true
|
|
293
|
+
const wantedRoots = Array.isArray(opts.rootDirs) && opts.rootDirs.length > 0 ? new Set(opts.rootDirs) : null
|
|
294
|
+
|
|
295
|
+
const allPkgs = await collectInScopeVuePackages(cwd)
|
|
296
|
+
const pkgs = wantedRoots ? allPkgs.filter(p => wantedRoots.has(p.rootDir)) : allPkgs
|
|
297
|
+
|
|
298
|
+
const results = []
|
|
299
|
+
for (const entry of pkgs) {
|
|
300
|
+
try {
|
|
301
|
+
const diagnosis = await diagnosePackage(entry)
|
|
302
|
+
const written = fixMissing ? await fixMissingSections(entry, diagnosis.sections) : []
|
|
303
|
+
results.push({ ...diagnosis, written })
|
|
304
|
+
} catch (error) {
|
|
305
|
+
results.push({
|
|
306
|
+
rootDir: entry.rootDir,
|
|
307
|
+
status: 'broken',
|
|
308
|
+
error: error instanceof Error ? error.message : String(error)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return results
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Іконка статусу пакета (`package.status`) для рядка звіту. */
|
|
316
|
+
const PACKAGE_STATUS_ICONS = Object.freeze({ canonical: '✅', 'missing-files': '➕' })
|
|
317
|
+
/** Іконка статусу секції (`STATUS.*`) для рядка звіту. */
|
|
318
|
+
const SECTION_STATUS_ICONS = Object.freeze({ [STATUS.MATCH]: ' ✓', [STATUS.MISSING]: ' +' })
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Рядки звіту одного НЕ зламаного пакета (статус + секції + перелік згенерованого).
|
|
322
|
+
* @param {{ rootDir: string, status: string, sections: object[], written?: string[] }} r результат одного пакета
|
|
323
|
+
* @returns {string[]} рядки звіту
|
|
324
|
+
*/
|
|
325
|
+
function formatPackageLines(r) {
|
|
326
|
+
const label = r.rootDir === '.' ? 'корінь' : r.rootDir
|
|
327
|
+
const lines = [`${PACKAGE_STATUS_ICONS[r.status] ?? '⚠️ '} [${label}] ${r.status}`]
|
|
328
|
+
for (const s of r.sections) {
|
|
329
|
+
const sIcon = SECTION_STATUS_ICONS[s.status] ?? ' ✗'
|
|
330
|
+
const suffix = s.detail ? ` — ${s.detail}` : ''
|
|
331
|
+
lines.push(`${sIcon} ${s.name} (${s.file}): ${s.status}${suffix}`)
|
|
332
|
+
}
|
|
333
|
+
if (r.written && r.written.length > 0) {
|
|
334
|
+
lines.push(` згенеровано: ${r.written.join(', ')}`)
|
|
335
|
+
}
|
|
336
|
+
return lines
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Форматує людський звіт по результатах `runAdopt` (українською, для виводу скіла в CLI).
|
|
341
|
+
* @param {Awaited<ReturnType<typeof runAdopt>>} results результати `runAdopt`
|
|
342
|
+
* @returns {string} багаторядковий текстовий звіт
|
|
343
|
+
*/
|
|
344
|
+
export function formatReport(results) {
|
|
345
|
+
if (results.length === 0) {
|
|
346
|
+
return 'storybook adopt: немає Vue component library пакетів у скоупі (storybook.mdc → scope/main.mjs)'
|
|
347
|
+
}
|
|
348
|
+
const lines = []
|
|
349
|
+
for (const r of results) {
|
|
350
|
+
if (r.status === 'broken') {
|
|
351
|
+
const label = r.rootDir === '.' ? 'корінь' : r.rootDir
|
|
352
|
+
lines.push(`⚠️ [${label}] діагностика впала (circuit breaker, пропущено): ${r.error}`)
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
lines.push(...formatPackageLines(r))
|
|
356
|
+
}
|
|
357
|
+
return lines.join('\n')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* CLI-вхід: `bun .../storybook/adopt/main.mjs [--fix-missing] [--cwd <path>] [rootDir...]`.
|
|
362
|
+
* @returns {Promise<void>} завершення прогону (друкує звіт, exit-код — кількість `differs`/`broken`)
|
|
363
|
+
*/
|
|
364
|
+
async function runCli() {
|
|
365
|
+
const args = process.argv.slice(2)
|
|
366
|
+
const fixMissing = args.includes('--fix-missing')
|
|
367
|
+
const cwdIdx = args.indexOf('--cwd')
|
|
368
|
+
const cwd = cwdIdx !== -1 && args[cwdIdx + 1] ? args[cwdIdx + 1] : process.cwd()
|
|
369
|
+
const rootDirs = args.filter((a, i) => !a.startsWith('--') && args[i - 1] !== '--cwd')
|
|
370
|
+
|
|
371
|
+
const results = await runAdopt(cwd, { fixMissing, rootDirs })
|
|
372
|
+
console.log(formatReport(results))
|
|
373
|
+
const hasIssues = results.some(r => r.status === 'differs' || r.status === 'broken')
|
|
374
|
+
process.exitCode = hasIssues ? 1 : 0
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isRunAsCli(import.meta.url)) {
|
|
378
|
+
await runCli()
|
|
379
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/hygiene/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 9e36e1a2
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 85
|
|
10
|
+
issues: internal-name:collectInScopeVuePackages,anchor-miss:(storybook.mdc),judge:inaccurate:0.99
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл описує read-only lint-перевірку `lint` для Storybook-правил, що спираються на `package.json`. Перевірка потрібна, щоб виявляти порушення конфігурації без змін у файловій системі чи БД; помилки перехоплюються fail-safe і не виходять назовні як винятки.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. `lint` визначає Vue component library пакети, для яких діє Storybook-канон. Якщо таких пакетів немає, повертає успішний результат із повідомленням.
|
|
21
|
+
|
|
22
|
+
2. Для кожного знайденого пакета читає його `package.json` і формує перелік дозволених third-party залежностей із `dependencies` та `peerDependencies`.
|
|
23
|
+
|
|
24
|
+
3. Переглядає `.vue` файли пакета з урахуванням ignore-конфігурації та знаходить імпорти сторонніх пакетів. Відносні шляхи, alias-шляхи, Node builtin модулі та auto-import глобали не вважаються порушеннями.
|
|
25
|
+
|
|
26
|
+
4. Якщо `.vue` файл імпортує сторонній пакет, якого немає в `package.json` поточного пакета, додає порушення `undeclared-import`. Для одного файлу один і той самий пакет повідомляється лише один раз.
|
|
27
|
+
|
|
28
|
+
5. Перевіряє, чи має пакет глобальні Quasar SCSS-змінні. Якщо вони є, але `.storybook/main.js` не вмикає їх для Storybook, додає попередження `missing-sass-variables`.
|
|
29
|
+
|
|
30
|
+
6. Не змінює файлову систему або зовнішній стан: перевірка працює read-only і лише повертає результат лінту.
|
|
31
|
+
|
|
32
|
+
7. Некоректні або нерозбірні файли не зупиняють перевірку назовні: такі випадки обробляються fail-safe, щоб лінт міг продовжити роботу.
|
|
33
|
+
|
|
34
|
+
## Публічний API
|
|
35
|
+
|
|
36
|
+
- lint — Detector concern-а `storybook/hygiene`: для кожного Vue component library пакета у скоупі
|
|
37
|
+
канону Storybook (`collectInScopeVuePackages`) — undeclared third-party imports у `.vue` та
|
|
38
|
+
auto-detect глобальних Quasar SCSS-змінних без `sassVariables` у `.storybook/main.js`
|
|
39
|
+
(storybook.mdc, ADR Кластер 6). Breaking-change guard при мажорному апгрейді
|
|
40
|
+
third-party-пакетів свідомо не автоматизується — людський пункт, hygiene.mdc.
|
|
41
|
+
|
|
42
|
+
## Гарантії поведінки
|
|
43
|
+
|
|
44
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
45
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|