@7n/rules-lang-js 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/doc-files/docs/extractors.md +1 -1
  3. package/doc-files/docs/index.md +1 -0
  4. package/doc-files/docs/js-facts.md +47 -0
  5. package/doc-files/docs/units-js.md +1 -2
  6. package/doc-files/extractors.mjs +135 -12
  7. package/doc-files/js-facts.mjs +28 -0
  8. package/doc-files/units-js.mjs +26 -10
  9. package/package.json +1 -1
  10. package/rules/bun/package_json/package_json.mdc +3 -1
  11. package/rules/bun/package_json/package_json.rego +30 -1
  12. package/rules/npm-module/npm_package_json/npm_package_json.mdc +18 -3
  13. package/rules/npm-module/npm_package_json/npm_package_json.rego +42 -5
  14. package/rules/storybook/adopt/docs/index.md +9 -0
  15. package/rules/storybook/adopt/docs/main.md +46 -0
  16. package/rules/storybook/adopt/main.mjs +379 -0
  17. package/rules/storybook/hygiene/concern.json +4 -0
  18. package/rules/storybook/hygiene/docs/index.md +9 -0
  19. package/rules/storybook/hygiene/docs/main.md +45 -0
  20. package/rules/storybook/hygiene/main.mjs +254 -0
  21. package/rules/storybook/main.json +1 -0
  22. package/rules/storybook/main.mdc +60 -0
  23. package/rules/storybook/mocking/concern.json +3 -0
  24. package/rules/storybook/mocking/mocking.mdc +108 -0
  25. package/rules/storybook/scaffold/concern.json +5 -0
  26. package/rules/storybook/scaffold/docs/fix-scaffold.md +29 -0
  27. package/rules/storybook/scaffold/docs/index.md +10 -0
  28. package/rules/storybook/scaffold/docs/main.md +36 -0
  29. package/rules/storybook/scaffold/fix-scaffold.mjs +150 -0
  30. package/rules/storybook/scaffold/main.mjs +164 -0
  31. package/rules/storybook/scaffold/template/docs/index.md +10 -0
  32. package/rules/storybook/scaffold/template/docs/main.md +30 -0
  33. package/rules/storybook/scaffold/template/docs/preview.md +46 -0
  34. package/rules/storybook/scaffold/template/main.js +45 -0
  35. package/rules/storybook/scaffold/template/mocks/docs/gql-sse.md +36 -0
  36. package/rules/storybook/scaffold/template/mocks/docs/index.md +9 -0
  37. package/rules/storybook/scaffold/template/mocks/gql-sse.js +25 -0
  38. package/rules/storybook/scaffold/template/preview.js +47 -0
  39. package/rules/storybook/scope/concern.json +4 -0
  40. package/rules/storybook/scope/docs/index.md +9 -0
  41. package/rules/storybook/scope/docs/main.md +62 -0
  42. package/rules/storybook/scope/main.mjs +202 -0
  43. package/rules/storybook/vitest-config/concern.json +8 -0
  44. package/rules/storybook/vitest-config/docs/fix-vitest-config.md +38 -0
  45. package/rules/storybook/vitest-config/docs/index.md +10 -0
  46. package/rules/storybook/vitest-config/docs/main.md +72 -0
  47. package/rules/storybook/vitest-config/fix-vitest-config.mjs +340 -0
  48. package/rules/storybook/vitest-config/main.mjs +358 -0
  49. package/rules/storybook/vitest-config/template/docs/index.md +12 -0
  50. package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +34 -0
  51. package/rules/storybook/vitest-config/template/docs/unit-project-entry.md +30 -0
  52. package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +29 -0
  53. package/rules/storybook/vitest-config/template/docs/vitest.stryker.config.baseline.md +31 -0
  54. package/rules/storybook/vitest-config/template/storybook-project-entry.js +22 -0
  55. package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
  56. package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +37 -0
  57. package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
  58. package/rules/storybook/vitest-config/vitest-config.mdc +33 -0
@@ -0,0 +1,254 @@
1
+ /** @see ./docs/main.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join, relative } from 'node:path'
5
+
6
+ import { parseSync } from 'oxc-parser'
7
+
8
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
9
+ import { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
10
+ import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
11
+ import {
12
+ dynamicImportModule,
13
+ langFromPath,
14
+ requireCallModule,
15
+ walkAstWithAncestors
16
+ } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
17
+ import { contentForVueImportScan } from '@7n/rules/scripts/lib/js-source-signals.mjs'
18
+ import { isNodeBuiltinSpecifier } from '../../vue/lib/vue-forbidden-imports.mjs'
19
+ import { collectInScopeVuePackages } from '../scope/main.mjs'
20
+
21
+ const VUE_EXT_RE = /\.vue$/u
22
+
23
+ // Quasar CLI-конвенція за замовчуванням (quasar.dev/style/sass-scss-variables): плагін шукає
24
+ // саме цей файл, якщо `sassVariables` не задає власний шлях — .scss першим, .sass fallback-ом.
25
+ const SASS_VARIABLES_CANDIDATES = ['src/css/quasar.variables.scss', 'src/css/quasar.variables.sass']
26
+
27
+ // quasar({ sassVariables: true }) або quasar({ sassVariables: 'шлях' }) — обидві форми вмикають
28
+ // підключення SCSS-змінних; boolean false/відсутність поля — ні.
29
+ const SASS_VARIABLES_MARKER_RE = /sassVariables\s*:\s*(?:true|['"])/u
30
+
31
+ /**
32
+ * Віртуальний шлях для oxc-парсера: `.vue` розбирається як `.ts` (після витягу `<script>`-блоку),
33
+ * решта — за власним розширенням.
34
+ * @param {string} relPath шлях файлу (posix, відносно пакета)
35
+ * @returns {string} шлях для вибору `lang` парсером
36
+ */
37
+ function virtualPathForParse(relPath) {
38
+ return relPath.endsWith('.vue') ? relPath.replace(VUE_EXT_RE, '.ts') : relPath
39
+ }
40
+
41
+ /**
42
+ * Витягає import-specifier'и з `.vue` SFC (лише `<script>`-блоки) чи звичайного JS/TS-файлу:
43
+ * static import + dynamic `import()` + `require()`. Той самий oxc-parser pipeline, що й
44
+ * `js/dep-policy` і `vue/lib/vue-forbidden-imports.mjs` — лише для довільного specifier-а,
45
+ * не для конкретного заборонного списку.
46
+ * @param {string} content сирий вміст файлу
47
+ * @param {string} relPath шлях файлу (для вибору мови/віртуального шляху парсера)
48
+ * @returns {string[]} список import-specifier'ів (можуть повторюватись)
49
+ */
50
+ function extractImportSpecifiers(content, relPath) {
51
+ const scan = contentForVueImportScan(content, relPath)
52
+ const virtualPath = virtualPathForParse(relPath)
53
+ let parsed
54
+ try {
55
+ parsed = parseSync(virtualPath, scan, { lang: langFromPath(virtualPath), sourceType: 'module' })
56
+ } catch {
57
+ return []
58
+ }
59
+ if (parsed.errors?.length) return []
60
+
61
+ const out = []
62
+ for (const imp of parsed.module?.staticImports ?? []) {
63
+ if (typeof imp?.moduleRequest?.value === 'string') out.push(imp.moduleRequest.value)
64
+ }
65
+ const program = parsed.program
66
+ if (program && typeof program === 'object') {
67
+ walkAstWithAncestors(program, [], node => {
68
+ const dyn = dynamicImportModule(node)
69
+ if (dyn !== null) out.push(dyn)
70
+ const req = requireCallModule(node)
71
+ if (req !== null) out.push(req)
72
+ })
73
+ }
74
+ return out
75
+ }
76
+
77
+ /**
78
+ * Чи є specifier відносним імпортом чи псевдонімом шляху (не сторонній пакет): `./x`, `../x`,
79
+ * абсолютний шлях, чи типові Vite-аліаси `@/...`/`~/...` на `src/`. Автоімпорт-глобали
80
+ * (`ref`, `computed`, Quasar-композаблі через `unplugin-auto-import`) сюди не потрапляють —
81
+ * вони не є import-специфікаторами взагалі, AST їх не бачить.
82
+ * @param {string} spec значення `moduleRequest.value`
83
+ * @returns {boolean} `true`, якщо це не сторонній пакет
84
+ */
85
+ function isRelativeOrAliasSpecifier(spec) {
86
+ return spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('~/') || spec.startsWith('@/')
87
+ }
88
+
89
+ /**
90
+ * Ім'я пакета верхнього рівня зі specifier-а: враховує scoped-пакети (`@scope/name`) і
91
+ * subpath-імпорти (`pkg/sub/path` → `pkg`, `@scope/name/sub` → `@scope/name`).
92
+ * @param {string} spec сторонній import-specifier
93
+ * @returns {string} ім'я пакета для звірки з package.json deps
94
+ */
95
+ function topLevelPackageName(spec) {
96
+ if (spec.startsWith('@')) {
97
+ const parts = spec.split('/')
98
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec
99
+ }
100
+ const idx = spec.indexOf('/')
101
+ return idx === -1 ? spec : spec.slice(0, idx)
102
+ }
103
+
104
+ /**
105
+ * Множина задекларованих пакетів (`dependencies` + `peerDependencies`) — workspace-пакети
106
+ * (`@nitra/*`, `@7n/*`) не потребують окремої обробки: вони так само оголошуються тут
107
+ * (workspace-протокол), як і звичайні npm-залежності.
108
+ * @param {Record<string, unknown>} pkg розпарсений package.json пакета
109
+ * @returns {Set<string>} імена задекларованих пакетів
110
+ */
111
+ function collectDeclaredDeps(pkg) {
112
+ const names = new Set()
113
+ for (const field of ['dependencies', 'peerDependencies']) {
114
+ const obj = pkg?.[field]
115
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
116
+ for (const name of Object.keys(obj)) names.add(name)
117
+ }
118
+ }
119
+ return names
120
+ }
121
+
122
+ /**
123
+ * Збирає абсолютні шляхи всіх `.vue`-файлів у дереві пакета.
124
+ * @param {string} absDir абсолютний шлях кореня пакета
125
+ * @param {string[]} ignorePaths абсолютні шляхи, повністю виключені з обходу
126
+ * @returns {Promise<string[]>} відсортовані абсолютні шляхи `.vue`-файлів
127
+ */
128
+ async function collectVueFiles(absDir, ignorePaths) {
129
+ const files = []
130
+ await walkDir(
131
+ absDir,
132
+ p => {
133
+ if (p.endsWith('.vue')) files.push(p)
134
+ },
135
+ ignorePaths
136
+ )
137
+ return files
138
+ }
139
+
140
+ /**
141
+ * Будує posix-relative шлях від `cwd` для violation.file — `entry.rootDir` уже relative до `cwd`
142
+ * (`.` для кореня монорепо), `relFromPkg` — relative до `entry.absDir`.
143
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
144
+ * @param {string} relFromPkg posix-relative шлях від кореня пакета
145
+ * @returns {string} posix-relative шлях від `cwd`
146
+ */
147
+ function fileRelFromCwd(entry, relFromPkg) {
148
+ return entry.rootDir === '.' ? relFromPkg : `${entry.rootDir}/${relFromPkg}`
149
+ }
150
+
151
+ /**
152
+ * Перевіряє один пакет на undeclared third-party imports у `.vue`-файлах: import стороннього
153
+ * пакета, якого немає в `dependencies`/`peerDependencies` package.json цього ж пакета (реальний
154
+ * кейс ADR — зламаний default-export `@vuepic/vue-datepicker` v14, silent breakage без цієї
155
+ * перевірки). Відносні імпорти, аліаси (`@/`, `~/`), Node-builtin і auto-import глобали
156
+ * пропускаються.
157
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
158
+ * @param {string[]} ignorePaths абсолютні шляхи, виключені з обходу
159
+ * @param {ReturnType<typeof createViolationReporter>} reporter репортер порушень
160
+ * @returns {Promise<void>}
161
+ */
162
+ async function checkUndeclaredImportsForPackage(entry, ignorePaths, reporter) {
163
+ const declared = collectDeclaredDeps(entry.pkg)
164
+ const vueFiles = await collectVueFiles(entry.absDir, ignorePaths)
165
+
166
+ for (const absFile of vueFiles) {
167
+ const content = await readFile(absFile, 'utf8')
168
+ const relFromPkg = relative(entry.absDir, absFile).split('\\').join('/')
169
+ const specifiers = extractImportSpecifiers(content, relFromPkg)
170
+
171
+ const reportedForFile = new Set()
172
+ for (const spec of specifiers) {
173
+ if (isRelativeOrAliasSpecifier(spec) || isNodeBuiltinSpecifier(spec)) continue
174
+ const pkgName = topLevelPackageName(spec)
175
+ if (declared.has(pkgName) || reportedForFile.has(pkgName)) continue
176
+ reportedForFile.add(pkgName)
177
+
178
+ const fileRel = fileRelFromCwd(entry, relFromPkg)
179
+ reporter.fail(
180
+ `[undeclared-import] ${fileRel}: import '${spec}' — пакет '${pkgName}' відсутній у dependencies/peerDependencies ${entry.rootDir === '.' ? 'кореня монорепо' : entry.rootDir} (storybook.mdc hygiene)`,
181
+ {
182
+ reason: 'undeclared-import',
183
+ file: fileRel,
184
+ data: { rootDir: entry.rootDir, package: pkgName, specifier: spec }
185
+ }
186
+ )
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Чи має пакет глобальні Quasar SCSS-змінні — canonical шлях за замовчуванням
193
+ * (quasar.dev/style/sass-scss-variables): `src/css/quasar.variables.scss` (fallback `.sass`).
194
+ * @param {string} absDir абсолютний шлях кореня пакета
195
+ * @returns {boolean} `true`, якщо знайдено файл глобальних SCSS-змінних
196
+ */
197
+ function hasGlobalSassVariables(absDir) {
198
+ return SASS_VARIABLES_CANDIDATES.some(f => existsSync(join(absDir, f)))
199
+ }
200
+
201
+ /**
202
+ * Перевіряє один пакет на auto-detect глобальних SCSS-змінних: якщо в пакеті є
203
+ * `quasar.variables.scss`/`.sass`, а `.storybook/main.js` не вмикає `sassVariables` у
204
+ * `quasar()`-плагіні, глобальні SCSS-змінні недоступні у Storybook (тихий розсинхрон зі
205
+ * звичайним build). Рівень — `warn` (м'який сигнал, не гейт хвилі 1, аналогічно
206
+ * `onUnhandledRequest` у `preview.js`). Відсутність самого `.storybook/main.js` вже покриває
207
+ * `storybook/scaffold` — тут не дублюється.
208
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
209
+ * @param {ReturnType<typeof createViolationReporter>} reporter репортер порушень
210
+ * @returns {Promise<void>}
211
+ */
212
+ async function checkSassVariablesForPackage(entry, reporter) {
213
+ if (!hasGlobalSassVariables(entry.absDir)) return
214
+
215
+ const mainJsPath = join(entry.absDir, '.storybook/main.js')
216
+ if (!existsSync(mainJsPath)) return
217
+
218
+ const content = await readFile(mainJsPath, 'utf8')
219
+ if (SASS_VARIABLES_MARKER_RE.test(content)) return
220
+
221
+ const fileRel = fileRelFromCwd(entry, '.storybook/main.js')
222
+ reporter.fail(
223
+ `[sass-variables] ${fileRel}: пакет має глобальні Quasar SCSS-змінні (${SASS_VARIABLES_CANDIDATES.join(' | ')}), але quasar({ sassVariables }) не задано в .storybook/main.js (storybook.mdc hygiene)`,
224
+ { reason: 'missing-sass-variables', file: fileRel, severity: 'warn', data: { rootDir: entry.rootDir } }
225
+ )
226
+ }
227
+
228
+ /**
229
+ * Detector concern-а `storybook/hygiene`: для кожного Vue component library пакета у скоупі
230
+ * канону Storybook (`collectInScopeVuePackages`) — undeclared third-party imports у `.vue` та
231
+ * auto-detect глобальних Quasar SCSS-змінних без `sassVariables` у `.storybook/main.js`
232
+ * (storybook.mdc, ADR Кластер 6). Breaking-change guard при мажорному апгрейді
233
+ * third-party-пакетів свідомо не автоматизується — людський пункт, hygiene.mdc.
234
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
235
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
236
+ */
237
+ export async function lint(ctx) {
238
+ const reporter = createViolationReporter(ctx)
239
+ const cwd = ctx.cwd
240
+
241
+ const pkgs = await collectInScopeVuePackages(cwd)
242
+ if (pkgs.length === 0) {
243
+ reporter.pass('storybook hygiene: немає Vue component library пакетів у скоупі (storybook.mdc)')
244
+ return reporter.result()
245
+ }
246
+
247
+ const ignorePaths = await loadCursorIgnorePaths(cwd)
248
+ for (const entry of pkgs) {
249
+ await checkUndeclaredImportsForPackage(entry, ignorePaths, reporter)
250
+ await checkSassVariablesForPackage(entry, reporter)
251
+ }
252
+
253
+ return reporter.result()
254
+ }
@@ -0,0 +1 @@
1
+ { "auto": { "glob": ["package.json", "**/*.vue", ".storybook/**"] } }
@@ -0,0 +1,60 @@
1
+ ---
2
+ description: Канонічний Storybook для Vue-компонентних бібліотек — скафолд, vitest/Stryker, гігієна залежностей, рецепти мокання
3
+ version: '1.0'
4
+ globs: ["package.json", "**/*.vue", ".storybook/**"]
5
+ alwaysApply: false
6
+ ---
7
+
8
+ # Канон Storybook для Vue-компонентних бібліотек — хвиля 1
9
+
10
+ Джерело рішення: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`. Storybook впроваджувався вручну двічі в різних nitra-репо в одній сесії — обидва рази з тими самими невидимими заздалегідь проблемами (конфлікт `@vitejs/plugin-vue`/`quasar()`, недоступні внутрішні Quasar-іконки без `iconSet`+`iconMapFn`, ручне мокання мережі) і одним реальним merge-конфліктом між двома паралельними ручними реалізаціями тієї самої фічі. Це правило робить канонічний скафолд стандартом, а не одноразовим рішенням кожного агента.
11
+
12
+ **Хвильовий rollout:** `alwaysApply: false` — правило не гейтить CI, доки не ввімкнене явно в `.n-rules.json` консюмер-репо (`rules: ["storybook"]`). Хвиля 1 (це правило) покриває детекцію скоупу, канонічний скафолд `.storybook/`, vitest/Stryker-конфіг (Кластер 5), гігієну сторонніх залежностей (Кластер 6), рецепти мокання (docs-only, Кластер 3) і `--adopt`-режим rollout-у для вже впроваджених вручну пакетів (Кластер 8); **LLM-генерація args для stories — хвиля 2**, свідомо відкладена (ADR: ризик одночасного org-wide CI-блоку на необкатаному generation pipeline, на відміну від production-proven doc-files).
13
+
14
+ Усі concern-и хвилі 1 (`scope`/`scaffold`/`vitest-config`/`hygiene`) мають `lint`-поверхню в `concern.json` і виконуються під `npx @7n/rules lint storybook` — `scope`/`scaffold`/`vitest-config` раніше декларували лише `check: true` без `lint`-блоку і тому мовчки не підхоплювались unified lint-рушієм (`run-detectors.mjs` виконує лише concern-и з явним `lint` чи `policy` блоком); виправлено разом із введенням `adopt`-режиму.
15
+
16
+ ## Скоуп
17
+
18
+ Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати. Пакети без розпізнаваного `vite.config.{js,ts,mjs}` (нестандартний build) пропускаються мовчки — автоматичний скафолд спирається на `viteConfigPath` пакета.
19
+
20
+ Опційно (вимкнено за замовчуванням) — app-проєкти (`vue` у `dependencies`, не бібліотека, + `src/pages/`). Детекція реалізована в `scope/main.mjs`, але не викликається доки в `.n-rules.json` не задано прапорець хвилі 2 — рішення розширювати скоуп на app-проєкти залишається відкритим питанням ADR.
21
+
22
+ **Opt-out:** окремий пакет можна виключити зі скоупу через `.n-rules.json` → `storybook.optOut: string[]` (root dir пакета, той самий формат що й у виводі workspace-роутингу — `.` для кореня, `packages/ui` тощо). Виняток — намір, не помилка: якщо в `optOut` вказано неіснуючий workspace-пакет, `scope`-концерн репортує це як застаріле налаштування.
23
+
24
+ Логіка детекції (поріг, opt-out, нестандартний build, app-проєкти) — у `scope/main.mjs`, не дублюється тут.
25
+
26
+ ## Канонічний скафолд
27
+
28
+ Для кожного пакета в скоупі обов'язкові: `.storybook/main.js`, `.storybook/preview.js`, `package.json#scripts.storybook`. Канонічні шаблони — `scaffold/template/`.
29
+
30
+ Фіксовані рішення (деталі — ADR, Кластер 2):
31
+
32
+ - **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
33
+ - **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
34
+ - **`viteFinal`** зчитує `vite.config` самого пакета (той самий, що й для звичайного білду) і мерджить його плагіни в конфіг Storybook, **знімаючи** `vite-plugin-pages`/`vite-plugin-vue-layouts` — файлова маршрутизація належить додатку-споживачу, не ізольованому рендеру компонента.
35
+ - **`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
+ - **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
38
+ - **`package.json#scripts.storybook`** — уніфікований скрипт, однаковий для всіх пакетів у скоупі (значення — `scaffold/main.mjs`, не дублюється тут).
39
+
40
+ Перевірка присутності й ключових маркерів канону — `scaffold/main.mjs`; детерміноване відтворення відсутніх файлів із `scaffold/template/` — `scaffold/fix-scaffold.mjs` (fixability: `config` — канонічна форма одна, LLM у ланцюжку фіксу не потрібен).
41
+
42
+ ## Vitest-конфіг і Stryker-ізоляція (Кластер 5)
43
+
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
+
46
+ ## Гігієна сторонніх залежностей (Кластер 6)
47
+
48
+ `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`.
49
+
50
+ ## Мокання (Кластер 3, docs-only)
51
+
52
+ Рецепти router/`@nitra/tfm`/Apollo-GraphQL(MSW)/Pinia/сторінкових stories — `mocking/mocking.mdc`. Свідомо без механічної перевірки (`concern.json` без `check`/`policy`/`lint`-блоку, як і решта чисто-документаційних concern-ів репозиторію) — кожен пакет мокає свій набір залежностей по-своєму, детермінований чек дав би або хибні спрацювання, або нульове покриття.
53
+
54
+ ## Adopt-режим і скіл `n-storybook` (Кластер 8)
55
+
56
+ Скіл `npm/skills/storybook/` (`.cursor/skills/n-storybook/` після синку) — тонка обгортка запуску: звичайний режим — `npx @7n/rules lint storybook`; `--adopt` — окремий діагностичний JS-модуль `adopt/main.mjs` для пакетів, де ВЖЕ є ручний `.storybook/`, що не збігається з каноном. Adopt діагностує diff по секціях проти `template/` (main.js/preview.js/mocks/gql-sse.js/package.json#scripts.storybook/vitest test.projects/vitest.stryker.config) — статус `match`/`differ`/`missing` на секцію, **без сліпого перезапису** розбіжних файлів; автофікс (`--fix-missing`) генерує лише секції зі статусом `missing`. Circuit breaker: збій діагностики одного пакета деградує до `status: 'broken'` для нього, решта пакетів прогону обробляються далі. Викликається напряму (`bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs`), без окремого CLI-прапорця в ядрі `n-rules.js` — деталі й приклади звіту в `SKILL.md` скіла.
57
+
58
+ ## Що свідомо поза хвилею 1
59
+
60
+ LLM-генерація `args` для stories з `defineProps`/`defineEmits`/slots (Кластер 4 ADR) — хвиля 2, свідомо відкладена. Governance-винятки в `n-npm-module.mdc`/`n-bun.mdc` (Кластер 7 — Storybook-devDeps у `npm/package.json`, canonical version pin, review-гейт лише для нових stories) — окремий трек, не в обсязі цього правила.
@@ -0,0 +1,3 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json"
3
+ }
@@ -0,0 +1,108 @@
1
+ ## Мокання зовнішніх залежностей у Storybook
2
+
3
+ Джерело рішень: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`, Кластер 3 і розділ «Розширення (2026-07-20): сторінки — route.params + Apollo subscription + Pinia» (прототип-верифікація на реальному кейсі `gt`). Це docs-only концерн — рецепти для агента, що впроваджує/підтримує Storybook у консюмер-репо; механічної перевірки немає (кожен пакет мокає свій набір залежностей по-своєму, детермінований чек дав би або хибні спрацювання, або нульове покриття).
4
+
5
+ ### Router
6
+
7
+ Детекція: компонент використовує `useRoute`/`useRouter` (composition API з `vue-router`). Базовий рецепт — `createMemoryHistory()` з одним catch-all-маршрутом, без реальних params:
8
+
9
+ ```js
10
+ import { createMemoryHistory, createRouter } from 'vue-router'
11
+
12
+ const router = createRouter({
13
+ history: createMemoryHistory(),
14
+ routes: [{ path: '/:pathMatch(.*)*', component: { render: () => null } }]
15
+ })
16
+ ```
17
+
18
+ Для сторінок (хвиля 2, app-проєкти з `src/pages/`) — реальний параметризований маршрут замість catch-all, `router.push` перед mount і `await router.isReady()` у Storybook `loaders` (не в декораторі — `loaders` виконується per-story до рендеру, прибирає перший рендер без `route.params`):
19
+
20
+ ```js
21
+ const router = createRouter({
22
+ history: createMemoryHistory(),
23
+ routes: [{ path: '/task/:id', component: TaskPage }]
24
+ })
25
+
26
+ export const pageLoader = ({ args }) => async () => {
27
+ await router.push(`/task/${args.taskId}`)
28
+ await router.isReady()
29
+ return { router }
30
+ }
31
+ ```
32
+
33
+ ### `@nitra/tfm`
34
+
35
+ No-op. `@nitra/tfm` (`tf`/`lang`/`getTr`) не потребує мокання — компонент рендериться з реальним модулем, переклади вже статичні дані в самому файлі (`vue/tfm-translations.mdc`). Задокументуй це явно в story-файлі коментарем (`// @nitra/tfm — no-op, реальний модуль`), щоб наступний агент не витрачав час на пошук неіснуючого моку.
36
+
37
+ ### Apollo/GraphQL — виключно MSW
38
+
39
+ **Рішення ADR-розширення:** мокання GraphQL (query/mutation/subscription) — тільки на мережевому рівні через `msw-storybook-addon`, **без** `resolve.alias`-підмін app-коду (`boot/apollo.js` чи еквівалент лишається повністю справжнім). Facilitator у сесії розширення рекомендував лінк-рівневий alias-мок (справжні хуки `@vue3-apollo/core` + фейковий termination `ApolloLink`), а після вибору MSW — гібрид MSW+alias для side-effect boot-модулів; обрано чистий MSW, бо app-код (включно з link-split http/sse) не розходиться з production-поведінкою, а мок-хендлери переносні в майбутні vitest/playwright.
40
+
41
+ Query/mutation — стандартний `graphql.query`/`graphql.mutation` handler, сценарії через `parameters.msw`:
42
+
43
+ ```js
44
+ import { graphql, HttpResponse } from 'msw'
45
+
46
+ export const Default = {
47
+ parameters: {
48
+ msw: {
49
+ handlers: [graphql.query('GetTask', () => HttpResponse.json({ data: { task: taskFixture } }))]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ Підписки — `msw-storybook-addon` не має готового `graphql.subscription()`-хендлера; wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) мокається вручну через канонічний хелпер `sseSubscription` з `.storybook/mocks/gql-sse.js` (генерується скафолдом, `scaffold/template/mocks/gql-sse.js` — не копіювати логіку в кожен пакет, одне джерело істини для протоколу):
56
+
57
+ ```js
58
+ import { http, HttpResponse } from 'msw'
59
+ import { sseSubscription } from '../../.storybook/mocks/gql-sse.js'
60
+
61
+ export const Realtime = {
62
+ parameters: {
63
+ msw: {
64
+ handlers: [
65
+ http.post('/graphql/stream', () =>
66
+ new HttpResponse(sseSubscription([{ data: { taskUpdated: frame1 } }, { data: { taskUpdated: frame2 } }]), {
67
+ headers: { 'Content-Type': 'text/event-stream' }
68
+ })
69
+ )
70
+ ]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### Pinia
77
+
78
+ Справжня `createPinia()` у page-декораторі — **без** `pinia-plugin-persistedstate` (`persist: true` у сторі стає no-op, читання/запис localStorage не потрібні в ізольованому рендері). Наповнення початкового стану — через `parameters.pinia.initialState`, не хардкод у декораторі:
79
+
80
+ ```js
81
+ import { createPinia } from 'pinia'
82
+
83
+ export function pageDecorator({ route, pinia } = {}) {
84
+ return (story, ctx) => {
85
+ const app = createPinia()
86
+ if (ctx.parameters.pinia?.initialState) {
87
+ for (const [id, state] of Object.entries(ctx.parameters.pinia.initialState)) {
88
+ app.state.value[id] = state
89
+ }
90
+ }
91
+ // ...install router/pinia у app instance, повернути wrapped story
92
+ }
93
+ }
94
+ ```
95
+
96
+ **Не** `@pinia/testing` (`createTestingPinia`) — вона за замовчуванням підміняє actions заглушками, а сторінка в Storybook має жити зі справжньою бізнес-логікою стору, не з заглушкою.
97
+
98
+ ### Патерн story для сторінки
99
+
100
+ - Одна фабрика-декоратор `pageDecorator({ route, pinia })` на весь повторюваний код (router + pinia + Quasar layout) — не дублювати в кожному story-файлі.
101
+ - **`QLayout`/`QPageContainer`-wrapper обов'язковий**: `q-page` кидає виняток без layout-предка. Quasar SFC-transform (auto-реєстрація компонентів через build-time трансформ) **не працює** в runtime-шаблонах декораторів — `QLayout`/`QPageContainer`/`QPage` реєструються явно (`app.component('QLayout', QLayout)` тощо), інакше рендер падає з незрозумілою помилкою про невідомий тег.
102
+ - Фікстури — окремим модулем `.storybook/fixtures/<page>.js`, не inline в story-файлі (переюз між `Default`/`Loading`/`Error`/`Realtime`).
103
+ - **Smoke-мінімум**: одна story «рендериться без помилок» на кожну сторінку — обов'язковий нижній рівень покриття, навіть без окремих stories для кожного стану.
104
+ - Окремі stories для `Loading`/`Error`/`Realtime` (сценарій з кількома SSE-кадрами через `sseSubscription`) — де застосовно, поверх smoke-мінімуму.
105
+
106
+ ### Мережеві side-effect компоненти без моку
107
+
108
+ Компонент робить мережеві виклики (analytics-трекер, session-boot тощо), для яких мокання поки не покрите канонічним рецептом — постав у story коментар-маркер `// n-storybook: лише display-стан, мережеві side-effects без моку`, щоб було видно свідоме рішення, а не забутий пропуск.
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "lint": { "scope": "full", "glob": ["package.json", "**/*.vue", ".storybook/**"] }
5
+ }
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix-scaffold.mjs
4
+ resource: plugins/lang-js/rules/storybook/scaffold/fix-scaffold.mjs
5
+ docgen:
6
+ crc: 31a867f5
7
+ model: openai-codex/gpt-5.4-mini
8
+ score: 90
9
+ issues: internal-name:detectStoriesGlob,judge:inaccurate:0.95
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Файл відновлює канонічні Storybook-артефакти для concern-а `storybook/scaffold`: створює відсутні `.storybook/main.js`, `.storybook/preview.js`, `.storybook/mocks/gql-sse.js` і синхронізує `package.json#scripts.storybook` за шаблоном concern-а. `main.js` відновлюється з однією заміною для конкретного пакета — stories-glob за layout-детекцією (`detectStoriesGlob`, `main.mjs`); `preview.js` і `.storybook/mocks/gql-sse.js` відновлюються як verbatim-копії, однакові для всіх пакетів. Це потрібно, щоб привести пакет до канонічного Storybook-стану concern-а. Код працює fail-safe і не кидає винятків назовні; конфіги, на які спирається код: package.json
16
+
17
+ ## Поведінка
18
+
19
+ 1. `patterns` запускає два окремі відновлювальні сценарії для Storybook: один для відсутніх `.storybook/main.js` і `.storybook/preview.js`, інший — для відсутнього `scripts.storybook` у `package.json`.
20
+ 2. Для кожного пакета з проблемою `missing-main-js` створює `.storybook/main.js` за шаблоном concern-а, підставляючи пакетний stories glob відповідно до layout-перевірки.
21
+ 3. Для того ж пакета за потреби створює `.storybook/mocks/gql-sse.js` як канонічну копію з шаблону; якщо файл уже є, не перезаписує його.
22
+ 4. Для кожного пакета з проблемою `missing-preview-js` створює `.storybook/preview.js` як канонічну копію з шаблону concern-а.
23
+ 5. Для кожного `package.json` з проблемою `missing-storybook-script` додає або оновлює `scripts.storybook` до канонічного значення; якщо JSON не читається, запис пропускається без падіння.
24
+ 6. Усі зміни застосовуються лише там, де доступний корінь concern-а або шлях до файлу, тож autofix працює fail-safe і не зупиняє весь прогін через одну некоректну ціль.
25
+ 7. Орієнтується на `package.json` як на конфігураційне джерело для скрипта Storybook.
26
+
27
+ ## Гарантії поведінки
28
+
29
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/lang-js/rules/storybook/scaffold
4
+ resource: plugins/lang-js/rules/storybook/scaffold/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ----------------------------------- | --------- |
9
+ | [fix-scaffold.mjs](fix-scaffold.md) | JS Module |
10
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,36 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/storybook/scaffold/main.mjs
5
+ docgen:
6
+ crc: c06a9e15
7
+ model: openai-codex/gpt-5.4-mini
8
+ score: 100
9
+ issues: judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Файл підтримує Storybook-скафолд для Vue-пакетів і орієнтується на `package.json`, щоб узгодити сценарій запуску з проєктним конфігом. Експортована константа-рядок `STORYBOOK_SCRIPT="storybook dev -p 6006 --no-open"` задає стандартний Storybook-запуск без відкриття браузера. `detectStoriesGlob` визначає межі пошуку stories для пакета. `lint` перевіряє наявність очікуваних файлів і канонічних маркерів та підказує `npx @7n/rules fix storybook` у разі проблем.
16
+
17
+ ## Поведінка
18
+
19
+ - STORYBOOK_SCRIPT — канонічне значення `package.json#scripts.storybook`: `storybook dev -p 6006 --no-open`.
20
+ - detectStoriesGlob — визначає glob для Storybook stories залежно від структури пакета: для `src/components/` звужує пошук до цієї теки, інакше бере ширший glob по `src/`; шлях формується відносно `.storybook/`.
21
+ - lint — перевіряє для всіх Vue-пакетів у скоупі канонічний Storybook-скафолд: `.storybook/main.js`, `.storybook/preview.js` і `package.json#scripts.storybook`; якщо файлу або потрібних маркерів бракує, повідомляє порушення з підказкою на `npx @7n/rules fix storybook`.
22
+
23
+ Changelog: pending
24
+
25
+ ## Публічний API
26
+
27
+ - STORYBOOK_SCRIPT — Канонічне значення `package.json#scripts.storybook` (storybook.mdc).
28
+ - detectStoriesGlob — Layout-детекція для stories-glob (ADR Кластер 2): `src/components/` присутній → glob
29
+ звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по `src/`.
30
+ Шлях відносний до `.storybook/` (де лежить сам `main.js`), тому з префіксом `../`.
31
+ - lint — Перевіряє канонічний Storybook-скафолд (`.storybook/main.js`, `.storybook/preview.js`,
32
+ `package.json#scripts.storybook`) для всіх пакетів у скоупі (`scope/main.mjs`).
33
+
34
+ ## Гарантії поведінки
35
+
36
+ - Read-only: не виконує операцій запису (ФС/БД).