@7n/rules-lang-js 0.10.0 → 0.11.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 +10 -0
- package/package.json +1 -1
- package/rules/bun/package_json/package_json.mdc +2 -2
- package/rules/bun/package_json/package_json.rego +15 -9
- package/rules/storybook/adopt/docs/main.md +1 -2
- package/rules/storybook/adopt/main.mjs +5 -2
- package/rules/storybook/ci/concern.json +15 -0
- package/rules/storybook/ci/docs/fix-ci.md +39 -0
- package/rules/storybook/ci/docs/index.md +10 -0
- package/rules/storybook/ci/docs/main.md +50 -0
- package/rules/storybook/ci/fix-ci.mjs +105 -0
- package/rules/storybook/ci/main.mjs +112 -0
- package/rules/storybook/ci/template/lint-storybook.yml.snippet.yml +41 -0
- package/rules/storybook/ci/template/setup-playwright-chromium.action.yml +34 -0
- package/rules/storybook/main.mdc +7 -1
- package/rules/storybook/mocking/mocking.mdc +24 -0
- package/rules/storybook/scaffold/docs/main.md +1 -1
- package/rules/storybook/scaffold/main.mjs +37 -5
- package/rules/storybook/scaffold/template/docs/main.md +1 -2
- package/rules/storybook/scaffold/template/main.js +53 -6
- package/rules/storybook/vitest-config/docs/fix-vitest-config.md +1 -2
- package/rules/storybook/vitest-config/docs/main.md +1 -2
- package/rules/storybook/vitest-config/fix-vitest-config.mjs +22 -6
- package/rules/storybook/vitest-config/main.mjs +33 -2
- package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +1 -2
- package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +1 -2
- package/rules/storybook/vitest-config/template/storybook-project-entry.js +6 -2
- package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +2 -1
- package/rules/storybook/vitest-config/vitest-config.mdc +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- storybook: новий concern `ci` (ADR Кластер 5, CI-частина) — канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише chromium) і `.github/workflows/lint-storybook.yml` (швидкий PR-прогін `vitest --project=storybook`), гейтований `requires.capability: ci:github`
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- storybook: viteFinal-фільтр стійкий до VueMacros-стека (Promise/масив-резолв, сімейний фільтр vite:*/vue-macros), vitest@^4 provider-factory (@vitest/browser-playwright) замість застарілого рядка `'playwright'`, flat-root layout у detectStoriesGlob (components без src/), точковий alias-мок одного модуля в mocking.mdc, і STORIES_RE false positive на `storybookTest({ configDir })` без явного include — усе за результатами пілота adopt-діагностики на nitra/components. Заодно governance package_json.rego: allowlist доповнено `@vitest/browser-playwright`.
|
|
12
|
+
|
|
3
13
|
## [0.10.0] - 2026-07-21
|
|
4
14
|
|
|
5
15
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-lang-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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,8 +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`, `@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).
|
|
12
|
+
2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@vitest/browser`, `@vitest/browser-playwright`, `@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
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'ів.
|
|
14
|
+
`@vitest/browser`/`@vitest/browser-playwright`/`playwright`/`@storybook/addon-vitest` — додані для named vitest project "storybook" (канон Storybook, кластер 5: browser-mode, лише chromium) — той самий root vitest.config, що й `unit`-проект; `@storybook/addon-vitest` постачає `storybookTest`-плагін для цього vitest-конфіга (канонічний template правила `storybook`). `@vitest/browser-playwright` — окремий npm-пакет: vitest@^4 прибрав рядкове API browser-провайдера (`provider: 'playwright'`), потрібна factory `import { playwright } from '@vitest/browser-playwright'` → `provider: playwright()` (звірено з реальним `bun.lock` пілотного консюмера — усі три `@vitest/*`/`playwright`-пакети встановлені поруч). **Межа з `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'ів.
|
|
15
15
|
|
|
16
16
|
Перевірки, що потребують FS або cross-file контексту (наприклад наявність `yarn.lock`), лишаються у JS-шарі.
|
|
@@ -8,10 +8,11 @@
|
|
|
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`
|
|
12
|
-
# `storybookTest`-плагін для named vitest project
|
|
13
|
-
# Storybook кластер 5) теж root-only test peers:
|
|
14
|
-
# `unit`-проект, живе в корені монорепо-споживача.
|
|
11
|
+
# - `@vitest/browser`/`@vitest/browser-playwright`/`playwright`/`@storybook/addon-vitest`
|
|
12
|
+
# (browser-mode provider factory + `storybookTest`-плагін для named vitest project
|
|
13
|
+
# "storybook", лише chromium — канон Storybook кластер 5) теж root-only test peers:
|
|
14
|
+
# той самий vitest.config, що й `unit`-проект, живе в корені монорепо-споживача.
|
|
15
|
+
# Storybook-специфічні
|
|
15
16
|
# identity-пакети (`storybook`, `@storybook/vue3*`, `msw*`) НЕ сюди — вони живуть у
|
|
16
17
|
# `npm/package.json` (канон Storybook кластер 7, `npm-module.mdc`), бо
|
|
17
18
|
# `isStorybookRoot()` @7n/test читає саме той файл, не кореневий package.json.
|
|
@@ -59,11 +60,15 @@ deny contains msg if {
|
|
|
59
60
|
# @stryker-mutator/core — обов'язковий exact-pin peer vitest-runner@9+ (раніше тягнувся транзитивно)
|
|
60
61
|
# @7n/test — оркестратор `coverage` (npx @7n/test coverage); devDependency, щоб npx резолвив
|
|
61
62
|
# локально без мережевого fetch щоразу.
|
|
62
|
-
# @vitest/browser + playwright — провайдер browser-mode для
|
|
63
|
-
# "storybook" (канон Storybook кластер 5: лише chromium, PR —
|
|
64
|
-
# --project=storybook).
|
|
65
|
-
#
|
|
66
|
-
#
|
|
63
|
+
# @vitest/browser + @vitest/browser-playwright + playwright — провайдер browser-mode для
|
|
64
|
+
# named vitest project "storybook" (канон Storybook кластер 5: лише chromium, PR —
|
|
65
|
+
# швидкий --project=storybook). vitest@^4 прибрав рядкове API провайдера
|
|
66
|
+
# (`provider: 'playwright'`) — `@vitest/browser-playwright` постачає factory
|
|
67
|
+
# `import { playwright } from '@vitest/browser-playwright'` → `provider: playwright()`,
|
|
68
|
+
# окремий npm-пакет, не входить у `@vitest/browser` (звірено з реальним bun.lock пілота —
|
|
69
|
+
# усі три пакети встановлені поруч). `playwright` (не `@playwright/test`) — сирий driver,
|
|
70
|
+
# який `@vitest/browser-playwright` використовує як provider; `@playwright/test` лишається
|
|
71
|
+
# окремо для змістовних E2E-сценаріїв (n-vue.mdc).
|
|
67
72
|
# @storybook/addon-vitest — постачає `storybookTest` для vitest-плагіна в канонічному
|
|
68
73
|
# vitest.config named-проекту "storybook" (той самий канон Storybook кластер 5); версія
|
|
69
74
|
# з лінійки Storybook 9.x (узгоджена з `storybook`@9.1.10, запіненим у
|
|
@@ -73,6 +78,7 @@ allowed_root_test_deps := {
|
|
|
73
78
|
"vitest",
|
|
74
79
|
"@vitest/coverage-v8",
|
|
75
80
|
"@vitest/browser",
|
|
81
|
+
"@vitest/browser-playwright",
|
|
76
82
|
"@stryker-mutator/vitest-runner",
|
|
77
83
|
"@stryker-mutator/core",
|
|
78
84
|
"@playwright/test",
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/adopt/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: c8e6e421
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 90
|
|
10
9
|
issues: anchor-miss:(mocking.mdc),anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -29,9 +29,10 @@ import {
|
|
|
29
29
|
classifyProjects,
|
|
30
30
|
findProperty,
|
|
31
31
|
findTestObject,
|
|
32
|
+
hasStoriesMarker,
|
|
32
33
|
parseModule,
|
|
34
|
+
PROVIDER_FACTORY_RE,
|
|
33
35
|
resolveVitestConfigPath,
|
|
34
|
-
STORIES_RE,
|
|
35
36
|
strykerConfigPathFor
|
|
36
37
|
} from '../vitest-config/main.mjs'
|
|
37
38
|
|
|
@@ -167,7 +168,9 @@ function diagnoseVitestProjectsSection(entry) {
|
|
|
167
168
|
const missingHints = []
|
|
168
169
|
if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
|
|
169
170
|
if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
|
|
170
|
-
if (!
|
|
171
|
+
if (!hasStoriesMarker(storybookSlice)) missingHints.push('stories-джерело (include або storybookTest({ configDir }))')
|
|
172
|
+
if (!PROVIDER_FACTORY_RE.test(storybookSlice))
|
|
173
|
+
missingHints.push('provider-factory (playwright() з @vitest/browser-playwright)')
|
|
171
174
|
if (missingHints.length > 0) {
|
|
172
175
|
return { name, file, status: STATUS.DIFFER, detail: `storybook-project без: ${missingHints.join(', ')}` }
|
|
173
176
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
|
|
3
|
+
"fixability": "config",
|
|
4
|
+
"requires": { "capability": "ci:github" },
|
|
5
|
+
"lint": {
|
|
6
|
+
"scope": "full",
|
|
7
|
+
"glob": [
|
|
8
|
+
"package.json",
|
|
9
|
+
"**/*.vue",
|
|
10
|
+
".storybook/**",
|
|
11
|
+
".github/actions/setup-playwright-chromium/action.yml",
|
|
12
|
+
".github/workflows/lint-storybook.yml"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: fix-ci.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/ci/fix-ci.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 877b9feb
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 90
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Порожній шаблон `storybook/ci`, з якого відтворюються репо-рівневі CI-артефакти: `setup-playwright-chromium` як verbatim-копія composite action для Playwright Chromium і `.github/workflows/lint-storybook.yml` із матрицею `strategy.matrix.package`, згенерованою з фактичного списку пакетів через `collectInScopeVuePackages`. Публічні anchors: `TEMPLATE_DIR`, `renderPlaywrightAction`, `renderPackageDirsYaml`, `renderStorybookWorkflow`, `patterns`.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- **TEMPLATE_DIR** — вказує на каталог `template/` цього concern-а як джерело канонічних CI-шаблонів.
|
|
20
|
+
- **renderPlaywrightAction** — читає канонічний composite action `setup-playwright-chromium` із шаблону без змін.
|
|
21
|
+
- **renderPackageDirsYaml** — перетворює список коренів пакетів у YAML-рядки для матриці Storybook CI.
|
|
22
|
+
- **renderStorybookWorkflow** — збирає канонічний `.github/workflows/lint-storybook.yml`, підставляючи в нього матрицю пакетів зі скоупу репозиторію.
|
|
23
|
+
- **patterns** — описує T0-правила автофіксу для відновлення відсутніх CI-файлів цього concern-а.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- TEMPLATE_DIR — Каталог `template/` цього concern-а.
|
|
28
|
+
- renderPlaywrightAction — Вміст канонічного composite action `setup-playwright-chromium` — verbatim з template.
|
|
29
|
+
- renderPackageDirsYaml — Рендерить YAML-фрагмент `matrix.package` списку — по одному `- <rootDir>` на рядок, з тим
|
|
30
|
+
самим відступом, що й токен-рядок у шаблоні (10 пробілів — рівень елемента списку під
|
|
31
|
+
`strategy.matrix.package:`). `rootDir === '.'` (корінь монорепо) лишається `.` — валідний
|
|
32
|
+
`working-directory` для GitHub Actions.
|
|
33
|
+
- renderStorybookWorkflow — Вміст канонічного `.github/workflows/lint-storybook.yml` — template з підставленою
|
|
34
|
+
матрицею пакетів у скоупі.
|
|
35
|
+
- patterns — повертає набір шаблонів, за якими далі шукають або зіставляють потрібні значення.
|
|
36
|
+
|
|
37
|
+
## Гарантії поведінки
|
|
38
|
+
|
|
39
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/rules/storybook/ci
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/ci/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ----------------------- | --------- |
|
|
9
|
+
| [fix-ci.mjs](fix-ci.md) | JS Module |
|
|
10
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/ci/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 65df5791
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 85
|
|
10
|
+
issues: internal-name:collectInScopeVuePackages,anchor-miss:(storybook.mdc),judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл фіксує канонічні репо-шляхи й маркери для storybook CI, щоб у Vue component library пакетах у scope швидко знаходити правильні `.github/actions/setup-playwright-chromium/action.yml` і `.github/workflows/lint-storybook.yml` та зіставляти їх із повідомленнями в `storybook.mdc`. Експортовані константи-рядки: PLAYWRIGHT_ACTION_REL=".github/actions/setup-playwright-chromium/action.yml" — канонічний шлях до Playwright action; STORYBOOK_WORKFLOW_REL=".github/workflows/lint-storybook.yml" — канонічний шлях до workflow linting. Маркери повідомлень: PLAYWRIGHT_ACTION_MARKERS і STORYBOOK_WORKFLOW_MARKERS, які прив’язують перевірки до повідомлень у `storybook.mdc`. Це потрібно, щоб підтримувати єдиний storybook-канон без зайвого обходу `.github` і `.git`, із кешуванням результату в межах одного прогону.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- PLAYWRIGHT_ACTION_REL — задає репо-рівневий шлях до канонічного composite action `.github/actions/setup-playwright-chromium/action.yml`, який перевіряється як частина storybook CI.
|
|
21
|
+
- STORYBOOK_WORKFLOW_REL — задає репо-рівневий шлях до канонічного workflow `.github/workflows/lint-storybook.yml`, що відповідає за швидкий storybook-лінт.
|
|
22
|
+
- PLAYWRIGHT_ACTION_MARKERS — описує канонічні ознаки composite action для storybook CI, щоб гарантувати кеш Playwright і встановлення лише chromium; у повідомленнях використовується прив’язка до `storybook.mdc`.
|
|
23
|
+
- STORYBOOK_WORKFLOW_MARKERS — описує канонічні ознаки workflow для storybook CI, щоб гарантувати запуск швидкого `vitest --project=storybook`; у повідомленнях використовується прив’язка до `storybook.mdc`.
|
|
24
|
+
- lint — перевіряє наявність storybook CI-канону лише для репозиторіїв із Vue component library пакетами в scope; якщо таких пакетів немає, завершується без зауважень. Свідомо не зачіпає `.github` і `.git` як окремі області обходу; кешує результат у межах одного прогону.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- PLAYWRIGHT_ACTION_REL — Repo-relative шлях канонічного composite action (не per-package — один на репозиторій).
|
|
29
|
+
- STORYBOOK_WORKFLOW_REL — Repo-relative шлях канонічного workflow, що запускає `vitest --project=storybook`.
|
|
30
|
+
- PLAYWRIGHT_ACTION_MARKERS — Маркери канону composite action `setup-playwright-chromium` (ADR Кластер 5): кеш
|
|
31
|
+
`ms-playwright` через `actions/cache`, ключ від версії playwright, install лише chromium.
|
|
32
|
+
Текстовий пошук — той самий підхід, що й `MAIN_JS_MARKERS`/`PREVIEW_JS_MARKERS` у `scaffold`.
|
|
33
|
+
- STORYBOOK_WORKFLOW_MARKERS — Маркери канону `.github/workflows/lint-storybook.yml`: композитний Playwright-кеш ПІСЛЯ
|
|
34
|
+
setup-bun-deps, і швидкий `vitest --project=storybook` (ADR Кластер 5 — nightly-only
|
|
35
|
+
`@7n/test coverage`/mutation-testing на PR не запускається, лише цей швидкий шлях).
|
|
36
|
+
- lint — Detector concern-а `storybook/ci` (ADR Кластер 5, CI-частина): для репозиторіїв з бодай
|
|
37
|
+
одним Vue component library пакетом у скоупі Storybook (`collectInScopeVuePackages`) —
|
|
38
|
+
канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише
|
|
39
|
+
chromium) і канонічний `.github/workflows/lint-storybook.yml`, що запускає швидкий
|
|
40
|
+
`vitest --project=storybook` на PR. Гейтований `requires.capability: ci:github` — спить
|
|
41
|
+
у репозиторіях без плагіна `@7n/rules-ci-github` (немає `.github/workflows`).
|
|
42
|
+
|
|
43
|
+
Nightly-only `@7n/test coverage` (mutation testing) — поза обсягом цього concern-а: ADR
|
|
44
|
+
Кластер 5 явно розділяє швидкий PR-шлях (цей concern) і nightly mutation-прогін, який
|
|
45
|
+
лишається окремою інфраструктурою `test/stryker_config`.
|
|
46
|
+
|
|
47
|
+
## Гарантії поведінки
|
|
48
|
+
|
|
49
|
+
- Кешує результати в межах одного прогону.
|
|
50
|
+
- Свідомо пропускає шляхи: `.github`, `.git`.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-autofix для concern-а `storybook/ci` (ADR Кластер 5, CI-частина): відтворює канонічний
|
|
3
|
+
* composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише chromium) і
|
|
4
|
+
* канонічний `.github/workflows/lint-storybook.yml` з `template/` цього concern-а.
|
|
5
|
+
*
|
|
6
|
+
* Обидва файли — репо-рівневі (не per-package): composite action — verbatim-копія; workflow —
|
|
7
|
+
* матриця `strategy.matrix.package` генерується з фактичного списку пакетів у скоупі
|
|
8
|
+
* (`collectInScopeVuePackages`), щоб CI реально покривав усі Storybook-пакети репозиторію.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { readFile } from 'node:fs/promises'
|
|
14
|
+
|
|
15
|
+
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
16
|
+
|
|
17
|
+
/** Каталог `template/` цього concern-а. */
|
|
18
|
+
export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'template')
|
|
19
|
+
|
|
20
|
+
/** Токен у `lint-storybook.yml.snippet.yml`, що заміщається списком пакетів матриці. */
|
|
21
|
+
const PACKAGE_DIRS_TOKEN = '__STORYBOOK_CI_PACKAGE_DIRS__'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Вміст канонічного composite action `setup-playwright-chromium` — verbatim з template.
|
|
25
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
26
|
+
* @returns {Promise<string>} вміст `action.yml`
|
|
27
|
+
*/
|
|
28
|
+
export function renderPlaywrightAction(templateDir = TEMPLATE_DIR) {
|
|
29
|
+
return readFile(join(templateDir, 'setup-playwright-chromium.action.yml'), 'utf8')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Рендерить YAML-фрагмент `matrix.package` списку — по одному `- <rootDir>` на рядок, з тим
|
|
34
|
+
* самим відступом, що й токен-рядок у шаблоні (10 пробілів — рівень елемента списку під
|
|
35
|
+
* `strategy.matrix.package:`). `rootDir === '.'` (корінь монорепо) лишається `.` — валідний
|
|
36
|
+
* `working-directory` для GitHub Actions.
|
|
37
|
+
* @param {string[]} rootDirs відносні (posix) корені пакетів у скоупі
|
|
38
|
+
* @returns {string} YAML-рядки (без завершального переносу)
|
|
39
|
+
*/
|
|
40
|
+
export function renderPackageDirsYaml(rootDirs) {
|
|
41
|
+
return rootDirs.map(dir => ` - ${dir}`).join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Вміст канонічного `.github/workflows/lint-storybook.yml` — template з підставленою
|
|
46
|
+
* матрицею пакетів у скоупі.
|
|
47
|
+
* @param {string[]} rootDirs відносні корені пакетів у скоупі (`collectInScopeVuePackages`)
|
|
48
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
49
|
+
* @returns {Promise<string>} вміст `lint-storybook.yml`
|
|
50
|
+
*/
|
|
51
|
+
export async function renderStorybookWorkflow(rootDirs, templateDir = TEMPLATE_DIR) {
|
|
52
|
+
const raw = await readFile(join(templateDir, 'lint-storybook.yml.snippet.yml'), 'utf8')
|
|
53
|
+
const tokenLineRe = new RegExp(`^[ \\t]*${PACKAGE_DIRS_TOKEN}[ \\t]*$`, 'mu')
|
|
54
|
+
// Replacer-функція (не рядок) — уникає unicorn/no-unsafe-string-replacement:
|
|
55
|
+
// рядок-заміна мав би трактувати `$`-послідовності як спецсимволи `String#replace`.
|
|
56
|
+
return raw.replace(tokenLineRe, () => renderPackageDirsYaml(rootDirs))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Записує файл, створюючи батьківські каталоги й реєструючи запис для rollback (той самий
|
|
61
|
+
* патерн, що й `writeScaffoldFile` у `scaffold/fix-scaffold.mjs`).
|
|
62
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').FixContext} ctx fix-контекст рунга
|
|
63
|
+
* @param {string} absPath абсолютний шлях цільового файлу
|
|
64
|
+
* @param {string} content вміст файлу
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
function writeCiFile(ctx, absPath, content) {
|
|
68
|
+
mkdirSync(dirname(absPath), { recursive: true })
|
|
69
|
+
ctx.recordWrite?.(absPath)
|
|
70
|
+
writeFileSync(absPath, content, 'utf8')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
74
|
+
export const patterns = [
|
|
75
|
+
{
|
|
76
|
+
id: 'storybook-ci-playwright-action',
|
|
77
|
+
test: violations => violations.some(v => v.reason === 'missing-playwright-action' && v.file),
|
|
78
|
+
apply: async (violations, ctx) => {
|
|
79
|
+
const target = violations.find(v => v.reason === 'missing-playwright-action' && v.file)
|
|
80
|
+
if (!target || !ctx.concernDir) return { touchedFiles: [] }
|
|
81
|
+
|
|
82
|
+
const content = await renderPlaywrightAction(join(ctx.concernDir, 'template'))
|
|
83
|
+
const abs = join(ctx.cwd, target.file)
|
|
84
|
+
writeCiFile(ctx, abs, content)
|
|
85
|
+
return { touchedFiles: [abs], message: `${target.file}: створено composite action setup-playwright-chromium` }
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'storybook-ci-workflow',
|
|
90
|
+
test: violations => violations.some(v => v.reason === 'missing-storybook-workflow' && v.file),
|
|
91
|
+
apply: async (violations, ctx) => {
|
|
92
|
+
const target = violations.find(v => v.reason === 'missing-storybook-workflow' && v.file)
|
|
93
|
+
if (!target || !ctx.concernDir) return { touchedFiles: [] }
|
|
94
|
+
|
|
95
|
+
const pkgs = await collectInScopeVuePackages(ctx.cwd)
|
|
96
|
+
const rootDirs = pkgs.map(p => p.rootDir)
|
|
97
|
+
if (rootDirs.length === 0) return { touchedFiles: [] }
|
|
98
|
+
|
|
99
|
+
const content = await renderStorybookWorkflow(rootDirs, join(ctx.concernDir, 'template'))
|
|
100
|
+
const abs = join(ctx.cwd, target.file)
|
|
101
|
+
writeCiFile(ctx, abs, content)
|
|
102
|
+
return { touchedFiles: [abs], message: `${target.file}: створено (${rootDirs.length} пакет(ів) у матриці)` }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** @see ./docs/main.md */
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
|
|
7
|
+
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
8
|
+
import { missingMarkers } from '../scaffold/main.mjs'
|
|
9
|
+
|
|
10
|
+
/** Repo-relative шлях канонічного composite action (не per-package — один на репозиторій). */
|
|
11
|
+
export const PLAYWRIGHT_ACTION_REL = '.github/actions/setup-playwright-chromium/action.yml'
|
|
12
|
+
|
|
13
|
+
/** Repo-relative шлях канонічного workflow, що запускає `vitest --project=storybook`. */
|
|
14
|
+
export const STORYBOOK_WORKFLOW_REL = '.github/workflows/lint-storybook.yml'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Маркери канону composite action `setup-playwright-chromium` (ADR Кластер 5): кеш
|
|
18
|
+
* `ms-playwright` через `actions/cache`, ключ від версії playwright, install лише chromium.
|
|
19
|
+
* Текстовий пошук — той самий підхід, що й `MAIN_JS_MARKERS`/`PREVIEW_JS_MARKERS` у `scaffold`.
|
|
20
|
+
*/
|
|
21
|
+
export const PLAYWRIGHT_ACTION_MARKERS = [
|
|
22
|
+
{ token: 'ms-playwright', hint: 'кеш каталогу ms-playwright' },
|
|
23
|
+
{ token: 'actions/cache@', hint: 'actions/cache для Playwright-браузерів' },
|
|
24
|
+
{ token: 'playwright install chromium', hint: 'install лише chromium (не всі браузери)' }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Маркери канону `.github/workflows/lint-storybook.yml`: композитний Playwright-кеш ПІСЛЯ
|
|
29
|
+
* setup-bun-deps, і швидкий `vitest --project=storybook` (ADR Кластер 5 — nightly-only
|
|
30
|
+
* `@7n/test coverage`/mutation-testing на PR не запускається, лише цей швидкий шлях).
|
|
31
|
+
*/
|
|
32
|
+
export const STORYBOOK_WORKFLOW_MARKERS = [
|
|
33
|
+
{ token: './.github/actions/setup-bun-deps', hint: 'setup-bun-deps перед Playwright-кроком' },
|
|
34
|
+
{ token: './.github/actions/setup-playwright-chromium', hint: 'композитний Playwright-кеш' },
|
|
35
|
+
{ token: '--project=storybook', hint: 'швидкий прогін лише storybook-проєкту (не повний coverage)' }
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Перевіряє один репо-рівневий канонічний файл (composite action чи workflow): відсутність →
|
|
40
|
+
* `missingReason`-порушення з посиланням на `npx \@7n/rules fix storybook`; присутність без
|
|
41
|
+
* якогось маркера → `markerReason`-порушення на конкретний маркер. Той самий патерн, що й
|
|
42
|
+
* `checkCanonFile` у `scaffold/main.mjs`, але без per-package `rootDir` — файл один на репо.
|
|
43
|
+
* @param {string} cwd абсолютний корінь репозиторію
|
|
44
|
+
* @param {string} relFile posix-relative шлях файлу від кореня репозиторію
|
|
45
|
+
* @param {{ token: string, hint: string }[]} markers канонічні маркери файлу
|
|
46
|
+
* @param {string} missingReason reason для порушення "файл відсутній"
|
|
47
|
+
* @param {string} markerReason reason для порушення "маркер відсутній"
|
|
48
|
+
* @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async function checkRepoCanonFile(cwd, relFile, markers, missingReason, markerReason, reporter) {
|
|
52
|
+
const abs = join(cwd, relFile)
|
|
53
|
+
if (existsSync(abs)) {
|
|
54
|
+
const content = await readFile(abs, 'utf8')
|
|
55
|
+
for (const m of missingMarkers(content, markers)) {
|
|
56
|
+
reporter.fail(`${relFile} не відповідає канону — бракує: ${m.hint} (storybook.mdc, ADR Кластер 5)`, {
|
|
57
|
+
reason: markerReason,
|
|
58
|
+
file: relFile
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
reporter.fail(
|
|
64
|
+
`Відсутній ${relFile} — канонічний Playwright-кеш для vitest storybook-проєкту: npx @7n/rules fix storybook (storybook.mdc, ADR Кластер 5)`,
|
|
65
|
+
{ reason: missingReason, file: relFile }
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detector concern-а `storybook/ci` (ADR Кластер 5, CI-частина): для репозиторіїв з бодай
|
|
71
|
+
* одним Vue component library пакетом у скоупі Storybook (`collectInScopeVuePackages`) —
|
|
72
|
+
* канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише
|
|
73
|
+
* chromium) і канонічний `.github/workflows/lint-storybook.yml`, що запускає швидкий
|
|
74
|
+
* `vitest --project=storybook` на PR. Гейтований `requires.capability: ci:github` — спить
|
|
75
|
+
* у репозиторіях без плагіна `@7n/rules-ci-github` (немає `.github/workflows`).
|
|
76
|
+
*
|
|
77
|
+
* Nightly-only `@7n/test coverage` (mutation testing) — поза обсягом цього concern-а: ADR
|
|
78
|
+
* Кластер 5 явно розділяє швидкий PR-шлях (цей concern) і nightly mutation-прогін, який
|
|
79
|
+
* лишається окремою інфраструктурою `test/stryker_config`.
|
|
80
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
|
|
81
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
|
|
82
|
+
*/
|
|
83
|
+
export async function lint(ctx) {
|
|
84
|
+
const reporter = createViolationReporter(ctx)
|
|
85
|
+
const cwd = ctx.cwd
|
|
86
|
+
|
|
87
|
+
const pkgs = await collectInScopeVuePackages(cwd)
|
|
88
|
+
if (pkgs.length === 0) {
|
|
89
|
+
reporter.pass('storybook/ci: немає Vue component library пакетів у скоупі (storybook.mdc)')
|
|
90
|
+
return reporter.result()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await checkRepoCanonFile(
|
|
94
|
+
cwd,
|
|
95
|
+
PLAYWRIGHT_ACTION_REL,
|
|
96
|
+
PLAYWRIGHT_ACTION_MARKERS,
|
|
97
|
+
'missing-playwright-action',
|
|
98
|
+
'playwright-action-marker-missing',
|
|
99
|
+
reporter
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await checkRepoCanonFile(
|
|
103
|
+
cwd,
|
|
104
|
+
STORYBOOK_WORKFLOW_REL,
|
|
105
|
+
STORYBOOK_WORKFLOW_MARKERS,
|
|
106
|
+
'missing-storybook-workflow',
|
|
107
|
+
'storybook-workflow-marker-missing',
|
|
108
|
+
reporter
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return reporter.result()
|
|
112
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Lint Storybook
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
- '**/*.stories.@(js|ts)'
|
|
10
|
+
- '.storybook/**'
|
|
11
|
+
pull_request:
|
|
12
|
+
branches:
|
|
13
|
+
- dev
|
|
14
|
+
- main
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
storybook-test:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
strategy:
|
|
26
|
+
fail-fast: false
|
|
27
|
+
matrix:
|
|
28
|
+
package:
|
|
29
|
+
__STORYBOOK_CI_PACKAGE_DIRS__
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v6
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
|
|
35
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
36
|
+
|
|
37
|
+
- uses: ./.github/actions/setup-playwright-chromium
|
|
38
|
+
|
|
39
|
+
- name: Storybook vitest project (${{ matrix.package }})
|
|
40
|
+
working-directory: ${{ matrix.package }}
|
|
41
|
+
run: bunx vitest run --project=storybook
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
|
|
2
|
+
|
|
3
|
+
name: Setup Playwright Chromium
|
|
4
|
+
description: >-
|
|
5
|
+
Кеш Playwright-браузерів (лише chromium, storybook.mdc ADR Кластер 5) для vitest
|
|
6
|
+
browser-mode storybook-проєкту. Викликати ПІСЛЯ ./.github/actions/setup-bun-deps
|
|
7
|
+
(потрібен node_modules/bun.lock для визначення версії).
|
|
8
|
+
|
|
9
|
+
runs:
|
|
10
|
+
using: composite
|
|
11
|
+
steps:
|
|
12
|
+
- name: Resolve Playwright version (bun.lock)
|
|
13
|
+
id: playwright-version
|
|
14
|
+
shell: bash
|
|
15
|
+
run: |
|
|
16
|
+
VERSION=$(grep -oE '"playwright(-core)?@[0-9]+\.[0-9]+\.[0-9]+"' bun.lock | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
|
|
17
|
+
if [ -z "$VERSION" ]; then
|
|
18
|
+
VERSION=$(node -p "require('playwright-core/package.json').version" 2>/dev/null || echo unknown)
|
|
19
|
+
fi
|
|
20
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
21
|
+
|
|
22
|
+
- name: Cache Playwright browsers (chromium)
|
|
23
|
+
id: playwright-cache
|
|
24
|
+
uses: actions/cache@v5
|
|
25
|
+
with:
|
|
26
|
+
path: |
|
|
27
|
+
~/.cache/ms-playwright
|
|
28
|
+
~/Library/Caches/ms-playwright
|
|
29
|
+
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
|
|
30
|
+
|
|
31
|
+
- name: Install Playwright chromium (лише chromium, storybook.mdc)
|
|
32
|
+
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
33
|
+
shell: bash
|
|
34
|
+
run: bunx playwright install chromium --with-deps
|
package/rules/storybook/main.mdc
CHANGED
|
@@ -31,7 +31,7 @@ alwaysApply: false
|
|
|
31
31
|
|
|
32
32
|
- **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
|
|
33
33
|
- **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
|
|
34
|
-
- **`viteFinal`** зчитує `vite.config` самого пакета (той самий, що й для звичайного білду) і мерджить його плагіни в конфіг Storybook, **знімаючи** `vite-plugin-pages
|
|
34
|
+
- **`viteFinal`** зчитує `vite.config` самого пакета (той самий, що й для звичайного білду) і мерджить його плагіни в конфіг Storybook, **знімаючи** роутинг-плагіни файлової маршрутизації додатка-споживача (`vite-plugin-pages`, `unplugin-vue-router`, `vite-plugin-vue-layouts`, `vite-plugin-vue-layouts-next`) — вони не мають сенсу в ізольованому рендері компонента. Плагіни `plugins`-масиву можуть бути `Promise`/вкладеними масивами (`VueMacros({ plugins: { vue: Vue() } })` — реальний стек пілотного консюмера повертає `Promise`, що резолвиться в масив плагінів) — фільтр спершу resolve/flatten-ить їх, інакше порівняння імені з `Promise`, що ще не резолвився, мовчки пропускає дублікат `@vitejs/plugin-vue`-трансформу (подвійна SFC-трансформація). Vue-трансформери фільтруються за сімейством імені (`vite:vue`-префікс або підрядок `vue-macros`), не лише буквальним `@vitejs/plugin-vue` — деталі й обґрунтування коментарем у `scaffold/template/main.js`.
|
|
35
35
|
- **`staticDirs`** покриває `.storybook/public` — статичний asset для msw service worker (`preview.js`).
|
|
36
36
|
- **`preview.js`**: повний `Quasar`-install (не тільки окремі компоненти) + `iconSet`+`iconMapFn`-комбо — без цієї пари внутрішні Quasar-компоненти (напр. стрілка `QSelect`) не резолвлять вбудовані іконки поза full CLI build. `msw-storybook-addon` ініціалізується з `onUnhandledRequest`-фільтром: **same-origin GET мовчки пропускається** (Vite HMR/asset-шум), решта — попередження (не білд-помилка — навмисно м'яко для хвилі 1).
|
|
37
37
|
- **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
|
|
@@ -43,6 +43,12 @@ alwaysApply: false
|
|
|
43
43
|
|
|
44
44
|
Канонічний `test.projects` (`unit`+`storybook`, browser-mode лише chromium) і ізольований `vitest.stryker.config` (той самий unit-набір, без browser-mode — `@stryker-mutator/vitest-runner` крашиться на browser-mode `projects`) — `vitest-config/main.mjs` (перевірка, AST через `oxc-parser`) і `vitest-config/fix-vitest-config.mjs` (точкові insert-only правки наявного конфіга, fixability: `config`). Деталі канону, чому саме chromium і межі автофіксу — `vitest-config/vitest-config.mdc`, не дублюється тут.
|
|
45
45
|
|
|
46
|
+
### CI: Playwright-кеш і швидкий PR-прогін (Кластер 5, CI-частина)
|
|
47
|
+
|
|
48
|
+
`ci/main.mjs` + `ci/fix-ci.mjs` (fixability: `config`, `requires.capability: ci:github` — спить у репозиторіях без плагіна `@7n/rules-ci-github`): для кожного репозиторію з бодай одним пакетом у скоупі — канонічний composite action `.github/actions/setup-playwright-chromium/action.yml` (кеш `~/.cache/ms-playwright`/`~/Library/Caches/ms-playwright` за версією playwright з `bun.lock`, install **лише** `chromium --with-deps` при cache miss) і `.github/workflows/lint-storybook.yml` (матриця `strategy.matrix.package` — фактичні пакети у скоупі, `checkout` → `setup-bun-deps` → `setup-playwright-chromium` → `vitest run --project=storybook`). Композитний action і workflow — репо-рівневі файли (не per-package), перевірка й автофікс не per-package.
|
|
49
|
+
|
|
50
|
+
Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.
|
|
51
|
+
|
|
46
52
|
## Гігієна сторонніх залежностей (Кластер 6)
|
|
47
53
|
|
|
48
54
|
`hygiene/main.mjs`: (1) undeclared third-party imports у `.vue`-файлах пакета — import стороннього пакета, якого немає в `dependencies`/`peerDependencies` (реальний кейс ADR — зламаний default-export `@vuepic/vue-datepicker` v14, silent breakage без цієї перевірки); (2) наявність `src/css/quasar.variables.{scss,sass}` без відповідного `quasar({ sassVariables: true })` у `.storybook/main.js` — глобальні Quasar SCSS-змінні пакета інакше не резолвляться в ізольованому Storybook-рендері. Docs-only детальний виклад не потрібен — концерн самодостатній, без окремого `.mdc`.
|
|
@@ -73,6 +73,30 @@ export const Realtime = {
|
|
|
73
73
|
}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Точковий alias-мок одного модуля
|
|
77
|
+
|
|
78
|
+
**Скоуп цього рецепта — компонентні бібліотеки, НЕ сторінки.** Рішення «виключно MSW» вище (розділ «Apollo/GraphQL — виключно MSW») стосується сторінок (розширення ADR, `2026-07-20`) — там повний data-flow (query+mutation+subscription, кілька викликів на сторінку), і мок-хендлери мають бути переносними в майбутні vitest/playwright-сценарії. Реальний кейс компонентної бібліотеки (пілот `components/npm`) — інший: один компонент статично імпортує модуль із side-effect на рівні build-у (напр. `@nitra/vite-boot/apollo` — конструює Apollo-клієнт із `VITE_HASURA_HOST`/WS-link env-змінних, яких немає в Storybook), і компонент має 1-2 точкові виклики цього модуля, не повний GraphQL data-flow. Розгортати MSW-стек (`msw-storybook-addon`, worker, handlers) заради одного статичного імпорту — зайві накладні витрати, надто коли `msw` навіть не встановлений у пакеті.
|
|
79
|
+
|
|
80
|
+
Рецепт — build-time підміна імпорту на рівні Vite-resolve у `viteFinal` `.storybook/main.js` (не мокає мережу, підміняє сам модуль):
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
async viteFinal(config) {
|
|
84
|
+
const { mergeConfig } = await import('vite')
|
|
85
|
+
return mergeConfig(config, {
|
|
86
|
+
resolve: {
|
|
87
|
+
alias: {
|
|
88
|
+
// NImages.vue статично імпортує apolloClient/gql (не provide/inject) — реальний
|
|
89
|
+
// модуль конструює Apollo-клієнт з env-змінних, яких немає в Storybook.
|
|
90
|
+
// Підміна на рівні Vite-resolve замість зміни коду компонента.
|
|
91
|
+
'@nitra/vite-boot/apollo': join(dirName, 'mocks/apollo.js')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Критерій вибору:** компонент з 1-2 точковими викликами стороннього модуля (side-effect boot/client-конструктор, не повний query/mutation/subscription-flow) → alias-мок одного модуля. Сторінки, підписки чи повний GraphQL data-flow (кілька query/mutation, стан, error/loading-сценарії) → MSW (розділ вище) — не супереч рішенню розширення ADR, а розмежування скоупу: alias — для компонентних бібліотек із мінімальним мережевим слідом, MSW — для сторінок і повнішого data-flow.
|
|
99
|
+
|
|
76
100
|
### Pinia
|
|
77
101
|
|
|
78
102
|
Справжня `createPinia()` у page-декораторі — **без** `pinia-plugin-persistedstate` (`persist: true` у сторі стає no-op, читання/запис localStorage не потрібні в ізольованому рендері). Наповнення початкового стану — через `parameters.pinia.initialState`, не хардкод у декораторі:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @see ./docs/main.md */
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
@@ -17,7 +17,16 @@ export const MAIN_JS_MARKERS = [
|
|
|
17
17
|
{ token: '@storybook/vue3-vite', hint: 'framework @storybook/vue3-vite' },
|
|
18
18
|
{ token: 'viteFinal', hint: 'viteFinal-override vite.config пакета' },
|
|
19
19
|
{ token: "'vite-plugin-pages'", hint: 'фільтр vite-plugin-pages у viteFinal' },
|
|
20
|
-
{ token: "'vite-plugin-vue-layouts'", hint: 'фільтр vite-plugin-vue-layouts у viteFinal' }
|
|
20
|
+
{ token: "'vite-plugin-vue-layouts'", hint: 'фільтр vite-plugin-vue-layouts у viteFinal' },
|
|
21
|
+
{ token: "'vite-plugin-vue-layouts-next'", hint: 'фільтр vite-plugin-vue-layouts-next у viteFinal' },
|
|
22
|
+
{
|
|
23
|
+
token: 'isVueTransformFamily',
|
|
24
|
+
hint: 'сімейний фільтр vue-трансформерів (vite:vue/vue-macros) — стійкість до VueMacros-стека'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
token: 'resolvePluginEntry',
|
|
28
|
+
hint: 'resolve/flatten Promise/масиву плагінів перед фільтрацією (VueMacros повертає Promise)'
|
|
29
|
+
}
|
|
21
30
|
]
|
|
22
31
|
|
|
23
32
|
/** Маркери канону `.storybook/preview.js`. Експортовано — переюз у `adopt/main.mjs`. */
|
|
@@ -30,13 +39,36 @@ export const PREVIEW_JS_MARKERS = [
|
|
|
30
39
|
]
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
42
|
+
* Чи має корінь пакета плоскі `.vue`-файли (flat-root layout — `NDialog.vue`,
|
|
43
|
+
* `NDialog.stories.js` лежать прямо в КОРЕНІ пакета, `src/` майже порожній чи
|
|
44
|
+
* відсутній). Реальний кейс пілотного консюмера (components/npm) — component
|
|
45
|
+
* library без `src/components/`, детекція за самою наявністю `src/` дала б 0
|
|
46
|
+
* знайдених історій (тихий регрес adopt-діагностики). Перевірка нерекурсивна —
|
|
47
|
+
* дивиться лише файли безпосередньо в `absPkgDir`.
|
|
48
|
+
* @param {string} absPkgDir абсолютний шлях кореня пакета
|
|
49
|
+
* @returns {boolean} true — у корені пакета є хоча б один `.vue`-файл
|
|
50
|
+
*/
|
|
51
|
+
function hasFlatRootVueFiles(absPkgDir) {
|
|
52
|
+
let entries
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(absPkgDir, { withFileTypes: true })
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
return entries.some(e => e.isFile() && e.name.endsWith('.vue'))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Layout-детекція для stories-glob (ADR Кластер 2, розширено пілотом на flat-root):
|
|
63
|
+
* `.vue`-файли прямо в корені пакета (без `src/`) → flat-root glob по корені;
|
|
64
|
+
* інакше `src/components/` присутній → glob звужується до нього; інакше — ширший
|
|
65
|
+
* glob по всьому `src/`. Шлях відносний до `.storybook/` (де лежить сам `main.js`),
|
|
66
|
+
* тому з префіксом `../`.
|
|
36
67
|
* @param {string} absPkgDir абсолютний шлях кореня пакета
|
|
37
68
|
* @returns {string} glob для `stories` у `.storybook/main.js`
|
|
38
69
|
*/
|
|
39
70
|
export function detectStoriesGlob(absPkgDir) {
|
|
71
|
+
if (hasFlatRootVueFiles(absPkgDir)) return '../*.stories.@(js|ts)'
|
|
40
72
|
return existsSync(join(absPkgDir, 'src/components'))
|
|
41
73
|
? '../src/components/**/*.stories.@(js|ts)'
|
|
42
74
|
: '../src/**/*.stories.@(js|ts)'
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.js
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/scaffold/template/main.js
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 74cf3397
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.96
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -12,8 +12,53 @@ import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
|
|
12
12
|
const OWN_PLUGINS = [vue({ template: { transformAssetUrls } }), quasar({ sassVariables: true })]
|
|
13
13
|
|
|
14
14
|
// Плагіни файлової маршрутизації додатка-споживача — не мають сенсу в ізольованому
|
|
15
|
-
// рендері одного компонента, знімаються при обʼєднанні з vite.config пакета.
|
|
16
|
-
|
|
15
|
+
// рендері одного компонента, знімаються при обʼєднанні з vite.config пакета. Обидві
|
|
16
|
+
// версії layouts-пакета (старий і `-next`-форк) та pages-роутери — реальні консюмери
|
|
17
|
+
// мігрують поступово, старий і новий пакет можуть співіснувати в різних vite.config.
|
|
18
|
+
const INCOMPATIBLE_PLUGIN_NAMES = new Set([
|
|
19
|
+
'vite-plugin-pages',
|
|
20
|
+
'unplugin-vue-router',
|
|
21
|
+
'vite-plugin-vue-layouts',
|
|
22
|
+
'vite-plugin-vue-layouts-next'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Плагін належить до сімейства Vue SFC-трансформерів, які ДУБЛЮЮТЬ `OWN_PLUGINS`'
|
|
27
|
+
* `vue()`. Реальний стек консюмерів (components/npm/vite.config.js) обгортає
|
|
28
|
+
* `@vitejs/plugin-vue` через `VueMacros({ plugins: { vue: Vue() } })` — після
|
|
29
|
+
* резолву цей виклик повертає МАСИВ плагінів: сам `vite:vue` (той самий transform,
|
|
30
|
+
* що й `OWN_PLUGINS`' `vue()` — дублювання дає ПОДВІЙНУ SFC-трансформацію) плюс
|
|
31
|
+
* службові `vue-macros-*` (devtools/exclude-dep-optimize). Плагіни macro-синтаксису
|
|
32
|
+
* (`unplugin-vue-define-props`/`define-emit`/`reactivity-transform` тощо) — НЕ
|
|
33
|
+
* фільтруються: вони не дублюються `OWN_PLUGINS` і потрібні для macro-фіч пакета;
|
|
34
|
+
* Vite впорядковує їх через власний `enforce: 'pre'|'post'` незалежно від позиції
|
|
35
|
+
* в підсумковому масиві `plugins`, тож порядок відносно `OWN_PLUGINS` тут не важливий.
|
|
36
|
+
* @param {string | undefined} name ім'я плагіна
|
|
37
|
+
* @returns {boolean} true — плагін дублює `OWN_PLUGINS`' `vue()`
|
|
38
|
+
*/
|
|
39
|
+
function isVueTransformFamily(name) {
|
|
40
|
+
return typeof name === 'string' && (name.startsWith('vite:vue') || name.includes('vue-macros'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Резолвить один запис `config.plugins` у плаский масив реальних плагінів. Vite
|
|
45
|
+
* офіційно підтримує `Plugin | Promise<Plugin> | (Plugin | Promise<Plugin>)[]`
|
|
46
|
+
* (довільна вкладеність) — `VueMacros(...)` сам повертає `Promise`, що резолвиться
|
|
47
|
+
* в масив плагінів. `loadConfigFromFile` читає файл конфіга як є й НЕ виконує це
|
|
48
|
+
* resolve/flatten (це робить лише повний `resolveConfig` пізніше у власному циклі
|
|
49
|
+
* Vite) — без ручного resolve/flatten тут фільтр порівнював би ім'я з
|
|
50
|
+
* `Promise`-об'єктом, що ще не резолвився (`undefined`), і пропускав би дублікат далі.
|
|
51
|
+
* @param {unknown} entry один елемент/Promise/масив із `config.plugins`
|
|
52
|
+
* @returns {Promise<object[]>} плаский масив плагінів після resolve
|
|
53
|
+
*/
|
|
54
|
+
async function resolvePluginEntry(entry) {
|
|
55
|
+
const resolved = await entry
|
|
56
|
+
if (Array.isArray(resolved)) {
|
|
57
|
+
const nested = await Promise.all(resolved.map(resolvePluginEntry))
|
|
58
|
+
return nested.flat(Infinity)
|
|
59
|
+
}
|
|
60
|
+
return resolved ? [resolved] : []
|
|
61
|
+
}
|
|
17
62
|
|
|
18
63
|
/** @type {import('@storybook/vue3-vite').StorybookConfig} */
|
|
19
64
|
const config = {
|
|
@@ -26,13 +71,15 @@ const config = {
|
|
|
26
71
|
staticDirs: ['./public'],
|
|
27
72
|
async viteFinal(storybookConfig) {
|
|
28
73
|
const loaded = await loadConfigFromFile({ command: 'serve', mode: 'development' }, undefined, process.cwd())
|
|
29
|
-
const
|
|
30
|
-
|
|
74
|
+
const rawPlugins = loaded?.config?.plugins ?? []
|
|
75
|
+
const resolvedNested = await Promise.all(rawPlugins.map(resolvePluginEntry))
|
|
76
|
+
const userPlugins = resolvedNested
|
|
77
|
+
.flat(Infinity)
|
|
31
78
|
.filter(Boolean)
|
|
32
79
|
.filter(p => !INCOMPATIBLE_PLUGIN_NAMES.has(p.name))
|
|
33
80
|
// vue()/quasar() пакета замінюємо власними екземплярами у фіксованому порядку вище —
|
|
34
|
-
// не дублюємо; решта плагінів пакета (auto-import,
|
|
35
|
-
.filter(p => p.name
|
|
81
|
+
// не дублюємо; решта плагінів пакета (auto-import, VueMacros macro-sugar тощо) лишається.
|
|
82
|
+
.filter(p => !isVueTransformFamily(p.name) && p.name !== 'quasar')
|
|
36
83
|
|
|
37
84
|
return mergeConfig(storybookConfig, {
|
|
38
85
|
resolve: loaded?.config?.resolve,
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: fix-vitest-config.mjs
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/vitest-config/fix-vitest-config.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 35b10da8
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/vitest-config/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 543716c9
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 85
|
|
10
9
|
issues: internal-name:detectStoriesGlob,anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
findTestObject,
|
|
22
22
|
LEADING_COMMA_RE,
|
|
23
23
|
parseModule,
|
|
24
|
+
PLAYWRIGHT_PROVIDER_IMPORT,
|
|
24
25
|
REASON_STORYBOOK_PROJECT_MISSING,
|
|
25
26
|
REASON_STRYKER_CONFIG_MISSING,
|
|
26
27
|
REASON_UNIT_PROJECT_MISSING,
|
|
@@ -145,14 +146,29 @@ function arrayInsertBeforeClose(arr, indent, entries) {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
/**
|
|
148
|
-
* Гарантує наявність import-у
|
|
149
|
-
*
|
|
150
|
-
*
|
|
149
|
+
* Гарантує наявність import-у на самому початку файлу — ESM толерантний до
|
|
150
|
+
* порядку import-ів, тож prepend завжди синтаксично коректний незалежно від
|
|
151
|
+
* наявних import-ів.
|
|
151
152
|
* @param {string} src вихідний текст
|
|
153
|
+
* @param {string} marker підрядок, наявність якого означає "import уже є"
|
|
154
|
+
* @param {string} importLine рядок import-у (з `\n` наприкінці), що вставляється, якщо `marker` відсутній
|
|
152
155
|
* @returns {string} текст з гарантованим import-ом
|
|
153
156
|
*/
|
|
154
|
-
function
|
|
155
|
-
return src.includes(
|
|
157
|
+
function ensureImport(src, marker, importLine) {
|
|
158
|
+
return src.includes(marker) ? src : `${importLine}${src}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Гарантує наявність обох import-ів, потрібних новому storybook-запису
|
|
163
|
+
* `test.projects`: `storybookTest` (`@storybook/addon-vitest/vitest-plugin`) і
|
|
164
|
+
* `playwright`-provider factory (`@vitest/browser-playwright`, vitest@^4 —
|
|
165
|
+
* рядкове API `provider: 'playwright'` застаріле).
|
|
166
|
+
* @param {string} src вихідний текст
|
|
167
|
+
* @returns {string} текст з гарантованими import-ами
|
|
168
|
+
*/
|
|
169
|
+
function ensureStorybookEntryImports(src) {
|
|
170
|
+
const withStorybookTest = ensureImport(src, '@storybook/addon-vitest/vitest-plugin', STORYBOOK_TEST_IMPORT)
|
|
171
|
+
return ensureImport(withStorybookTest, '@vitest/browser-playwright', PLAYWRIGHT_PROVIDER_IMPORT)
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
/**
|
|
@@ -224,7 +240,7 @@ async function computeVitestConfigAugment(absPkgDir, vitestConfigPath) {
|
|
|
224
240
|
if (!plan) return null
|
|
225
241
|
|
|
226
242
|
let next = applyEdits(src, [plan.edit])
|
|
227
|
-
if (plan.needsImport) next =
|
|
243
|
+
if (plan.needsImport) next = ensureStorybookEntryImports(next)
|
|
228
244
|
|
|
229
245
|
// Safety: результат має компілюватися; інакше не пишемо (fail-closed, як у test/stryker_config).
|
|
230
246
|
let recheck
|
|
@@ -18,6 +18,9 @@ const VITE_CONFIG_NAMES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'
|
|
|
18
18
|
/** Import, який має бути присутній у файлі, щоб `storybookTest(...)` резолвився. */
|
|
19
19
|
export const STORYBOOK_TEST_IMPORT = "import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'\n"
|
|
20
20
|
|
|
21
|
+
/** Import, який має бути присутній у файлі, щоб `playwright(...)`-factory (vitest@^4) резолвилась. */
|
|
22
|
+
export const PLAYWRIGHT_PROVIDER_IMPORT = "import { playwright } from '@vitest/browser-playwright'\n"
|
|
23
|
+
|
|
21
24
|
// Стабільні reasons (namespace: ruleId/concernId/reason).
|
|
22
25
|
export const REASON_VITEST_CONFIG_MISSING = 'vitest-config-missing'
|
|
23
26
|
export const REASON_STRYKER_CONFIG_MISSING = 'stryker-config-missing'
|
|
@@ -29,12 +32,20 @@ export const REASON_STORYBOOK_PROJECT_MARKER_MISSING = 'storybook-project-marker
|
|
|
29
32
|
|
|
30
33
|
// Маркери канонічного storybook-проєкту (текстовий пошук у зрізі джерела самого
|
|
31
34
|
// елемента масиву — стійко до форматування/AST-варіацій, як і scaffold/main.mjs).
|
|
32
|
-
//
|
|
35
|
+
// Експортовані — переюз у `adopt/main.mjs`.
|
|
33
36
|
const UNIT_NAME_RE = /name\s*:\s*['"]unit['"]/u
|
|
34
37
|
const STORYBOOK_NAME_RE = /name\s*:\s*['"]storybook['"]/u
|
|
35
38
|
export const CHROMIUM_RE = /chromium/u
|
|
36
39
|
export const BROWSER_KEY_RE = /\bbrowser\s*:/u
|
|
37
40
|
export const STORIES_RE = /stories/iu
|
|
41
|
+
// `storybookTest({ configDir: ... })` без явного `include` — легітимний патерн:
|
|
42
|
+
// Storybook підхоплює stories-glob автоматично зі своєї `.storybook/main.js`-конфігурації
|
|
43
|
+
// (той самий stories-glob, що й `detectStoriesGlob`), явний include у vitest-конфізі не
|
|
44
|
+
// обов'язковий (реальний кейс components/npm/vitest.config.js — пілот adopt-діагностики).
|
|
45
|
+
export const STORYBOOK_TEST_CONFIG_DIR_RE = /storybookTest\([^)]*configDir/u
|
|
46
|
+
// vitest@^4: `browser.provider` — factory-виклик (`playwright()` з `@vitest/browser-playwright`),
|
|
47
|
+
// не рядок `'playwright'` (застаріле API попередніх мажорів).
|
|
48
|
+
export const PROVIDER_FACTORY_RE = /provider\s*:\s*playwright\s*\(/u
|
|
38
49
|
const STORIES_GLOB_PREFIX_RE = /^\.\.\//u
|
|
39
50
|
// Module-scope (prefer-static-regex): рядок-відступ цілком whitespace; leading
|
|
40
51
|
// кома (можливо з whitespace) — спільні з fix-vitest-config.mjs патерном augment-у.
|
|
@@ -173,6 +184,22 @@ export function classifyProjects(src, arr) {
|
|
|
173
184
|
return { hasUnit, storybookSlice }
|
|
174
185
|
}
|
|
175
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Чи присутній валідний маркер джерела stories у зрізі `storybook`-проєкту:
|
|
189
|
+
* явний stories-glob (`include: [...]`, {@link STORIES_RE}) АБО виклик
|
|
190
|
+
* `storybookTest({ configDir })` без явного `include` — Storybook підхоплює glob
|
|
191
|
+
* автоматично зі своєї конфігурації, явний include не обов'язковий (реальний
|
|
192
|
+
* кейс components/npm/vitest.config.js — пілот adopt-діагностики). Раніше гола
|
|
193
|
+
* вимога підрядка `stories` давала хибний позитив на цьому валідному патерні.
|
|
194
|
+
* Спільна логіка для `main.mjs` (lint) і `adopt/main.mjs` (diff-діагностика) —
|
|
195
|
+
* не дублювати комбінацію двох regex у двох місцях.
|
|
196
|
+
* @param {string} storybookSlice текстовий зріз елемента `storybook` у `test.projects`
|
|
197
|
+
* @returns {boolean} true — маркер джерела stories присутній (явно чи неявно)
|
|
198
|
+
*/
|
|
199
|
+
export function hasStoriesMarker(storybookSlice) {
|
|
200
|
+
return STORIES_RE.test(storybookSlice) || STORYBOOK_TEST_CONFIG_DIR_RE.test(storybookSlice)
|
|
201
|
+
}
|
|
202
|
+
|
|
176
203
|
/**
|
|
177
204
|
* Будує спільний контекст перевірки одного пакета (label/relPrefix/шлях vitest-конфіга).
|
|
178
205
|
* @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
|
|
@@ -272,7 +299,11 @@ async function checkVitestConfigContent(pkgCtx, reporter) {
|
|
|
272
299
|
const missingHints = []
|
|
273
300
|
if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
|
|
274
301
|
if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
|
|
275
|
-
if (!
|
|
302
|
+
if (!hasStoriesMarker(storybookSlice))
|
|
303
|
+
missingHints.push('stories-джерело (include або storybookTest({ configDir }))')
|
|
304
|
+
if (!PROVIDER_FACTORY_RE.test(storybookSlice)) {
|
|
305
|
+
missingHints.push("provider-factory (vitest v4: import { playwright } from '@vitest/browser-playwright')")
|
|
306
|
+
}
|
|
276
307
|
if (missingHints.length > 0) {
|
|
277
308
|
reporter.fail(
|
|
278
309
|
`[${label}] ${relVitestFile}: storybook-project без канонічних маркерів — бракує: ${missingHints.join(', ')} (vitest-config.mdc)`,
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: storybook-project-entry.js
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/vitest-config/template/storybook-project-entry.js
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 2646d55c
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: vitest.config.baseline.mjs
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/vitest-config/template/vitest.config.baseline.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 89da594d
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
|
-
tier: cloud-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Канонічний вигляд запису `storybook`-проєкту у `test.projects` (storybook.mdc,
|
|
2
2
|
// vitest-config-концерн, ADR Кластер 5): browser-mode, лише chromium, stories-glob
|
|
3
3
|
// (`__STORYBOOK_STORIES_GLOB__` — токен, `detectStoriesGlob` зі scaffold-концерна
|
|
4
|
-
// без префіксу `../`).
|
|
4
|
+
// без префіксу `../`). `provider: playwright()` — factory-виклик з
|
|
5
|
+
// `@vitest/browser-playwright`, не рядок `'playwright'`: vitest@^4 прибрав рядкове
|
|
6
|
+
// API провайдера, `@vitest/browser-playwright` — окремий пакет (не входить у
|
|
7
|
+
// `@vitest/browser`). Сам файл — валідний модуль лише для JS-лінту репозиторію
|
|
5
8
|
// правил; `fix-vitest-config.mjs` зчитує лише вміст `export default {...}`.
|
|
6
9
|
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
|
|
10
|
+
import { playwright } from '@vitest/browser-playwright'
|
|
7
11
|
|
|
8
12
|
export default {
|
|
9
13
|
extends: true,
|
|
@@ -14,7 +18,7 @@ export default {
|
|
|
14
18
|
browser: {
|
|
15
19
|
enabled: true,
|
|
16
20
|
headless: true,
|
|
17
|
-
provider:
|
|
21
|
+
provider: playwright(),
|
|
18
22
|
instances: [{ browser: 'chromium' }]
|
|
19
23
|
},
|
|
20
24
|
setupFiles: ['.storybook/vitest.setup.js']
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineConfig, mergeConfig } from 'vitest/config'
|
|
2
2
|
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
|
|
3
|
+
import { playwright } from '@vitest/browser-playwright'
|
|
3
4
|
import viteConfig from './__VITE_CONFIG_IMPORT__'
|
|
4
5
|
|
|
5
6
|
// Канонічний vitest-конфіг Vue-компонентної бібліотеки у скоупі Storybook
|
|
@@ -25,7 +26,7 @@ export default mergeConfig(
|
|
|
25
26
|
browser: {
|
|
26
27
|
enabled: true,
|
|
27
28
|
headless: true,
|
|
28
|
-
provider:
|
|
29
|
+
provider: playwright(),
|
|
29
30
|
instances: [{ browser: 'chromium' }]
|
|
30
31
|
},
|
|
31
32
|
setupFiles: ['.storybook/vitest.setup.js']
|
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
Vue-компонентна бібліотека вже має `vitest.config.{mjs,js,ts}` за каноном `vue/testing.mdc` (`mergeConfig(viteConfig, defineConfig({ test: {...} }))` з `environment: 'happy-dom'`). Storybook-хвиля **дописує** до цього `test`-блоку named-project `unit`+`storybook` — існуючі поля (`include`, `environment`, `coverage`) лишаються без змін, `projects` — нова властивість поруч:
|
|
8
8
|
|
|
9
9
|
- `{ extends: true, test: { name: 'unit' } }` — успадковує решту `test`-опцій пакета як є.
|
|
10
|
-
- `storybook`-проєкт — **browser-mode, лише chromium** (`browser: { enabled: true, provider:
|
|
10
|
+
- `storybook`-проєкт — **browser-mode, лише chromium** (`browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium' }] }`) і `include` на stories-glob пакета (та сама layout-детекція, що й `scaffold/main.mjs#detectStoriesGlob`, лише без префіксу `../` — vitest-конфіг лежить у корені пакета, а не в `.storybook/`).
|
|
11
11
|
|
|
12
12
|
Firefox/webkit свідомо не додаються — ADR і main.mdc хвилі 1 фіксують лише chromium; розширення на інші браузери — поза обсягом цього концерну.
|
|
13
13
|
|
|
14
|
+
**`provider` — factory-виклик, не рядок.** vitest@^4 прибрав рядкове API browser-провайдера: `provider: 'playwright'` більше не резолвиться, потрібен `import { playwright } from '@vitest/browser-playwright'` і `provider: playwright()` (окремий npm-пакет, не входить у `@vitest/browser`). Канон і автофікс генерують факторі-форму; рядкова форма (застаріле API попередніх мажорів) — `storybook-project-marker-missing`.
|
|
15
|
+
|
|
16
|
+
**Явний `include` не обов'язковий, якщо є `storybookTest({ configDir })`.** Реальний кейс пілота (components/npm/vitest.config.js): `storybook`-проєкт без явного `include` — Storybook підхоплює stories-glob автоматично зі своєї `.storybook/main.js`-конфігурації. Перевірка (`hasStoriesMarker`) визнає запис валідним, якщо є АБО явний stories-glob, АБО виклик `storybookTest({ configDir: ... })`.
|
|
17
|
+
|
|
14
18
|
### Чому окремий `vitest.stryker.config`
|
|
15
19
|
|
|
16
20
|
`@stryker-mutator/vitest-runner` крашиться, коли конфіг, який він завантажує, містить browser-mode `projects` (сам факт присутності `storybook`-проєкту в основному vitest-конфізі ламає mutation-прогін, незалежно від того, чи Stryker фактично торкається storybook-файлів). Тому концерн генерує **сусідній** ізольований `vitest.stryker.config.<ext>` — той самий `mergeConfig(viteConfig, defineConfig({ test: {...} }))`, але **без** `projects`/browser-mode: лише unit-набір (`include`, `happy-dom`, `coverage`), на якому mutation-testing працює штатно.
|
|
@@ -19,7 +23,7 @@ Firefox/webkit свідомо не додаються — ADR і main.mdc хви
|
|
|
19
23
|
|
|
20
24
|
### Що перевіряє й що виправляє концерн
|
|
21
25
|
|
|
22
|
-
Перевірка (`main.mjs`) — AST-аналіз (`oxc-parser`) наявного vitest-конфіга: знаходить `test`-блок (незалежно від того, чи він огорнутий у `defineConfig`/`mergeConfig`, чи це простий об'єкт), дивиться на `test.projects` і класифікує елементи за `name: 'unit'`/`name: 'storybook'`; для `storybook`-елемента перевіряє маркери `chromium`/`browser
|
|
26
|
+
Перевірка (`main.mjs`) — AST-аналіз (`oxc-parser`) наявного vitest-конфіга: знаходить `test`-блок (незалежно від того, чи він огорнутий у `defineConfig`/`mergeConfig`, чи це простий об'єкт), дивиться на `test.projects` і класифікує елементи за `name: 'unit'`/`name: 'storybook'`; для `storybook`-елемента перевіряє маркери `chromium`/`browser`/`provider`-factory/stories-джерело (явний glob або `storybookTest({ configDir })`) текстовим пошуком. Стійка до форматування й порядку полів — не вимагає конкретного стилю запису, лише семантичну присутність маркерів.
|
|
23
27
|
|
|
24
28
|
Автофікс (`fix-vitest-config.mjs`, fixability `config`) виправляє й дописує **лише відсутнє**:
|
|
25
29
|
|
|
@@ -30,4 +34,4 @@ Firefox/webkit свідомо не додаються — ADR і main.mdc хви
|
|
|
30
34
|
|
|
31
35
|
Як і в `test/stryker_config`, правка — точкові string-splice-и (insert-only) поверх існуючого тексту, а не переписаний AST-serializer, щоб не зачепити форматування й коментарі решти файлу; результат повторно парситься, і при невалідному виводі запис скасовується (fail-closed).
|
|
32
36
|
|
|
33
|
-
**Свідомо без автофіксу:** `test.projects` — не статичний масив (spread/змінна), test-блок не вдалось знайти структурно, або `storybook`-запис уже присутній, але без якогось із маркерів (chromium/browser/stories
|
|
37
|
+
**Свідомо без автофіксу:** `test.projects` — не статичний масив (spread/змінна), test-блок не вдалось знайти структурно, або `storybook`-запис уже присутній, але без якогось із маркерів (chromium/browser/provider-factory/stories-джерело) — надійно виправити частковий вручну написаний запис програмно ризиковано, тож це лишається порушенням для ручного review (той самий компроміс, що й marker-порушення в `scaffold`).
|