@7n/rules-lang-js 0.11.0 → 0.13.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/rules/bun/package_json/package_json.rego +11 -1
  4. package/rules/npm-module/npm_package_json/npm_package_json.mdc +1 -1
  5. package/rules/npm-module/npm_package_json/npm_package_json.rego +14 -5
  6. package/rules/storybook/adopt/docs/main.md +1 -1
  7. package/rules/storybook/adopt/main.mjs +32 -3
  8. package/rules/storybook/ci/concern.json +2 -2
  9. package/rules/storybook/ci/template/lint-storybook.yml.snippet.yml +1 -1
  10. package/rules/storybook/hygiene/concern.json +1 -1
  11. package/rules/storybook/main.json +1 -1
  12. package/rules/storybook/main.mdc +25 -4
  13. package/rules/storybook/scaffold/concern.json +1 -1
  14. package/rules/storybook/scaffold/docs/fix-scaffold.md +1 -1
  15. package/rules/storybook/scaffold/docs/main.md +1 -1
  16. package/rules/storybook/scaffold/fix-scaffold.mjs +50 -0
  17. package/rules/storybook/scaffold/main.mjs +31 -1
  18. package/rules/storybook/scaffold/template/docs/empty-vite.config.md +28 -0
  19. package/rules/storybook/scaffold/template/docs/index.md +5 -4
  20. package/rules/storybook/scaffold/template/docs/main.md +1 -1
  21. package/rules/storybook/scaffold/template/docs/preview.md +1 -2
  22. package/rules/storybook/scaffold/template/empty-vite.config.js +18 -0
  23. package/rules/storybook/scaffold/template/main.js +23 -0
  24. package/rules/storybook/scaffold/template/preview.js +13 -5
  25. package/rules/storybook/scope/docs/main.md +1 -2
  26. package/rules/storybook/scope/main.mjs +6 -17
  27. package/rules/storybook/vitest-config/concern.json +1 -1
  28. package/rules/storybook/vitest-config/docs/fix-vitest-config.md +1 -1
  29. package/rules/storybook/vitest-config/docs/main.md +1 -1
  30. package/rules/storybook/vitest-config/fix-vitest-config.mjs +23 -6
  31. package/rules/storybook/vitest-config/main.mjs +7 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.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
+ - storybook: скоуп-детекція більше не вимагає vite.config.* пакета (source-only Vue-бібліотеки, tauri-components/npm rollout) — hasStandardBuild прибрано, vitest-config fix толерує відсутній vite.config
14
+
15
+ ## [0.12.0] - 2026-07-21
16
+
17
+ ### Added
18
+
19
+ - 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`
20
+
21
+ ### Fixed
22
+
23
+ - 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`.
24
+ - 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-артефактів
25
+
3
26
  ## [0.11.0] - 2026-07-21
4
27
 
5
28
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-lang-js",
3
- "version": "0.11.0",
3
+ "version": "0.13.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 9.x (узгоджена з `storybook`@9.1.10, запіненим у
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": "9.1.10" } }
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
- # ручна правка цієї map.
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": "9.1.10",
48
- "@storybook/vue3-vite": "9.1.10",
49
- "@storybook/vue3": "9.1.10",
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: c8e6e421
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, vitest test.projects,
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
  }
@@ -5,9 +5,9 @@
5
5
  "lint": {
6
6
  "scope": "full",
7
7
  "glob": [
8
- "package.json",
8
+ "**/package.json",
9
9
  "**/*.vue",
10
- ".storybook/**",
10
+ "**/.storybook/**",
11
11
  ".github/actions/setup-playwright-chromium/action.yml",
12
12
  ".github/workflows/lint-storybook.yml"
13
13
  ]
@@ -7,7 +7,7 @@ on:
7
7
  - main
8
8
  paths:
9
9
  - '**/*.stories.@(js|ts)'
10
- - '.storybook/**'
10
+ - '**/.storybook/**'
11
11
  pull_request:
12
12
  branches:
13
13
  - dev
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "lint": { "scope": "full", "glob": ["package.json", "**/*.vue", ".storybook/**"] }
3
+ "lint": { "scope": "full", "glob": ["**/package.json", "**/*.vue", "**/.storybook/**"] }
4
4
  }
@@ -1 +1 @@
1
- { "auto": { "glob": ["package.json", "**/*.vue", ".storybook/**"] } }
1
+ { "auto": { "glob": ["**/package.json", "**/*.vue", "**/.storybook/**"] } }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Канонічний Storybook для Vue-компонентних бібліотек — скафолд, vitest/Stryker, гігієна залежностей, рецепти мокання
3
3
  version: '1.0'
4
- globs: ["package.json", "**/*.vue", ".storybook/**"]
4
+ globs: ["**/package.json", "**/*.vue", "**/.storybook/**"]
5
5
  alwaysApply: false
6
6
  ---
7
7
 
@@ -15,13 +15,17 @@ alwaysApply: false
15
15
 
16
16
  ## Скоуп
17
17
 
18
- Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати. Пакети без розпізнаваного `vite.config.{js,ts,mjs}` (нестандартний build) пропускаються мовчки — автоматичний скафолд спирається на `viteConfigPath` пакета.
18
+ Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати.
19
+
20
+ **Наявність `vite.config.{js,ts,mjs}` пакета — НЕ умова скоупу** (хвиля 1.4, фікс за результатами rollout-у на `tauri-components/npm`). До хвилі 1.4 тут була функція `hasStandardBuild` — вона мовчки виключала зі скоупу пакети без власного `vite.config.*` ("skip нестандартного build", ADR Кластер 1). На практиці це виключало легітимні source-only Vue-бібліотеки (peerDependencies.vue + достатньо `.vue`-файлів, просто без окремого Vite-білду) — а канонічний скафолд для них працює без жодних змін: `viteConfigPath` у `.storybook/main.js` завжди вказує на власний `empty-vite.config.js` скафолда (не на `vite.config.*` пакета), а `viteFinal` викликає `loadConfigFromFile`, який толерує відсутній конфіг пакета (повертає `null`, `viteFinal` мерджить порожній список плагінів). Емпірично перевірено на `tauri-components/npm`: `storybook build` + повний прогін тестів у реальному chromium — без жодного `vite.config.*` у пакеті. `hasStandardBuild` прибрано з `scope/main.mjs` разом з усіма згадками; для пакета, де канонічний скафолд дійсно не підходить (справжня екзотика білду — не сам факт відсутності Vite-конфіга), власник додає його у `storybook.optOut` вручну — окрема автоматична детекція "нестандартного build" дублювала б наявний opt-out і давала б хибні спрацювання.
19
21
 
20
22
  Опційно (вимкнено за замовчуванням) — app-проєкти (`vue` у `dependencies`, не бібліотека, + `src/pages/`). Детекція реалізована в `scope/main.mjs`, але не викликається доки в `.n-rules.json` не задано прапорець хвилі 2 — рішення розширювати скоуп на app-проєкти залишається відкритим питанням ADR.
21
23
 
22
24
  **Opt-out:** окремий пакет можна виключити зі скоупу через `.n-rules.json` → `storybook.optOut: string[]` (root dir пакета, той самий формат що й у виводі workspace-роутингу — `.` для кореня, `packages/ui` тощо). Виняток — намір, не помилка: якщо в `optOut` вказано неіснуючий workspace-пакет, `scope`-концерн репортує це як застаріле налаштування.
23
25
 
24
- Логіка детекції (поріг, opt-out, нестандартний build, app-проєкти) — у `scope/main.mjs`, не дублюється тут.
26
+ Логіка детекції (поріг, opt-out, app-проєкти) — у `scope/main.mjs`, не дублюється тут.
27
+
28
+ `vitest-config`-концерн (Кластер 5, нижче) генерує baseline `vitest.config.mjs`/`vitest.stryker.config.mjs` з `import viteConfig from './<vite.config.*>'` — для source-only пакета без `vite.config.*` це зламало б import неіснуючого файлу; `vitest-config/fix-vitest-config.mjs#applyViteConfigImport` підставляє замість import-у порожній локальний `const viteConfig = {}` (еквівалент для `mergeConfig`), решта baseline-структури не змінюється.
25
29
 
26
30
  ## Канонічний скафолд
27
31
 
@@ -32,13 +36,28 @@ alwaysApply: false
32
36
  - **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
33
37
  - **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
34
38
  - **`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`.
39
+ - **`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
40
  - **`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).
41
+ - **`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
42
  - **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
38
43
  - **`package.json#scripts.storybook`** — уніфікований скрипт, однаковий для всіх пакетів у скоупі (значення — `scaffold/main.mjs`, не дублюється тут).
39
44
 
40
45
  Перевірка присутності й ключових маркерів канону — `scaffold/main.mjs`; детерміноване відтворення відсутніх файлів із `scaffold/template/` — `scaffold/fix-scaffold.mjs` (fixability: `config` — канонічна форма одна, LLM у ланцюжку фіксу не потрібен).
41
46
 
47
+ ### knip-виключення для `.storybook/`-артефактів
48
+
49
+ Кожен 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/`):
50
+
51
+ ```json
52
+ {
53
+ "ignore": ["**/.storybook/empty-vite.config.js", "**/.storybook/mocks/apollo.js", "**/.storybook/mocks/gql-sse.js"]
54
+ }
55
+ ```
56
+
57
+ (домердж у наявний масив `ignore`, не заміна). Звірено з реальним `knip.json` пілотного консюмера.
58
+
59
+ Симетрична ручна правка для `oxfmt`: `.storybook/public/mockServiceWorker.js` (згенерований пакетом `msw` файл, `npx msw init` — той самий service worker, на який вказує `staticDirs` у `main.js`) слід додати в `.oxfmtrc.json` консюмера в `ignorePatterns: ["**/mockServiceWorker.js"]` — реформатування псує pristine-стан цього файлу (той самий принцип, що й `**/adr/**`-виняток `oxfmt`/`cspell`, `docs/adr/adr-виключення-oxfmt-cspell.md`).
60
+
42
61
  ## Vitest-конфіг і Stryker-ізоляція (Кластер 5)
43
62
 
44
63
  Канонічний `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 +66,8 @@ alwaysApply: false
47
66
 
48
67
  `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
68
 
69
+ `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`).
70
+
50
71
  Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.
51
72
 
52
73
  ## Гігієна сторонніх залежностей (Кластер 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", ".storybook/**"] }
4
+ "lint": { "scope": "full", "glob": ["**/package.json", "**/*.vue", "**/.storybook/**"] }
5
5
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-scaffold.mjs
4
4
  resource: plugins/lang-js/rules/storybook/scaffold/fix-scaffold.mjs
5
5
  docgen:
6
- crc: 31a867f5
6
+ crc: 10336699
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 90
9
9
  issues: internal-name:detectStoriesGlob,judge:inaccurate:0.95
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/scaffold/main.mjs
5
5
  docgen:
6
- crc: 2cad0405
6
+ crc: 0505810e
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -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
- | [main.js](main.md) | JS Module |
10
- | [preview.js](preview.md) | JS Module |
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,7 +3,7 @@ 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: 74cf3397
6
+ crc: 18fbef4b
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.96
@@ -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: 5590ef55
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, iconSet as quasarBuiltinIconSet, Notify, Quasar } from 'quasar'
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
- import { initialize, mswDecorator } from 'msw-storybook-addon'
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
- decorators: [mswDecorator],
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,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/scope/main.mjs
5
5
  docgen:
6
- crc: 04c71162
6
+ crc: e6dab761
7
7
  model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
8
  score: 90
10
9
  issues: internal-name:isVueComponentLibraryPkg,judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -11,7 +11,6 @@ import { isVueComponentLibraryPkg } from '../../vue/packages/main.mjs'
11
11
 
12
12
  const CONFIG_FILE = '.n-rules.json'
13
13
  const LEGACY_CONFIG_FILE = '.n-cursor.json'
14
- const VITE_CONFIG_FILES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
15
14
 
16
15
  /** Поріг кількості `.vue`-файлів для скоупу канону Storybook (ADR Кластер 1). */
17
16
  export const VUE_FILE_THRESHOLD = 3
@@ -86,18 +85,6 @@ export async function countVueFiles(absPkgDir, ignorePaths) {
86
85
  return count
87
86
  }
88
87
 
89
- /**
90
- * Чи має пакет "стандартний" build — розпізнаваний `vite.config.{js,ts,mjs}` у корені пакета.
91
- * Канонічний `.storybook/main.js` спирається саме на цей файл (`viteFinal` мерджить його
92
- * плагіни) — без нього автоматичний скафолд неможливий, і пакет пропускається мовчки
93
- * (ADR Кластер 1: "skip пакетів із нестандартним build").
94
- * @param {string} absPkgDir абсолютний шлях кореня пакета
95
- * @returns {boolean} true, якщо знайдено відомий `vite.config.*`
96
- */
97
- export function hasStandardBuild(absPkgDir) {
98
- return VITE_CONFIG_FILES.some(f => existsSync(join(absPkgDir, f)))
99
- }
100
-
101
88
  /**
102
89
  * Чи є пакет app-проєктом (не бібліотекою) для хвилі 2: `vue` у `dependencies` (не лише
103
90
  * `peerDependencies`) і не бібліотека компонентів. Реалізовано зараз (щоб не переписувати
@@ -129,7 +116,6 @@ async function evaluateCandidate(rootDir, cwd, matches, ignorePaths) {
129
116
  return null
130
117
  }
131
118
  if (!matches(pkg)) return null
132
- if (!hasStandardBuild(absDir)) return null
133
119
  const vueFileCount = await countVueFiles(absDir, ignorePaths)
134
120
  if (vueFileCount < VUE_FILE_THRESHOLD) return null
135
121
  return { rootDir, absDir, pkg, vueFileCount }
@@ -138,8 +124,11 @@ async function evaluateCandidate(rootDir, cwd, matches, ignorePaths) {
138
124
  /**
139
125
  * Збирає workspace-пакети у скоупі канону Storybook хвилі 1: Vue-компонентна бібліотека
140
126
  * (`vue` у `peerDependencies`, маркер `isVueComponentLibraryPkg` — той самий, що й `vue.mdc`)
141
- * з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`, зі
142
- * стандартним build (`vite.config.*`). Хвиля 2 (app-проєкти) додається лише за явного
127
+ * з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`. Наявність
128
+ * `vite.config.*` пакета НЕ умова скоупу (rollout tauri-components/npm, хвиля 1.4):
129
+ * канонічний скафолд (`viteConfigPath` на `empty-vite.config.js`, `loadConfigFromFile`
130
+ * толерує відсутній конфіг) працює й для source-only Vue-бібліотек без власного Vite-білду
131
+ * — див. секцію "Скоуп" у `main.mdc`. Хвиля 2 (app-проєкти) додається лише за явного
143
132
  * прапорця `storybook.detectApps` у `.n-rules.json`.
144
133
  * @param {string} cwd абсолютний корінь репозиторію
145
134
  * @returns {Promise<InScopePackage[]>} пакети у скоупі
@@ -173,7 +162,7 @@ export async function collectInScopeVuePackages(cwd) {
173
162
  /**
174
163
  * Self-check конфігурації: `.n-rules.json` → `storybook.optOut` не має посилатись на
175
164
  * неіснуючі workspace-пакети (застаріле налаштування — пакет перейменували/видалили, а
176
- * opt-out лишився). Сама детекція скоупу (поріг, build, app-проєкти) — pure-функції вище,
165
+ * opt-out лишився). Сама детекція скоупу (поріг, app-проєкти) — pure-функції вище,
177
166
  * покриті тестами напряму; тут лише конфіг-гігієна.
178
167
  * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
179
168
  * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
@@ -3,6 +3,6 @@
3
3
  "fixability": "config",
4
4
  "lint": {
5
5
  "scope": "full",
6
- "glob": ["package.json", "**/*.vue", ".storybook/**", "vitest.config.*", "vitest.stryker.config.*"]
6
+ "glob": ["**/package.json", "**/*.vue", "**/.storybook/**", "**/vitest.config.*", "**/vitest.stryker.config.*"]
7
7
  }
8
8
  }
@@ -3,7 +3,7 @@ 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: 35b10da8
6
+ crc: f00f073b
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -3,7 +3,7 @@ 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: 543716c9
6
+ crc: ae5f59a0
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 85
9
9
  issues: internal-name:detectStoriesGlob,anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
@@ -38,6 +38,10 @@ const HERE = dirname(fileURLToPath(import.meta.url))
38
38
  const TEMPLATE_DIR = join(HERE, 'template')
39
39
  const STORIES_GLOB_TOKEN = '__STORYBOOK_STORIES_GLOB__'
40
40
  const VITE_CONFIG_IMPORT_TOKEN = '__VITE_CONFIG_IMPORT__'
41
+ // Рядок import-у як він буквально лежить у baseline-шаблонах (до token-підстановки) —
42
+ // для source-only пакета (без власного vite.config.*, хвиля 1.4) замінюється на порожній
43
+ // placeholder, а не на import неіснуючого файлу.
44
+ const VITE_CONFIG_IMPORT_LINE = `import viteConfig from './${VITE_CONFIG_IMPORT_TOKEN}'\n`
41
45
 
42
46
  const TRIGGER_REASONS = new Set([
43
47
  REASON_VITEST_CONFIG_MISSING,
@@ -254,6 +258,22 @@ async function computeVitestConfigAugment(absPkgDir, vitestConfigPath) {
254
258
  return next === src ? null : next
255
259
  }
256
260
 
261
+ /**
262
+ * Підставляє в baseline-шаблон або import наявного `vite.config.*` пакета (звичайний
263
+ * шлях), або — для source-only пакета без жодного `vite.config.*` (хвиля 1.4,
264
+ * `resolveViteConfigName` повертає `null`) — порожній локальний placeholder замість
265
+ * import-у неіснуючого файлу. `mergeConfig({}, defineConfig({...}))` еквівалентний
266
+ * самому `defineConfig({...})`, тож решта структури шаблону (сама `mergeConfig`-обгортка)
267
+ * лишається незмінною — не потрібен окремий "плоский" варіант шаблону.
268
+ * @param {string} template вихідний текст baseline-шаблону (з токеном `__VITE_CONFIG_IMPORT__`)
269
+ * @param {string | null} viteConfigName ім'я `vite.config.*` пакета або `null`
270
+ * @returns {string} текст з підставленим import-ом чи placeholder-ом
271
+ */
272
+ function applyViteConfigImport(template, viteConfigName) {
273
+ if (viteConfigName) return template.split(VITE_CONFIG_IMPORT_TOKEN).join(viteConfigName)
274
+ return template.replace(VITE_CONFIG_IMPORT_LINE, 'const viteConfig = {}\n')
275
+ }
276
+
257
277
  /**
258
278
  * Генерує повністю новий `vitest.config.mjs` (unit+storybook projects) для
259
279
  * пакета без жодного наявного vitest-конфіга. Експортовано — переюз у
@@ -265,11 +285,8 @@ export async function buildFreshVitestConfig(absPkgDir) {
265
285
  const template = await readFile(join(TEMPLATE_DIR, 'vitest.config.baseline.mjs'), 'utf8')
266
286
  const storiesGlob = storiesGlobForVitestConfig(absPkgDir)
267
287
  const viteConfigName = resolveViteConfigName(absPkgDir)
268
- const content = template
269
- .split(STORIES_GLOB_TOKEN)
270
- .join(storiesGlob)
271
- .split(VITE_CONFIG_IMPORT_TOKEN)
272
- .join(viteConfigName)
288
+ const withStories = template.split(STORIES_GLOB_TOKEN).join(storiesGlob)
289
+ const content = applyViteConfigImport(withStories, viteConfigName)
273
290
  return { path: join(absPkgDir, 'vitest.config.mjs'), content }
274
291
  }
275
292
 
@@ -283,7 +300,7 @@ export async function buildFreshVitestConfig(absPkgDir) {
283
300
  export async function buildStrykerConfig(absPkgDir) {
284
301
  const template = await readFile(join(TEMPLATE_DIR, 'vitest.stryker.config.baseline.mjs'), 'utf8')
285
302
  const viteConfigName = resolveViteConfigName(absPkgDir)
286
- return template.split(VITE_CONFIG_IMPORT_TOKEN).join(viteConfigName)
303
+ return applyViteConfigImport(template, viteConfigName)
287
304
  }
288
305
 
289
306
  /**
@@ -66,12 +66,16 @@ export function resolveVitestConfigPath(absPkgDir) {
66
66
  /**
67
67
  * Резолвить ім'я `vite.config.*` пакета для import-шляху в baseline-шаблонах
68
68
  * (той самий файл, що й `viteFinal` у `.storybook/main.js`). Пакет у скоупі
69
- * гарантовано має один із них (`scope/main.mjs#hasStandardBuild`).
69
+ * Storybook НЕ гарантовано має власний `vite.config.*` (хвиля 1.4 — вимогу
70
+ * `hasStandardBuild` прибрано зі скоуп-детекції, `scope/main.mjs`) — source-only
71
+ * Vue-бібліотека (напр. tauri-components/npm) законно потрапляє у скоуп без
72
+ * жодного `vite.config.*`. `null` — сигнал викликачу (`fix-vitest-config.mjs`)
73
+ * підставити порожній placeholder замість імпорту неіснуючого файлу.
70
74
  * @param {string} absPkgDir абсолютний шлях кореня пакета
71
- * @returns {string} ім'я файлу vite-конфіга (дефолт `vite.config.js`)
75
+ * @returns {string | null} ім'я файлу vite-конфіга або `null`, якщо жодного немає
72
76
  */
73
77
  export function resolveViteConfigName(absPkgDir) {
74
- return VITE_CONFIG_NAMES.find(n => existsSync(join(absPkgDir, n))) ?? 'vite.config.js'
78
+ return VITE_CONFIG_NAMES.find(n => existsSync(join(absPkgDir, n))) ?? null
75
79
  }
76
80
 
77
81
  /**