@7n/rules-lang-js 0.11.0 → 0.12.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 +11 -0
- package/package.json +1 -1
- package/rules/bun/package_json/package_json.rego +11 -1
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +1 -1
- package/rules/npm-module/npm_package_json/npm_package_json.rego +14 -5
- package/rules/storybook/adopt/docs/main.md +1 -1
- package/rules/storybook/adopt/main.mjs +32 -3
- package/rules/storybook/ci/concern.json +2 -2
- package/rules/storybook/ci/template/lint-storybook.yml.snippet.yml +1 -1
- package/rules/storybook/hygiene/concern.json +1 -1
- package/rules/storybook/main.json +1 -1
- package/rules/storybook/main.mdc +17 -2
- package/rules/storybook/scaffold/concern.json +1 -1
- package/rules/storybook/scaffold/docs/fix-scaffold.md +1 -1
- package/rules/storybook/scaffold/docs/main.md +1 -1
- package/rules/storybook/scaffold/fix-scaffold.mjs +50 -0
- package/rules/storybook/scaffold/main.mjs +31 -1
- package/rules/storybook/scaffold/template/docs/empty-vite.config.md +28 -0
- package/rules/storybook/scaffold/template/docs/index.md +5 -4
- package/rules/storybook/scaffold/template/docs/main.md +1 -1
- package/rules/storybook/scaffold/template/docs/preview.md +1 -2
- package/rules/storybook/scaffold/template/empty-vite.config.js +18 -0
- package/rules/storybook/scaffold/template/main.js +23 -0
- package/rules/storybook/scaffold/template/preview.js +13 -5
- package/rules/storybook/vitest-config/concern.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.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
|
+
- canon Storybook: viteConfigPath-обхід (empty-vite.config.js), валідний iconSet-імпорт, mswLoader замість mswDecorator, повний .storybook/**-glob у CI/lint, вирівняні governance-піни (storybook ^10.5.3, root Vite build-tooling deps), knip-виключення для .storybook-артефактів
|
|
13
|
+
|
|
3
14
|
## [0.11.0] - 2026-07-21
|
|
4
15
|
|
|
5
16
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules-lang-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -71,9 +71,15 @@ deny contains msg if {
|
|
|
71
71
|
# окремо для змістовних E2E-сценаріїв (n-vue.mdc).
|
|
72
72
|
# @storybook/addon-vitest — постачає `storybookTest` для vitest-плагіна в канонічному
|
|
73
73
|
# vitest.config named-проекту "storybook" (той самий канон Storybook кластер 5); версія
|
|
74
|
-
# з лінійки Storybook
|
|
74
|
+
# з лінійки Storybook 10.x (узгоджена з `storybook`@^10.5.3, запіненим у
|
|
75
75
|
# npm_package_json.rego) — allowlist тут за іменем, точний пінінг версії root-tooling
|
|
76
76
|
# не робимо (на відміну від Storybook-identity-пакетів у npm/package.json).
|
|
77
|
+
# @storybook/builder-vite/@vitejs/plugin-vue/@quasar/vite-plugin — root-only build-tooling
|
|
78
|
+
# для канонічного `.storybook/main.js` (storybook.mdc scaffold: `core.builder`,
|
|
79
|
+
# `viteFinal`-плагіни `vue()`/`quasar()`); @quasar/extras — постачає material-icons
|
|
80
|
+
# CSS/fallback-набір для `.storybook/preview.js` (iconMapFn). Усі чотири звірено з
|
|
81
|
+
# реальним root package.json пілотного консюмера (components) — раніше були відсутні
|
|
82
|
+
# в allowlist, хоча канонічний скафолд Storybook уже на них покладався (реальний gap).
|
|
77
83
|
allowed_root_test_deps := {
|
|
78
84
|
"vitest",
|
|
79
85
|
"@vitest/coverage-v8",
|
|
@@ -84,6 +90,10 @@ allowed_root_test_deps := {
|
|
|
84
90
|
"@playwright/test",
|
|
85
91
|
"playwright",
|
|
86
92
|
"@storybook/addon-vitest",
|
|
93
|
+
"@storybook/builder-vite",
|
|
94
|
+
"@vitejs/plugin-vue",
|
|
95
|
+
"@quasar/vite-plugin",
|
|
96
|
+
"@quasar/extras",
|
|
87
97
|
"@7n/test",
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -62,7 +62,7 @@ FS-перевірки (наявність файлу зі шляху `types`, с
|
|
|
62
62
|
✓ Правильно — канонічний Storybook-devDep із зафіксованою версією (канон Storybook):
|
|
63
63
|
|
|
64
64
|
```json
|
|
65
|
-
{ "devDependencies": { "storybook": "
|
|
65
|
+
{ "devDependencies": { "storybook": "^10.5.3" } }
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
✗ Неправильно — Storybook-devDep присутній, але версія не збігається з каноном:
|
|
@@ -41,12 +41,21 @@ storybook_version_template := concat(" ", [
|
|
|
41
41
|
])
|
|
42
42
|
|
|
43
43
|
# Канонічні Storybook-devDeps (isStorybookRoot()-маркери, канон Storybook кластер 7):
|
|
44
|
-
#
|
|
45
|
-
#
|
|
44
|
+
# зафіксований точний РЯДОК версії, як він має бути записаний у package.json (включно з
|
|
45
|
+
# `^`, якщо канон його використовує) — єдине дозволене значення для кожного пакета.
|
|
46
|
+
# Оновлення — ручна правка цієї map, після емпіричної верифікації (звірка з реальним
|
|
47
|
+
# консюмером, що пройшов повний прогін storybook build + тести на цих версіях).
|
|
48
|
+
#
|
|
49
|
+
# storybook/@storybook/vue3-vite піднято з 9.1.10 (exact) на ^10.5.3 — пілотний консюмер
|
|
50
|
+
# (components/npm/package.json) реально пройшов storybook build + 58/58 тестів саме на
|
|
51
|
+
# 10.5.3 з caret-діапазоном, а не на exact-пінованій 9.1.10 (яка на практиці ніде вже не
|
|
52
|
+
# встановлюється). @storybook/vue3 — не окремий devDep пілота (транзитивна залежність
|
|
53
|
+
# @storybook/vue3-vite), версія вирівняна до тієї самої лінійки про запас (allowlist за
|
|
54
|
+
# іменем, presence не обов'язковий).
|
|
46
55
|
storybook_canon_dev_deps := {
|
|
47
|
-
"storybook": "
|
|
48
|
-
"@storybook/vue3-vite": "
|
|
49
|
-
"@storybook/vue3": "
|
|
56
|
+
"storybook": "^10.5.3",
|
|
57
|
+
"@storybook/vue3-vite": "^10.5.3",
|
|
58
|
+
"@storybook/vue3": "^10.5.3",
|
|
50
59
|
"msw": "2.11.3",
|
|
51
60
|
"msw-storybook-addon": "2.0.5",
|
|
52
61
|
}
|
|
@@ -3,7 +3,7 @@ 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: 986ffd02
|
|
7
7
|
model: openai-codex/gpt-5.4-mini
|
|
8
8
|
score: 90
|
|
9
9
|
issues: anchor-miss:(mocking.mdc),anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Adopt-режим канону Storybook (ADR канон-storybook-для-vue-компонентних-бібліотек,
|
|
3
3
|
* Кластер 8; main.mdc). Для пакетів, де ВЖЕ є ручний `.storybook/`, що не збігається з
|
|
4
4
|
* каноном, — діагностика diff по секціях проти канонічних `template/` (main.js,
|
|
5
|
-
* preview.js, mocks/gql-sse.js, package.json#scripts.storybook,
|
|
6
|
-
* vitest.stryker.config), БЕЗ сліпого перезапису розбіжних файлів. Автофікс
|
|
5
|
+
* preview.js, empty-vite.config.js, mocks/gql-sse.js, package.json#scripts.storybook,
|
|
6
|
+
* vitest test.projects, vitest.stryker.config), БЕЗ сліпого перезапису розбіжних файлів. Автофікс
|
|
7
7
|
* (`--fix-missing`) — лише для секцій, яких немає ВЗАГАЛІ (той самий рендер, що й
|
|
8
8
|
* T0-фікс concern-ів `scaffold`/`vitest-config` — переюз, не дублювання шаблонування).
|
|
9
9
|
*
|
|
@@ -21,7 +21,7 @@ import { isRunAsCli } from '@7n/rules/scripts/cli-entry.mjs'
|
|
|
21
21
|
|
|
22
22
|
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
23
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'
|
|
24
|
+
import { renderEmptyViteConfig, renderMainJs, renderMocksGqlSse, renderPreviewJs } from '../scaffold/fix-scaffold.mjs'
|
|
25
25
|
import { buildFreshVitestConfig, buildStrykerConfig } from '../vitest-config/fix-vitest-config.mjs'
|
|
26
26
|
import {
|
|
27
27
|
BROWSER_KEY_RE,
|
|
@@ -43,6 +43,7 @@ export const STATUS = Object.freeze({ MATCH: 'match', DIFFER: 'differ', MISSING:
|
|
|
43
43
|
export const SECTION = Object.freeze({
|
|
44
44
|
MAIN_JS: 'main.js',
|
|
45
45
|
PREVIEW_JS: 'preview.js',
|
|
46
|
+
EMPTY_VITE_CONFIG: 'empty-vite.config.js',
|
|
46
47
|
MOCKS_GQL_SSE: 'mocks/gql-sse.js',
|
|
47
48
|
PACKAGE_SCRIPT: 'package.json#scripts.storybook',
|
|
48
49
|
VITEST_PROJECTS: 'vitest test.projects (unit+storybook)',
|
|
@@ -104,6 +105,30 @@ function diagnoseMocksSection(entry) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Діагностика `.storybook/empty-vite.config.js` — verbatim-порівняння з канонічним
|
|
110
|
+
* стенд-ін файлом (не залежить від пакета, як і `mocks/gql-sse.js`): `main.js` посилається
|
|
111
|
+
* на нього напряму через `core.builder.options.viteConfigPath`, тому розбіжність тут
|
|
112
|
+
* ламає обхід autodiscovery `@storybook/builder-vite`, навіть якщо `main.js` канонічний.
|
|
113
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
114
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
115
|
+
*/
|
|
116
|
+
function diagnoseEmptyViteConfigSection(entry) {
|
|
117
|
+
const relPath = '.storybook/empty-vite.config.js'
|
|
118
|
+
const file = relFileFor(entry, relPath)
|
|
119
|
+
const abs = join(entry.absDir, relPath)
|
|
120
|
+
if (!existsSync(abs)) return { name: SECTION.EMPTY_VITE_CONFIG, file, status: STATUS.MISSING }
|
|
121
|
+
const actual = readFileSync(abs, 'utf8')
|
|
122
|
+
const canonical = renderEmptyViteConfig()
|
|
123
|
+
if (actual === canonical) return { name: SECTION.EMPTY_VITE_CONFIG, file, status: STATUS.MATCH }
|
|
124
|
+
return {
|
|
125
|
+
name: SECTION.EMPTY_VITE_CONFIG,
|
|
126
|
+
file,
|
|
127
|
+
status: STATUS.DIFFER,
|
|
128
|
+
detail: 'вміст відрізняється від канонічного стенд-іна (storybook.mdc) — має лишатись порожнім defineConfig({})'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
107
132
|
/**
|
|
108
133
|
* Діагностика `package.json#scripts.storybook`.
|
|
109
134
|
* @param {{ pkg: Record<string, unknown>, rootDir: string }} entry запис пакета
|
|
@@ -219,6 +244,7 @@ export async function diagnosePackage(entry) {
|
|
|
219
244
|
const sections = [
|
|
220
245
|
diagnoseMarkerFile(entry, '.storybook/main.js', SECTION.MAIN_JS, MAIN_JS_MARKERS),
|
|
221
246
|
diagnoseMarkerFile(entry, '.storybook/preview.js', SECTION.PREVIEW_JS, PREVIEW_JS_MARKERS),
|
|
247
|
+
diagnoseEmptyViteConfigSection(entry),
|
|
222
248
|
diagnoseMocksSection(entry),
|
|
223
249
|
diagnoseScriptSection(entry),
|
|
224
250
|
diagnoseVitestProjectsSection(entry),
|
|
@@ -255,6 +281,9 @@ export async function fixMissingSections(entry, sections) {
|
|
|
255
281
|
if (byName.get(SECTION.PREVIEW_JS)?.status === STATUS.MISSING) {
|
|
256
282
|
writeFileEnsureDir(join(entry.absDir, '.storybook/preview.js'), renderPreviewJs())
|
|
257
283
|
}
|
|
284
|
+
if (byName.get(SECTION.EMPTY_VITE_CONFIG)?.status === STATUS.MISSING) {
|
|
285
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/empty-vite.config.js'), renderEmptyViteConfig())
|
|
286
|
+
}
|
|
258
287
|
if (byName.get(SECTION.MOCKS_GQL_SSE)?.status === STATUS.MISSING) {
|
|
259
288
|
writeFileEnsureDir(join(entry.absDir, '.storybook/mocks/gql-sse.js'), renderMocksGqlSse())
|
|
260
289
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": { "glob": ["package.json", "**/*.vue", "
|
|
1
|
+
{ "auto": { "glob": ["**/package.json", "**/*.vue", "**/.storybook/**"] } }
|
package/rules/storybook/main.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Канонічний Storybook для Vue-компонентних бібліотек — скафолд, vitest/Stryker, гігієна залежностей, рецепти мокання
|
|
3
3
|
version: '1.0'
|
|
4
|
-
globs: ["package.json", "**/*.vue", "
|
|
4
|
+
globs: ["**/package.json", "**/*.vue", "**/.storybook/**"]
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -32,13 +32,26 @@ alwaysApply: false
|
|
|
32
32
|
- **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
|
|
33
33
|
- **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
|
|
34
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
|
+
- **`core.builder.options.viteConfigPath` на `.storybook/empty-vite.config.js` — ОБОВ'ЯЗКОВИЙ, не опційний.** Емпірично підтверджено (лише через `storybook build`, `dev --smoke-test` не ловить): без явного `viteConfigPath` `@storybook/builder-vite` сам через `loadConfigFromFile` знаходить `../vite.config.js` пакета ЩЕ ДО виклику `viteFinal` і домерджує його НЕФІЛЬТРОВАНІ плагіни в `storybookConfig` — фільтр у `viteFinal` тоді лише ДОДАЄ ще один `@vitejs/plugin-vue`, а не прибирає вже змерджений builder-vite дублікат (подвійна SFC-трансформація, `storybook build` падає на кожному `.vue`). `empty-vite.config.js` — канонічний порожній `defineConfig({})`-стенд-ін, генерується скафолдом поряд з `main.js`; окрема секція перевірки/adopt-діагностики (не частина `MAIN_JS_MARKERS` — main.js може бути канонічним, а сусідній файл видалено окремо).
|
|
35
36
|
- **`staticDirs`** покриває `.storybook/public` — статичний asset для msw service worker (`preview.js`).
|
|
36
|
-
- **`preview.js`**: повний `Quasar`-install (не тільки окремі компоненти) + `iconSet`+`iconMapFn`-комбо — без цієї пари внутрішні Quasar-компоненти (напр. стрілка `QSelect`) не резолвлять вбудовані іконки поза full CLI build. `msw-storybook-addon` ініціалізується з `onUnhandledRequest`-фільтром: **same-origin GET мовчки пропускається** (Vite HMR/asset-шум), решта — попередження (не білд-помилка — навмисно м'яко для хвилі 1).
|
|
37
|
+
- **`preview.js`**: повний `Quasar`-install (не тільки окремі компоненти) + `iconSet`+`iconMapFn`-комбо — без цієї пари внутрішні Quasar-компоненти (напр. стрілка `QSelect`) не резолвлять вбудовані іконки поза full CLI build. Обидва `iconSet` — підшляховий default-імпорт (`quasar/icon-set/svg-material-icons`, `quasar/icon-set/material-icons`), **НЕ** named-import `iconSet` напряму з пакета `quasar` — quasar 2.18.x не має такого runtime-binding (лише компонент `IconSet` з великої літери), `@quasar/vite-plugin`-transform падає на такому специфікаторі. `msw-storybook-addon` ініціалізується з `onUnhandledRequest`-фільтром: **same-origin GET мовчки пропускається** (Vite HMR/asset-шум), решта — попередження (не білд-помилка — навмисно м'яко для хвилі 1). Мережевий мок підключається через `loaders: [mswLoader]`, **не** `decorators: [mswDecorator]` — `mswDecorator` deprecated у `msw-storybook-addon` 2.x (буде видалений у наступному релізі), `mswLoader` виконується per-story до рендеру.
|
|
37
38
|
- **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
|
|
38
39
|
- **`package.json#scripts.storybook`** — уніфікований скрипт, однаковий для всіх пакетів у скоупі (значення — `scaffold/main.mjs`, не дублюється тут).
|
|
39
40
|
|
|
40
41
|
Перевірка присутності й ключових маркерів канону — `scaffold/main.mjs`; детерміноване відтворення відсутніх файлів із `scaffold/template/` — `scaffold/fix-scaffold.mjs` (fixability: `config` — канонічна форма одна, LLM у ланцюжку фіксу не потрібен).
|
|
41
42
|
|
|
43
|
+
### knip-виключення для `.storybook/`-артефактів
|
|
44
|
+
|
|
45
|
+
Кожен adopt цього канону наступає на ту саму knip-проблему: `empty-vite.config.js` (динамічний шлях через `join(dirName, …)` у `main.js` — knip не резолвить `join`-конструйовані шляхи), `mocks/apollo.js` (точковий alias-мок, `mocking.mdc` — не кожен пакет його використовує) і `mocks/gql-sse.js` (helper, статично імпортується лише зі story-файлів, яких у щойно заскафолженому пакеті може ще не бути) регулярно фолсяться knip-ом як orphan-files. Канонічний фікс — **docs-only** сніпет, не автофікс: механізм `js/check` copy-once канону `knip.json` (`n-js.mdc`) свідомо **не ревалідує** вміст після першого створення (консюмер вільно кастомізує), тож програмний patch наявного `knip.json` консюмера конфліктував би з цим дизайн-рішенням і вимагав би нового merge-механізму замість наявного copy-if-missing. Додай вручну в `knip.json` консюмера (root або поряд з першим `.storybook/`):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"ignore": ["**/.storybook/empty-vite.config.js", "**/.storybook/mocks/apollo.js", "**/.storybook/mocks/gql-sse.js"]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
(домердж у наявний масив `ignore`, не заміна). Звірено з реальним `knip.json` пілотного консюмера.
|
|
54
|
+
|
|
42
55
|
## Vitest-конфіг і Stryker-ізоляція (Кластер 5)
|
|
43
56
|
|
|
44
57
|
Канонічний `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`, не дублюється тут.
|
|
@@ -47,6 +60,8 @@ alwaysApply: false
|
|
|
47
60
|
|
|
48
61
|
`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
62
|
|
|
63
|
+
`workflow.on.push.paths` — **`**/.storybook/**`** (не `.storybook/**` без префіксу): пакет у скоупі майже завжди лежить не в корені репозиторію (`npm/.storybook/**` тощо), а GitHub Actions `paths`-глоб без `**/`-префіксу анкорить збіг до кореня репо й мовчки не спрацьовує на push у вкладений пакет. Той самий анкоринг-баг був і в усіх `concern.json#lint.glob`/`main.json#auto.glob`-патернах цього правила (`package.json`, `.storybook/**`, `vitest.config.*` без префіксу) — виправлено разом, звірено емпірично на пілотному консюмері (`components/.github/workflows/lint-storybook.yml`).
|
|
64
|
+
|
|
50
65
|
Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.
|
|
51
66
|
|
|
52
67
|
## Гігієна сторонніх залежностей (Кластер 6)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
|
|
3
3
|
"fixability": "config",
|
|
4
|
-
"lint": { "scope": "full", "glob": ["package.json", "**/*.vue", "
|
|
4
|
+
"lint": { "scope": "full", "glob": ["**/package.json", "**/*.vue", "**/.storybook/**"] }
|
|
5
5
|
}
|
|
@@ -49,6 +49,17 @@ export function renderMocksGqlSse(templateDir = TEMPLATE_DIR) {
|
|
|
49
49
|
return readFileSync(join(templateDir, 'mocks/gql-sse.js'), 'utf8')
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Вміст канонічного `.storybook/empty-vite.config.js` — verbatim з template (порожній
|
|
54
|
+
* стенд-ін для `core.builder.options.viteConfigPath` у `main.js`, не залежить від пакета).
|
|
55
|
+
* Експортовано — переюз у `adopt/main.mjs`.
|
|
56
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
57
|
+
* @returns {string} вміст `empty-vite.config.js`
|
|
58
|
+
*/
|
|
59
|
+
export function renderEmptyViteConfig(templateDir = TEMPLATE_DIR) {
|
|
60
|
+
return readFileSync(join(templateDir, 'empty-vite.config.js'), 'utf8')
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
/**
|
|
53
64
|
* Записує файл, створюючи батьківські каталоги й реєструючи запис для rollback.
|
|
54
65
|
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').FixContext} ctx fix-контекст рунга
|
|
@@ -83,6 +94,7 @@ export const patterns = [
|
|
|
83
94
|
|
|
84
95
|
const templateDir = join(ctx.concernDir, 'template')
|
|
85
96
|
const mocksTemplate = renderMocksGqlSse(templateDir)
|
|
97
|
+
const emptyViteConfigTemplate = renderEmptyViteConfig(templateDir)
|
|
86
98
|
|
|
87
99
|
const touchedFiles = []
|
|
88
100
|
for (const v of targets) {
|
|
@@ -98,10 +110,48 @@ export const patterns = [
|
|
|
98
110
|
writeScaffoldFile(ctx, mocksAbs, mocksTemplate)
|
|
99
111
|
touchedFiles.push(mocksAbs)
|
|
100
112
|
}
|
|
113
|
+
|
|
114
|
+
// empty-vite.config.js — main.js посилається на нього напряму (viteConfigPath),
|
|
115
|
+
// без нього щойно відтворений main.js неробочий; генерується разом (belt-and-suspenders
|
|
116
|
+
// з окремим 'storybook-scaffold-empty-vite-config' нижче, який покриває випадок, коли
|
|
117
|
+
// ЛИШЕ цей файл видалено, а main.js лишається канонічним).
|
|
118
|
+
const emptyViteConfigAbs = join(absPkgDir, '.storybook/empty-vite.config.js')
|
|
119
|
+
if (!existsSync(emptyViteConfigAbs)) {
|
|
120
|
+
writeScaffoldFile(ctx, emptyViteConfigAbs, emptyViteConfigTemplate)
|
|
121
|
+
touchedFiles.push(emptyViteConfigAbs)
|
|
122
|
+
}
|
|
101
123
|
}
|
|
102
124
|
return { touchedFiles, message: `.storybook/main.js: створено для ${targets.length} пакет(ів)` }
|
|
103
125
|
}
|
|
104
126
|
},
|
|
127
|
+
{
|
|
128
|
+
id: 'storybook-scaffold-empty-vite-config',
|
|
129
|
+
test: violations => violations.some(v => v.reason === 'missing-empty-vite-config'),
|
|
130
|
+
apply: (violations, ctx) => {
|
|
131
|
+
const targets = violations.filter(
|
|
132
|
+
v => v.reason === 'missing-empty-vite-config' && typeof v.data?.rootDir === 'string'
|
|
133
|
+
)
|
|
134
|
+
if (targets.length === 0 || !ctx.concernDir) return { touchedFiles: [] }
|
|
135
|
+
|
|
136
|
+
const emptyViteConfigTemplate = renderEmptyViteConfig(join(ctx.concernDir, 'template'))
|
|
137
|
+
|
|
138
|
+
const touchedFiles = []
|
|
139
|
+
for (const v of targets) {
|
|
140
|
+
const absPkgDir = resolvePkgDir(ctx.cwd, v.data.rootDir)
|
|
141
|
+
const abs = join(absPkgDir, '.storybook/empty-vite.config.js')
|
|
142
|
+
if (existsSync(abs)) continue
|
|
143
|
+
writeScaffoldFile(ctx, abs, emptyViteConfigTemplate)
|
|
144
|
+
touchedFiles.push(abs)
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
touchedFiles,
|
|
148
|
+
message:
|
|
149
|
+
touchedFiles.length > 0
|
|
150
|
+
? `.storybook/empty-vite.config.js: створено для ${touchedFiles.length} пакет(ів)`
|
|
151
|
+
: undefined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
105
155
|
{
|
|
106
156
|
id: 'storybook-scaffold-preview-js',
|
|
107
157
|
test: violations => violations.some(v => v.reason === 'missing-preview-js'),
|
|
@@ -26,6 +26,10 @@ export const MAIN_JS_MARKERS = [
|
|
|
26
26
|
{
|
|
27
27
|
token: 'resolvePluginEntry',
|
|
28
28
|
hint: 'resolve/flatten Promise/масиву плагінів перед фільтрацією (VueMacros повертає Promise)'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
token: 'viteConfigPath',
|
|
32
|
+
hint: 'core.builder.options.viteConfigPath на empty-vite.config.js (блокує builder-vite autodiscovery vite.config пакета — інакше подвійна SFC-трансформація на storybook build)'
|
|
29
33
|
}
|
|
30
34
|
]
|
|
31
35
|
|
|
@@ -35,7 +39,17 @@ export const PREVIEW_JS_MARKERS = [
|
|
|
35
39
|
{ token: 'iconSet', hint: 'iconSet' },
|
|
36
40
|
{ token: 'iconMapFn', hint: 'iconMapFn (без нього внутрішні Quasar-іконки недоступні)' },
|
|
37
41
|
{ token: 'msw-storybook-addon', hint: 'msw-storybook-addon' },
|
|
38
|
-
{ token: 'onUnhandledRequest', hint: 'onUnhandledRequest-фільтр' }
|
|
42
|
+
{ token: 'onUnhandledRequest', hint: 'onUnhandledRequest-фільтр' },
|
|
43
|
+
{ token: 'mswLoader', hint: 'mswLoader (не mswDecorator — deprecated у msw-storybook-addon 2.x)' }
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Маркери канону `.storybook/empty-vite.config.js` (сусідній файл main.js — стенд-ін для
|
|
48
|
+
* `core.builder.options.viteConfigPath`, блокує autodiscovery `vite.config` пакета
|
|
49
|
+
* `@storybook/builder-vite`-ом). Експортовано — переюз у `adopt/main.mjs`.
|
|
50
|
+
*/
|
|
51
|
+
export const EMPTY_VITE_CONFIG_MARKERS = [
|
|
52
|
+
{ token: 'defineConfig', hint: 'порожній defineConfig({}) — стенд-ін для viteConfigPath' }
|
|
39
53
|
]
|
|
40
54
|
|
|
41
55
|
/**
|
|
@@ -162,6 +176,22 @@ async function checkPackageScaffold({ rootDir, absDir, pkg }, reporter) {
|
|
|
162
176
|
reporter
|
|
163
177
|
)
|
|
164
178
|
|
|
179
|
+
// empty-vite.config.js — сусідній файл, на який main.js посилається через
|
|
180
|
+
// core.builder.options.viteConfigPath; без нього посилання в main.js "розбите" навіть
|
|
181
|
+
// якщо сам main.js канонічний (маркер viteConfigPath присутній), тому перевіряється
|
|
182
|
+
// окремо, а не лише як частина MAIN_JS_MARKERS.
|
|
183
|
+
await checkCanonFile(
|
|
184
|
+
absDir,
|
|
185
|
+
'.storybook/empty-vite.config.js',
|
|
186
|
+
EMPTY_VITE_CONFIG_MARKERS,
|
|
187
|
+
'missing-empty-vite-config',
|
|
188
|
+
'empty-vite-config-marker-missing',
|
|
189
|
+
label,
|
|
190
|
+
rootDir,
|
|
191
|
+
`${relPrefix}.storybook/empty-vite.config.js`,
|
|
192
|
+
reporter
|
|
193
|
+
)
|
|
194
|
+
|
|
165
195
|
const scriptValue = pkg?.scripts?.storybook
|
|
166
196
|
if (scriptValue !== STORYBOOK_SCRIPT) {
|
|
167
197
|
const pkgJsonRel = `${relPrefix}package.json`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: empty-vite.config.js
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/empty-vite.config.js
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5c9f8904
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Канонічний порожній `Vite`-конфіг як стенд-ін для `core.builder.options.viteConfigPath` у `.storybook/main.js`. Його відсутність або поломка змушує `@storybook/builder-vite` через `loadConfigFromFile` знайти пакетний `vite.config.js` і додатково змішати нефільтровані плагіни ще до `viteFinal`; `mergeConfig` конкатенує масиви `plugins`, тож фільтр у `viteFinal` не прибирає вже доданий дублікат `@vitejs/plugin-vue`, а лише додає ще один. Без цього файлу `storybook build` падає на кожному `.vue` з `At least one <template> or <script> is required` через подвійну SFC-трансформацію; `dev --smoke-test` цього не виявляє, це підтверджує лише повний `build`. `npx @7n/rules fix storybook` відтворює цей файл, якщо він видалений або зламаний канон.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. Файл виступає канонічним порожнім `Vite`-конфігом для `storybook` і використовується як явна точка підключення в `core.builder.options.viteConfigPath`.
|
|
21
|
+
2. Якщо канон пошкоджено або файл зник, `npx @7n/rules fix storybook` відтворює його, щоб зберегти узгоджений режим збірки.
|
|
22
|
+
3. Файл не додає власної поведінки до `Vite`-збірки; його роль — заблокувати неявне підхоплення пакетного `vite.config.js` з боку `@storybook/builder-vite`.
|
|
23
|
+
4. Це прибирає ризик домішування сторонніх `Vite`-плагінів у `storybook build`, які інакше можуть бути обʼєднані до `viteFinal` і спричинити подвійну обробку `Vue`-компонентів.
|
|
24
|
+
5. Завдяки цьому `storybook build` поводиться стабільно на `.vue`-файлах у бібліотеках компонентів; без такого стенд-іну повний build може падати, тоді як `dev --smoke-test` цього не виявляє.
|
|
25
|
+
|
|
26
|
+
## Гарантії поведінки
|
|
27
|
+
|
|
28
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -4,7 +4,8 @@ title: plugins/lang-js/rules/storybook/scaffold/template
|
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/scaffold/template/
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
| Файл
|
|
8
|
-
|
|
|
9
|
-
| [
|
|
10
|
-
| [
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| -------------------------------------------- | --------- |
|
|
9
|
+
| [empty-vite.config.js](empty-vite.config.md) | JS Module |
|
|
10
|
+
| [main.js](main.md) | JS Module |
|
|
11
|
+
| [preview.js](preview.md) | JS Module |
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: preview.js
|
|
4
4
|
resource: plugins/lang-js/rules/storybook/scaffold/template/preview.js
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 445c773b
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.96
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний ПОРОЖНІЙ Vite-конфіг — стенд-ін для `core.builder.options.viteConfigPath` у
|
|
3
|
+
* `.storybook/main.js` (storybook.mdc, ADR канон-storybook-для-vue-компонентних-бібліотек).
|
|
4
|
+
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
|
+
*
|
|
6
|
+
* Навіщо: `@storybook/builder-vite` без явного `viteConfigPath` сам через
|
|
7
|
+
* `loadConfigFromFile` знаходить `vite.config.js` ПАКЕТА і домерджує його НЕФІЛЬТРОВАНІ
|
|
8
|
+
* плагіни (той самий `vue()`, обгорнутий VueMacros/unplugin-auto-import) ще ДО виклику
|
|
9
|
+
* `viteFinal` у `main.js` — `mergeConfig` конкатенує масиви `plugins`, тож фільтр у
|
|
10
|
+
* `viteFinal` лише ДОДАЄ ще один `@vitejs/plugin-vue`, а не прибирає вже змерджений
|
|
11
|
+
* дублікат від builder-vite. Наслідок без цього файлу: `storybook build` (саме `build`,
|
|
12
|
+
* не `dev`!) падає на кожному `.vue` з "At least one <template> or <script> is required" —
|
|
13
|
+
* подвійна SFC-трансформація. `dev --smoke-test` цю різницю НЕ ловить (лінива
|
|
14
|
+
* трансформація), лише повний `build`. Перевірено емпірично на пілотному консюмері.
|
|
15
|
+
*/
|
|
16
|
+
import { defineConfig } from 'vite'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({})
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
* канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
|
|
4
4
|
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
5
|
*/
|
|
6
|
+
import process from 'node:process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
6
9
|
import { loadConfigFromFile, mergeConfig } from 'vite'
|
|
7
10
|
import vue from '@vitejs/plugin-vue'
|
|
8
11
|
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
|
9
12
|
|
|
13
|
+
// Абсолютний каталог `.storybook/` (де лежить сам main.js) — потрібен нижче для
|
|
14
|
+
// `core.builder.options.viteConfigPath` (порожній стенд-ін лежить поруч).
|
|
15
|
+
const dirName = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
|
|
10
17
|
// Плагіни власного Vite-збирача Storybook — порядок ФІКСОВАНИЙ: @vitejs/plugin-vue
|
|
11
18
|
// ПЕРЕД quasar() (інакше Quasar-плагін не бачить SFC, уже скомпільований plugin-vue).
|
|
12
19
|
const OWN_PLUGINS = [vue({ template: { transformAssetUrls } }), quasar({ sassVariables: true })]
|
|
@@ -69,6 +76,22 @@ const config = {
|
|
|
69
76
|
},
|
|
70
77
|
// Публічний asset для msw service worker, який ініціалізує preview.js.
|
|
71
78
|
staticDirs: ['./public'],
|
|
79
|
+
core: {
|
|
80
|
+
builder: {
|
|
81
|
+
name: '@storybook/builder-vite',
|
|
82
|
+
// ОБОВ'ЯЗКОВИЙ обхід (емпірично підтверджено — лише через `storybook build`, dev
|
|
83
|
+
// smoke-test не ловить): без цього `@storybook/builder-vite` сам через
|
|
84
|
+
// `loadConfigFromFile` знаходить `../vite.config.js` пакета ще ДО виклику
|
|
85
|
+
// `viteFinal` нижче й домерджує його НЕФІЛЬТРОВАНІ плагіни (VueMacros-обгорнутий
|
|
86
|
+
// `vue()`, layout-роутинг тощо) у storybookConfig. `mergeConfig` конкатенує масиви
|
|
87
|
+
// `plugins`, тож фільтр у `viteFinal` лише ДОДАЄ ще один `@vitejs/plugin-vue`, а НЕ
|
|
88
|
+
// прибирає вже змерджений builder-vite дублікат — подвійна SFC-трансформація,
|
|
89
|
+
// `storybook build` падає на кожному `.vue` з "At least one <template> or <script>
|
|
90
|
+
// is required". `viteConfigPath` на порожній стенд-ін (`empty-vite.config.js`,
|
|
91
|
+
// канонічний сусідній файл цього скафолда) блокує це autodiscovery повністю.
|
|
92
|
+
options: { viteConfigPath: join(dirName, 'empty-vite.config.js') }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
72
95
|
async viteFinal(storybookConfig) {
|
|
73
96
|
const loaded = await loadConfigFromFile({ command: 'serve', mode: 'development' }, undefined, process.cwd())
|
|
74
97
|
const rawPlugins = loaded?.config?.plugins ?? []
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
* канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
|
|
4
4
|
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
5
|
*/
|
|
6
|
-
import { setup } from '@storybook/vue3'
|
|
7
|
-
import { Dialog,
|
|
6
|
+
import { setup } from '@storybook/vue3-vite'
|
|
7
|
+
import { Dialog, Notify, Quasar } from 'quasar'
|
|
8
8
|
import iconSet from 'quasar/icon-set/svg-material-icons'
|
|
9
|
-
|
|
9
|
+
// Імпорт через підшлях пакета — НЕ named-import `iconSet` напряму з `quasar`: quasar
|
|
10
|
+
// 2.18.x не має цього імені як runtime-binding (лише компонент `IconSet` з великої
|
|
11
|
+
// літери), і `@quasar/vite-plugin`-transform падає з "Unknown import from Quasar" на
|
|
12
|
+
// такому специфікаторі. Підшлях (як і кастомний `iconSet` вище) обходить transform повністю.
|
|
13
|
+
import quasarBuiltinIconSet from 'quasar/icon-set/material-icons'
|
|
14
|
+
import { initialize, mswLoader } from 'msw-storybook-addon'
|
|
10
15
|
|
|
11
16
|
import 'quasar/dist/quasar.css'
|
|
12
17
|
|
|
@@ -36,9 +41,12 @@ setup(app => {
|
|
|
36
41
|
})
|
|
37
42
|
})
|
|
38
43
|
|
|
39
|
-
/** @type {import('@storybook/vue3').Preview} */
|
|
44
|
+
/** @type {import('@storybook/vue3-vite').Preview} */
|
|
40
45
|
const preview = {
|
|
41
|
-
|
|
46
|
+
// mswLoader (не mswDecorator — deprecated у msw-storybook-addon 2.x, буде видалений
|
|
47
|
+
// у наступному релізі): loaders виконуються per-story ДО рендеру й інтегруються з
|
|
48
|
+
// async story-рендерингом Storybook, на відміну від декоратора.
|
|
49
|
+
loaders: [mswLoader],
|
|
42
50
|
parameters: {
|
|
43
51
|
controls: { matchers: { color: /(background|color)$/iu, date: /Date$/u } }
|
|
44
52
|
}
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
"fixability": "config",
|
|
4
4
|
"lint": {
|
|
5
5
|
"scope": "full",
|
|
6
|
-
"glob": ["package.json", "**/*.vue", "
|
|
6
|
+
"glob": ["**/package.json", "**/*.vue", "**/.storybook/**", "**/vitest.config.*", "**/vitest.stryker.config.*"]
|
|
7
7
|
}
|
|
8
8
|
}
|