@7n/rules-lang-js 0.9.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 +13 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- storybook: канон Storybook хвилі 1 для Vue-компонентних бібліотек — детекція скоупу (isVueComponentLibraryPkg, поріг ≥3 .vue, opt-out), канонічний скафолд .storybook/main.js+preview.js+mocks/gql-sse.js, package.json#scripts.storybook (ADR канон-storybook-для-vue-компонентних-бібліотек)
|
|
8
|
+
- npm-module/bun: governance-виняток канону Storybook (кластер 7 ADR канон-storybook-для-vue-компонентних-бібліотек) — npm_package_json.rego дозволяє канонічні Storybook-devDeps (storybook, @storybook/vue3-vite, @storybook/vue3, msw, msw-storybook-addon) у npm/package.json із зафіксованою точною версією (deny на неканонічний пакет або неканонічну версію); bun/package_json.rego розширює root-only test peers на @vitest/browser + playwright (browser-mode provider для named vitest project "storybook", лише chromium) та @storybook/addon-vitest (storybookTest-плагін того самого vitest-конфіга) — Storybook-identity-пакети у корінь свідомо не додаються
|
|
9
|
+
- storybook: vitest-config-концерн хвилі 1 (ADR Кластер 5) — canonical test.projects unit+storybook (browser-mode, лише chromium, stories-glob) дописується поверх наявного vitest-конфіга, ізольований vitest.stryker.config генерується поруч (Stryker крашиться на browser-mode projects)
|
|
10
|
+
- storybook: концерни mocking (docs-only рецепти router/tfm/Apollo-MSW/Pinia/page-story) і hygiene (undeclared third-party imports у .vue, auto-detect sassVariables) — ADR Кластер 3/6
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- storybook: підключено concern-и scope/scaffold/vitest-config до unified lint-рушія (lint-блок у concern.json — check:true без lint мовчки ігнорувався run-detectors.mjs), додано --adopt-режим (adopt/main.mjs) і скіл n-storybook
|
|
15
|
+
|
|
3
16
|
## [0.9.0] - 2026-07-20
|
|
4
17
|
|
|
5
18
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-lang-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Плагін @7n/rules: JS/npm/bun-екосистема — lint-правила (js/bun/vue/js-run/npm-module/db), taze-провайдер (package.json, bunx taze) і doc-files-екстрактори (oxc AST)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"javascript",
|
|
@@ -9,6 +9,8 @@ Rego-пакет: `bun.package_json`
|
|
|
9
9
|
Gate виносить два класи deny:
|
|
10
10
|
|
|
11
11
|
1. **Заборонені top-level поля** — будь-яке поле з `package.json.deny.json` присутнє у файлі (навіть із порожнім значенням `{}`).
|
|
12
|
-
2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`, `@playwright/test`). Будь-який інший пакет → deny. CLI-тули, які `n-rules lint` спавнить через `bunx` (oxlint, jscpd, v8r, github-actionlint тощо), у root не пінуються — вони приїжджають як `dependencies` пакета `@7n/rules` (npm-module.mdc).
|
|
12
|
+
2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@vitest/browser`, `@stryker-mutator/vitest-runner`, `@stryker-mutator/core`, `@playwright/test`, `playwright`, `@storybook/addon-vitest`, `@7n/test`). Будь-який інший пакет → deny. CLI-тули, які `n-rules lint` спавнить через `bunx` (oxlint, jscpd, v8r, github-actionlint тощо), у root не пінуються — вони приїжджають як `dependencies` пакета `@7n/rules` (npm-module.mdc).
|
|
13
|
+
|
|
14
|
+
`@vitest/browser`/`playwright`/`@storybook/addon-vitest` — додані для named vitest project "storybook" (канон Storybook, кластер 5: browser-mode, лише chromium) — той самий root vitest.config, що й `unit`-проект; `@storybook/addon-vitest` постачає `storybookTest`-плагін для цього vitest-конфіга (канонічний template правила `storybook`). **Межа з `npm-module.mdc`**: Storybook-специфічні identity-пакети (`storybook`, `@storybook/vue3-vite`, `@storybook/vue3`, `msw`, `msw-storybook-addon`) у цей allowlist **не** додаються — вони живуть у `npm/package.json` консюмер-пакета (канон Storybook, кластер 7 Governance), бо `isStorybookRoot()` у `@7n/test` детектує Storybook-скоуп саме за тим файлом, не кореневим. `@storybook/addon-vitest` — виняток із цієї межі: він test-tooling (vitest-плагін), а не identity-маркер, тож root, як і решта vitest-peer'ів.
|
|
13
15
|
|
|
14
16
|
Перевірки, що потребують FS або cross-file контексту (наприклад наявність `yarn.lock`), лишаються у JS-шарі.
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
# - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для `@7n/test coverage`
|
|
9
9
|
# (правило `test` enabled завжди — див. `test/auto.md`; published workspace-и не мають
|
|
10
10
|
# `devDependencies` за `npm-module.mdc`)
|
|
11
|
+
# - `@vitest/browser`/`playwright`/`@storybook/addon-vitest` (browser-mode provider +
|
|
12
|
+
# `storybookTest`-плагін для named vitest project "storybook", лише chromium — канон
|
|
13
|
+
# Storybook кластер 5) теж root-only test peers: той самий vitest.config, що й
|
|
14
|
+
# `unit`-проект, живе в корені монорепо-споживача. Storybook-специфічні
|
|
15
|
+
# identity-пакети (`storybook`, `@storybook/vue3*`, `msw*`) НЕ сюди — вони живуть у
|
|
16
|
+
# `npm/package.json` (канон Storybook кластер 7, `npm-module.mdc`), бо
|
|
17
|
+
# `isStorybookRoot()` @7n/test читає саме той файл, не кореневий package.json.
|
|
18
|
+
# `@storybook/addon-vitest` — виняток із цього правила: це test-tooling (плагін
|
|
19
|
+
# vitest-конфіга), а не Storybook-identity-маркер, тож root, а не npm/package.json.
|
|
11
20
|
#
|
|
12
21
|
# Перевірки, які потребують FS / cross-file контексту, лишаються у JS.
|
|
13
22
|
package bun.package_json
|
|
@@ -50,7 +59,27 @@ deny contains msg if {
|
|
|
50
59
|
# @stryker-mutator/core — обов'язковий exact-pin peer vitest-runner@9+ (раніше тягнувся транзитивно)
|
|
51
60
|
# @7n/test — оркестратор `coverage` (npx @7n/test coverage); devDependency, щоб npx резолвив
|
|
52
61
|
# локально без мережевого fetch щоразу.
|
|
53
|
-
|
|
62
|
+
# @vitest/browser + playwright — провайдер browser-mode для named vitest project
|
|
63
|
+
# "storybook" (канон Storybook кластер 5: лише chromium, PR — швидкий
|
|
64
|
+
# --project=storybook). `playwright` (не `@playwright/test`) — сирий driver, який
|
|
65
|
+
# @vitest/browser використовує як provider; `@playwright/test` лишається окремо для
|
|
66
|
+
# змістовних E2E-сценаріїв (n-vue.mdc).
|
|
67
|
+
# @storybook/addon-vitest — постачає `storybookTest` для vitest-плагіна в канонічному
|
|
68
|
+
# vitest.config named-проекту "storybook" (той самий канон Storybook кластер 5); версія
|
|
69
|
+
# з лінійки Storybook 9.x (узгоджена з `storybook`@9.1.10, запіненим у
|
|
70
|
+
# npm_package_json.rego) — allowlist тут за іменем, точний пінінг версії root-tooling
|
|
71
|
+
# не робимо (на відміну від Storybook-identity-пакетів у npm/package.json).
|
|
72
|
+
allowed_root_test_deps := {
|
|
73
|
+
"vitest",
|
|
74
|
+
"@vitest/coverage-v8",
|
|
75
|
+
"@vitest/browser",
|
|
76
|
+
"@stryker-mutator/vitest-runner",
|
|
77
|
+
"@stryker-mutator/core",
|
|
78
|
+
"@playwright/test",
|
|
79
|
+
"playwright",
|
|
80
|
+
"@storybook/addon-vitest",
|
|
81
|
+
"@7n/test",
|
|
82
|
+
}
|
|
54
83
|
|
|
55
84
|
allowed_root_dev_dependency(name) if {
|
|
56
85
|
startswith(name, "@nitra/")
|
|
@@ -16,10 +16,13 @@ Rego-пакет: `npm-module.npm_package_json`
|
|
|
16
16
|
- Обовʼязкове, має бути непорожнім масивом.
|
|
17
17
|
- Subset-of перевірка: кожне значення з канонічного сніпету має бути присутнє у `files`. За замовчуванням — `"types"` обовʼязковий.
|
|
18
18
|
|
|
19
|
-
**Поле `devDependencies`** (inverse-pattern
|
|
19
|
+
**Поле `devDependencies`** (inverse-pattern + Storybook-виняток, логіка в rego):
|
|
20
20
|
|
|
21
21
|
- Не публікуються користувачам пакета — має бути відсутнє або порожнє `{}`.
|
|
22
|
-
-
|
|
22
|
+
- **Виняток — канонічні Storybook-пакети** (канон Storybook, кластер 7 Governance: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`): `storybook`, `@storybook/vue3-vite`, `@storybook/vue3`, `msw`, `msw-storybook-addon` дозволені як devDeps саме тут, у `npm/package.json` консюмер-пакета — **не** в кореневому `package.json`. Обґрунтування: майбутній `isStorybookRoot()` у `@7n/test` читає саме цей файл, щоб визначити Storybook-скоуп workspace-пакета, тож маркер-пакети мають бути видимі тут, а не в кореневих tooling-deps. Версія кожного канонічного пакета зафіксована точно (map `storybook_canon_dev_deps` у rego) — присутність пакета з іншою версією теж deny (окреме повідомлення, не плутати з allowlist-забороною).
|
|
23
|
+
- Будь-який інший devDep (не з канонічного Storybook-списку) → deny з переліком імен. Dev-інструментарій переноситься у кореневий `package.json`; CLI-тули, які пакет спавнить через `bunx` у репозиторіях-споживачах (пінінг версій), — у `dependencies` (кореневе bun-правило `package_json` такі пакети в root devDeps не пускає).
|
|
24
|
+
|
|
25
|
+
Канон Storybook-devDeps та їхні версії — static map у `npm_package_json.rego` (не template-driven: це опційний allowlist, а не mandatory-presence дані, тож генеричний T0-fix-writer цього concern-а їх у кожен `package.json` не мерджить — див. коментар на початку rego-файлу).
|
|
23
26
|
|
|
24
27
|
Канонічний сніпет `files`: [package.json.snippet.json](./template/package.json.snippet.json)
|
|
25
28
|
|
|
@@ -50,8 +53,20 @@ FS-перевірки (наявність файлу зі шляху `types`, с
|
|
|
50
53
|
{ "files": ["bin", "mdc"] }
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
✗ Неправильно — наявні `devDependencies
|
|
56
|
+
✗ Неправильно — наявні `devDependencies`, не з канонічного Storybook-списку:
|
|
54
57
|
|
|
55
58
|
```json
|
|
56
59
|
{ "devDependencies": { "@7n/rules": "^1.0.0" } }
|
|
57
60
|
```
|
|
61
|
+
|
|
62
|
+
✓ Правильно — канонічний Storybook-devDep із зафіксованою версією (канон Storybook):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "devDependencies": { "storybook": "9.1.10" } }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
✗ Неправильно — Storybook-devDep присутній, але версія не збігається з каноном:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{ "devDependencies": { "storybook": "8.0.0" } }
|
|
72
|
+
```
|
|
@@ -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
|
+
}
|