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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +1 -1
  3. package/rules/bun/package_json/package_json.mdc +3 -1
  4. package/rules/bun/package_json/package_json.rego +30 -1
  5. package/rules/npm-module/npm_package_json/npm_package_json.mdc +18 -3
  6. package/rules/npm-module/npm_package_json/npm_package_json.rego +42 -5
  7. package/rules/storybook/adopt/docs/index.md +9 -0
  8. package/rules/storybook/adopt/docs/main.md +46 -0
  9. package/rules/storybook/adopt/main.mjs +379 -0
  10. package/rules/storybook/hygiene/concern.json +4 -0
  11. package/rules/storybook/hygiene/docs/index.md +9 -0
  12. package/rules/storybook/hygiene/docs/main.md +45 -0
  13. package/rules/storybook/hygiene/main.mjs +254 -0
  14. package/rules/storybook/main.json +1 -0
  15. package/rules/storybook/main.mdc +60 -0
  16. package/rules/storybook/mocking/concern.json +3 -0
  17. package/rules/storybook/mocking/mocking.mdc +108 -0
  18. package/rules/storybook/scaffold/concern.json +5 -0
  19. package/rules/storybook/scaffold/docs/fix-scaffold.md +29 -0
  20. package/rules/storybook/scaffold/docs/index.md +10 -0
  21. package/rules/storybook/scaffold/docs/main.md +36 -0
  22. package/rules/storybook/scaffold/fix-scaffold.mjs +150 -0
  23. package/rules/storybook/scaffold/main.mjs +164 -0
  24. package/rules/storybook/scaffold/template/docs/index.md +10 -0
  25. package/rules/storybook/scaffold/template/docs/main.md +30 -0
  26. package/rules/storybook/scaffold/template/docs/preview.md +46 -0
  27. package/rules/storybook/scaffold/template/main.js +45 -0
  28. package/rules/storybook/scaffold/template/mocks/docs/gql-sse.md +36 -0
  29. package/rules/storybook/scaffold/template/mocks/docs/index.md +9 -0
  30. package/rules/storybook/scaffold/template/mocks/gql-sse.js +25 -0
  31. package/rules/storybook/scaffold/template/preview.js +47 -0
  32. package/rules/storybook/scope/concern.json +4 -0
  33. package/rules/storybook/scope/docs/index.md +9 -0
  34. package/rules/storybook/scope/docs/main.md +62 -0
  35. package/rules/storybook/scope/main.mjs +202 -0
  36. package/rules/storybook/vitest-config/concern.json +8 -0
  37. package/rules/storybook/vitest-config/docs/fix-vitest-config.md +38 -0
  38. package/rules/storybook/vitest-config/docs/index.md +10 -0
  39. package/rules/storybook/vitest-config/docs/main.md +72 -0
  40. package/rules/storybook/vitest-config/fix-vitest-config.mjs +340 -0
  41. package/rules/storybook/vitest-config/main.mjs +358 -0
  42. package/rules/storybook/vitest-config/template/docs/index.md +12 -0
  43. package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +34 -0
  44. package/rules/storybook/vitest-config/template/docs/unit-project-entry.md +30 -0
  45. package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +29 -0
  46. package/rules/storybook/vitest-config/template/docs/vitest.stryker.config.baseline.md +31 -0
  47. package/rules/storybook/vitest-config/template/storybook-project-entry.js +22 -0
  48. package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
  49. package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +37 -0
  50. package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
  51. package/rules/storybook/vitest-config/vitest-config.mdc +33 -0
@@ -0,0 +1,358 @@
1
+ /** @see ./docs/main.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { basename, dirname, extname, join } 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 { collectInScopeVuePackages } from '../scope/main.mjs'
10
+ import { detectStoriesGlob } from '../scaffold/main.mjs'
11
+
12
+ // Канонічні назви vitest-конфіга пакета (пріоритет .mjs — нові файли, js.mdc);
13
+ // .ts підтримано для "стійкості до варіацій" (vitest-config.mdc).
14
+ export const VITEST_CONFIG_NAMES = ['vitest.config.mjs', 'vitest.config.js', 'vitest.config.ts']
15
+ // Ті самі варіанти, що й `vite.config.*` у scope/main.mjs (не експортовано звідти — дублюємо).
16
+ const VITE_CONFIG_NAMES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
17
+
18
+ /** Import, який має бути присутній у файлі, щоб `storybookTest(...)` резолвився. */
19
+ export const STORYBOOK_TEST_IMPORT = "import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'\n"
20
+
21
+ // Стабільні reasons (namespace: ruleId/concernId/reason).
22
+ export const REASON_VITEST_CONFIG_MISSING = 'vitest-config-missing'
23
+ export const REASON_STRYKER_CONFIG_MISSING = 'stryker-config-missing'
24
+ export const REASON_CONFIG_UNRESOLVABLE = 'vitest-config-unresolvable'
25
+ export const REASON_PROJECTS_DYNAMIC = 'projects-dynamic'
26
+ export const REASON_UNIT_PROJECT_MISSING = 'unit-project-missing'
27
+ export const REASON_STORYBOOK_PROJECT_MISSING = 'storybook-project-missing'
28
+ export const REASON_STORYBOOK_PROJECT_MARKER_MISSING = 'storybook-project-marker-missing'
29
+
30
+ // Маркери канонічного storybook-проєкту (текстовий пошук у зрізі джерела самого
31
+ // елемента масиву — стійко до форматування/AST-варіацій, як і scaffold/main.mjs).
32
+ // Три останні (chromium/browser/stories) експортовано — переюз у `adopt/main.mjs`.
33
+ const UNIT_NAME_RE = /name\s*:\s*['"]unit['"]/u
34
+ const STORYBOOK_NAME_RE = /name\s*:\s*['"]storybook['"]/u
35
+ export const CHROMIUM_RE = /chromium/u
36
+ export const BROWSER_KEY_RE = /\bbrowser\s*:/u
37
+ export const STORIES_RE = /stories/iu
38
+ const STORIES_GLOB_PREFIX_RE = /^\.\.\//u
39
+ // Module-scope (prefer-static-regex): рядок-відступ цілком whitespace; leading
40
+ // кома (можливо з whitespace) — спільні з fix-vitest-config.mjs патерном augment-у.
41
+ export const WHITESPACE_ONLY_RE = /^\s*$/u
42
+ export const LEADING_COMMA_RE = /^\s*,/u
43
+
44
+ /**
45
+ * Резолвить абсолютний шлях наявного `vitest.config.*` пакета (перший знайдений
46
+ * за пріоритетом {@link VITEST_CONFIG_NAMES}), або `null` якщо жодного немає.
47
+ * @param {string} absPkgDir абсолютний шлях кореня пакета
48
+ * @returns {string | null} абсолютний шлях конфіга або null
49
+ */
50
+ export function resolveVitestConfigPath(absPkgDir) {
51
+ const name = VITEST_CONFIG_NAMES.find(n => existsSync(join(absPkgDir, n)))
52
+ return name ? join(absPkgDir, name) : null
53
+ }
54
+
55
+ /**
56
+ * Резолвить ім'я `vite.config.*` пакета для import-шляху в baseline-шаблонах
57
+ * (той самий файл, що й `viteFinal` у `.storybook/main.js`). Пакет у скоупі
58
+ * гарантовано має один із них (`scope/main.mjs#hasStandardBuild`).
59
+ * @param {string} absPkgDir абсолютний шлях кореня пакета
60
+ * @returns {string} ім'я файлу vite-конфіга (дефолт `vite.config.js`)
61
+ */
62
+ export function resolveViteConfigName(absPkgDir) {
63
+ return VITE_CONFIG_NAMES.find(n => existsSync(join(absPkgDir, n))) ?? 'vite.config.js'
64
+ }
65
+
66
+ /**
67
+ * Шлях до ізольованого `vitest.stryker.config.*` — той самий каталог і
68
+ * розширення, що й основний vitest-конфіг пакета.
69
+ * @param {string} vitestConfigPath абсолютний шлях `vitest.config.*`
70
+ * @returns {string} абсолютний шлях `vitest.stryker.config.*`
71
+ */
72
+ export function strykerConfigPathFor(vitestConfigPath) {
73
+ return join(dirname(vitestConfigPath), `vitest.stryker.config${extname(vitestConfigPath)}`)
74
+ }
75
+
76
+ /**
77
+ * Stories-glob для vitest-конфіга пакета (на відміну від `detectStoriesGlob`
78
+ * scaffold-концерна — той повертає шлях відносно `.storybook/`, тут vitest-конфіг
79
+ * лежить у корені пакета, тож префікс `../` треба зняти).
80
+ * @param {string} absPkgDir абсолютний шлях кореня пакета
81
+ * @returns {string} glob відносно кореня пакета
82
+ */
83
+ export function storiesGlobForVitestConfig(absPkgDir) {
84
+ return detectStoriesGlob(absPkgDir).replace(STORIES_GLOB_PREFIX_RE, '')
85
+ }
86
+
87
+ /**
88
+ * Парсить JS/TS файл через oxc-parser з обраним lang за розширенням.
89
+ * @param {string} absPath абсолютний шлях файлу (для diagnostics парсера)
90
+ * @param {string} src вміст файлу
91
+ * @returns {{program: object, errors?: Array<{message: string}>}} результат парсингу
92
+ */
93
+ export function parseModule(absPath, src) {
94
+ const lang = extname(absPath) === '.ts' ? 'ts' : 'js'
95
+ return parseSync(absPath, src, { lang, sourceType: 'module' })
96
+ }
97
+
98
+ /**
99
+ * Рекурсивно шукає перший `ObjectExpression` у довільному AST-вузлі, що має
100
+ * property `test` зі значенням-`ObjectExpression`.
101
+ * @param {unknown} node довільний AST-вузол/масив/примітив
102
+ * @returns {object | null} `test`-ObjectExpression або null
103
+ */
104
+ function findTestObjectIn(node) {
105
+ if (!node || typeof node !== 'object') return null
106
+ if (node.type === 'ObjectExpression') {
107
+ const prop = node.properties?.find(
108
+ p =>
109
+ p.type === 'Property' &&
110
+ !p.computed &&
111
+ p.key &&
112
+ (p.key.name === 'test' || p.key.value === 'test') &&
113
+ p.value?.type === 'ObjectExpression'
114
+ )
115
+ if (prop) return prop.value
116
+ }
117
+ for (const val of Object.values(node)) {
118
+ if (Array.isArray(val)) {
119
+ for (const item of val) {
120
+ const res = findTestObjectIn(item)
121
+ if (res) return res
122
+ }
123
+ } else if (val && typeof val === 'object') {
124
+ const res = findTestObjectIn(val)
125
+ if (res) return res
126
+ }
127
+ }
128
+ return null
129
+ }
130
+
131
+ /**
132
+ * Шукає перший `ObjectExpression` у дереві AST, що має property `test` зі
133
+ * значенням-`ObjectExpression` — незалежно від того, чи огорнутий він у
134
+ * `defineConfig(...)`/`mergeConfig(...)`, чи це простий об'єкт (стійко до
135
+ * варіацій testing.mdc-канону).
136
+ * @param {object} program oxc Program node
137
+ * @returns {object | null} `test`-ObjectExpression або null
138
+ */
139
+ export function findTestObject(program) {
140
+ return findTestObjectIn(program)
141
+ }
142
+
143
+ /**
144
+ * Шукає property `name` у `ObjectExpression`.
145
+ * @param {object} objExpr ObjectExpression node
146
+ * @param {string} name ім'я property
147
+ * @returns {object | null} Property node або null
148
+ */
149
+ export function findProperty(objExpr, name) {
150
+ return (
151
+ objExpr.properties.find(
152
+ p => p.type === 'Property' && !p.computed && p.key && (p.key.name === name || p.key.value === name)
153
+ ) ?? null
154
+ )
155
+ }
156
+
157
+ /**
158
+ * Класифікує елементи масиву `test.projects`: чи є `unit`-проєкт, і
159
+ * source-зріз елемента `storybook`-проєкту (для marker-перевірки).
160
+ * @param {string} src вихідний текст vitest-конфіга
161
+ * @param {object} arr ArrayExpression node (`test.projects`)
162
+ * @returns {{hasUnit: boolean, storybookSlice: string | null}} стан projects-масиву
163
+ */
164
+ export function classifyProjects(src, arr) {
165
+ let hasUnit = false
166
+ let storybookSlice = null
167
+ for (const el of arr.elements) {
168
+ if (!el || el.type !== 'ObjectExpression') continue
169
+ const slice = src.slice(el.start, el.end)
170
+ if (UNIT_NAME_RE.test(slice)) hasUnit = true
171
+ if (STORYBOOK_NAME_RE.test(slice)) storybookSlice = slice
172
+ }
173
+ return { hasUnit, storybookSlice }
174
+ }
175
+
176
+ /**
177
+ * Будує спільний контекст перевірки одного пакета (label/relPrefix/шлях vitest-конфіга).
178
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
179
+ * @returns {{rootDir: string, absDir: string, label: string, relPrefix: string, vitestConfigPath: string | null}} контекст
180
+ */
181
+ function buildPackageCtx({ rootDir, absDir }) {
182
+ return {
183
+ rootDir,
184
+ absDir,
185
+ label: rootDir === '.' ? 'корінь' : rootDir,
186
+ relPrefix: rootDir === '.' ? '' : `${rootDir}/`,
187
+ vitestConfigPath: resolveVitestConfigPath(absDir)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Перевіряє `test.projects` наявного vitest-конфіга: parse-помилки, відсутність
193
+ * test-блоку, відсутність/динамічність `projects`, відсутність unit/storybook-
194
+ * записів і канонічних маркерів storybook-запису (chromium/browser/stories).
195
+ * @param {ReturnType<typeof buildPackageCtx>} pkgCtx контекст пакета
196
+ * @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
197
+ * @returns {Promise<void>}
198
+ */
199
+ async function checkVitestConfigContent(pkgCtx, reporter) {
200
+ const { label, relPrefix, rootDir, vitestConfigPath } = pkgCtx
201
+ const relVitestFile = `${relPrefix}${basename(vitestConfigPath)}`
202
+ const src = await readFile(vitestConfigPath, 'utf8')
203
+
204
+ let parsed
205
+ try {
206
+ parsed = parseModule(vitestConfigPath, src)
207
+ } catch (error) {
208
+ reporter.fail(`[${label}] ${relVitestFile} не парситься (${error.message}) — перевір вручну (vitest-config.mdc)`, {
209
+ reason: REASON_CONFIG_UNRESOLVABLE,
210
+ file: relVitestFile
211
+ })
212
+ return
213
+ }
214
+ if (parsed.errors?.length) {
215
+ reporter.fail(`[${label}] ${relVitestFile} має syntax error — перевір вручну (vitest-config.mdc)`, {
216
+ reason: REASON_CONFIG_UNRESOLVABLE,
217
+ file: relVitestFile
218
+ })
219
+ return
220
+ }
221
+
222
+ const testObj = findTestObject(parsed.program)
223
+ if (!testObj) {
224
+ reporter.fail(
225
+ `[${label}] ${relVitestFile}: не вдалось знайти test-блок (defineConfig({ test: {...} })) — додай unit/storybook-projects вручну за template/ (vitest-config.mdc)`,
226
+ { reason: REASON_CONFIG_UNRESOLVABLE, file: relVitestFile }
227
+ )
228
+ return
229
+ }
230
+
231
+ const data = { rootDir, vitestConfigPath }
232
+ const projectsProp = findProperty(testObj, 'projects')
233
+ if (!projectsProp) {
234
+ reporter.fail(
235
+ `[${label}] ${relVitestFile}: бракує test.projects (unit) — npx @7n/rules fix storybook (vitest-config.mdc)`,
236
+ {
237
+ reason: REASON_UNIT_PROJECT_MISSING,
238
+ file: relVitestFile,
239
+ data
240
+ }
241
+ )
242
+ reporter.fail(
243
+ `[${label}] ${relVitestFile}: бракує test.projects (storybook) — npx @7n/rules fix storybook (vitest-config.mdc)`,
244
+ {
245
+ reason: REASON_STORYBOOK_PROJECT_MISSING,
246
+ file: relVitestFile,
247
+ data
248
+ }
249
+ )
250
+ return
251
+ }
252
+ if (projectsProp.value?.type !== 'ArrayExpression') {
253
+ reporter.fail(
254
+ `[${label}] ${relVitestFile}: test.projects — не статичний масив (spread/змінна) — додай unit/storybook-projects вручну (vitest-config.mdc)`,
255
+ { reason: REASON_PROJECTS_DYNAMIC, file: relVitestFile }
256
+ )
257
+ return
258
+ }
259
+
260
+ const { hasUnit, storybookSlice } = classifyProjects(src, projectsProp.value)
261
+ if (!hasUnit) {
262
+ reporter.fail(
263
+ `[${label}] ${relVitestFile}: test.projects без 'unit' — npx @7n/rules fix storybook (vitest-config.mdc)`,
264
+ {
265
+ reason: REASON_UNIT_PROJECT_MISSING,
266
+ file: relVitestFile,
267
+ data
268
+ }
269
+ )
270
+ }
271
+ if (storybookSlice) {
272
+ const missingHints = []
273
+ if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
274
+ if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
275
+ if (!STORIES_RE.test(storybookSlice)) missingHints.push('stories-glob')
276
+ if (missingHints.length > 0) {
277
+ reporter.fail(
278
+ `[${label}] ${relVitestFile}: storybook-project без канонічних маркерів — бракує: ${missingHints.join(', ')} (vitest-config.mdc)`,
279
+ { reason: REASON_STORYBOOK_PROJECT_MARKER_MISSING, file: relVitestFile, data: { rootDir } }
280
+ )
281
+ }
282
+ } else {
283
+ reporter.fail(
284
+ `[${label}] ${relVitestFile}: test.projects без 'storybook' — npx @7n/rules fix storybook (vitest-config.mdc)`,
285
+ {
286
+ reason: REASON_STORYBOOK_PROJECT_MISSING,
287
+ file: relVitestFile,
288
+ data
289
+ }
290
+ )
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Перевіряє наявність ізольованого `vitest.stryker.config.*` поруч із
296
+ * наявним vitest-конфігом пакета.
297
+ * @param {ReturnType<typeof buildPackageCtx>} pkgCtx контекст пакета
298
+ * @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
299
+ * @returns {void}
300
+ */
301
+ function checkStrykerConfigPresence(pkgCtx, reporter) {
302
+ const { label, relPrefix, rootDir, vitestConfigPath } = pkgCtx
303
+ const strykerPath = strykerConfigPathFor(vitestConfigPath)
304
+ if (existsSync(strykerPath)) return
305
+ const relStrykerFile = `${relPrefix}${basename(strykerPath)}`
306
+ reporter.fail(
307
+ `[${label}] відсутній ізольований ${relStrykerFile} — @stryker-mutator/vitest-runner крашиться на browser-mode projects: npx @7n/rules fix storybook (vitest-config.mdc)`,
308
+ { reason: REASON_STRYKER_CONFIG_MISSING, file: relStrykerFile, data: { rootDir, vitestConfigPath } }
309
+ )
310
+ }
311
+
312
+ /**
313
+ * Перевіряє один пакет у скоупі: наявність/канон `vitest.config.*`
314
+ * (`test.projects` — unit+storybook) і наявність ізольованого
315
+ * `vitest.stryker.config.*`.
316
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
317
+ * @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
318
+ * @returns {Promise<void>}
319
+ */
320
+ async function checkPackage(entry, reporter) {
321
+ const pkgCtx = buildPackageCtx(entry)
322
+ if (!pkgCtx.vitestConfigPath) {
323
+ reporter.fail(
324
+ `[${pkgCtx.label}] відсутній vitest.config.{mjs,js,ts} — канонічні projects unit+storybook (vitest-config.mdc): npx @7n/rules fix storybook`,
325
+ {
326
+ reason: REASON_VITEST_CONFIG_MISSING,
327
+ file: `${pkgCtx.relPrefix}vitest.config.mjs`,
328
+ data: { rootDir: pkgCtx.rootDir }
329
+ }
330
+ )
331
+ return
332
+ }
333
+
334
+ await checkVitestConfigContent(pkgCtx, reporter)
335
+ checkStrykerConfigPresence(pkgCtx, reporter)
336
+ }
337
+
338
+ /**
339
+ * Перевіряє канонічний vitest-конфіг (unit+storybook projects) і наявність
340
+ * ізольованого `vitest.stryker.config` для всіх пакетів у скоупі Storybook.
341
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
342
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
343
+ */
344
+ export async function lint(ctx) {
345
+ const reporter = createViolationReporter(ctx)
346
+ const pkgs = await collectInScopeVuePackages(ctx.cwd)
347
+
348
+ if (pkgs.length === 0) {
349
+ reporter.pass('storybook: немає Vue component library пакетів у скоупі (vitest-config.mdc)')
350
+ return reporter.result()
351
+ }
352
+
353
+ for (const entry of pkgs) {
354
+ await checkPackage(entry, reporter)
355
+ }
356
+
357
+ return reporter.result()
358
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/lang-js/rules/storybook/vitest-config/template
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/template/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ----------------------------------------------------------------------- | --------- |
9
+ | [storybook-project-entry.js](storybook-project-entry.md) | JS Module |
10
+ | [unit-project-entry.js](unit-project-entry.md) | JS Module |
11
+ | [vitest.config.baseline.mjs](vitest.config.baseline.md) | JS Module |
12
+ | [vitest.stryker.config.baseline.mjs](vitest.stryker.config.baseline.md) | JS Module |
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: JS Module
3
+ title: storybook-project-entry.js
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/template/storybook-project-entry.js
5
+ docgen:
6
+ crc: 5d9feafd
7
+ model: openai-codex/gpt-5.5
8
+ tier: cloud-avg
9
+ score: 100
10
+ issues: judge:inaccurate:0.98
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Файл задає еталонний фрагмент конфігурації для інструментів правил. Він потрібен як незмінний зразок для порівняння або відновлення очікуваного стану конфігурації. Механізм є read-only: не записує дані у файлову систему чи базу даних.
17
+
18
+ ## Поведінка
19
+
20
+ 1. Визначає канонічний запис окремого тестового проєкту для Storybook у конфігурації Vitest.
21
+
22
+ 2. Вмикає виконання Storybook-тестів у browser mode, щоб перевірки stories запускалися в браузерному середовищі.
23
+
24
+ 3. Обмежує браузерну перевірку Chromium, щоб результат був стабільним і відповідав прийнятому стандарту репозиторію.
25
+
26
+ 4. Задає пошук stories через шаблон, який підставляється під час генерації або виправлення конфігурації.
27
+
28
+ 5. Підключає Storybook setup-файл для Vitest, щоб тести stories отримували потрібне середовище Storybook.
29
+
30
+ 6. Слугує еталонним фрагментом для інструментів правил, які порівнюють або відновлюють правильний запис Storybook-проєкту в `test.projects`.
31
+
32
+ ## Гарантії поведінки
33
+
34
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,30 @@
1
+ ---
2
+ type: JS Module
3
+ title: unit-project-entry.js
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/template/unit-project-entry.js
5
+ docgen:
6
+ crc: 84a0a5f8
7
+ model: openai-codex/gpt-5.5
8
+ tier: cloud-avg
9
+ score: 100
10
+ issues: judge:inaccurate:0.94
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Файл містить еталонний запис для `unit`-проєкту. Він потрібен, щоб мати узгоджений фрагмент базового тестового середовища. Запис read-only: не пише у ФС/БД.
17
+
18
+ ## Поведінка
19
+
20
+ 1. Визначає канонічний запис `unit`-проєкту для списку `test.projects` у Vitest-конфігурації Storybook.
21
+
22
+ 2. Позначає, що `unit`-проєкт наслідує базові налаштування тестового середовища.
23
+
24
+ 3. Фіксує стабільну назву проєкту як `unit`, щоб конфігурація мала передбачуваний і уніфікований вигляд.
25
+
26
+ 4. Надає еталонний фрагмент для автоматичного вставлення в конфігурацію, не виконуючи змін у файловій системі чи зовнішніх сховищах.
27
+
28
+ ## Гарантії поведінки
29
+
30
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,29 @@
1
+ ---
2
+ type: JS Module
3
+ title: vitest.config.baseline.mjs
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/template/vitest.config.baseline.mjs
5
+ docgen:
6
+ crc: 1eca6e97
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
+ Файл збирає спільну тестову конфігурацію для Vue-бібліотеки в Storybook-контексті: окремо для звичайних unit-тестів і окремо для storybook-сценаріїв. Він існує, щоб один набір використовувався для ізольованих DOM-тестів у unit-режимі, а інший — для перевірки історій у Storybook-оточенні.
17
+
18
+ ## Поведінка
19
+
20
+ 1. Об’єднує базову конфігурацію проєкту з канонічним тестовим профілем для бібліотеки Vue-компонентів у контексті Storybook.
21
+ 2. Визначає окремий unit-набір для звичайних тестів і окремий storybook-набір для візуально-інтеграційних сценаріїв.
22
+ 3. Для unit-набору запускає тести в середовищі, придатному для DOM-логіки без браузера, і збирає покриття у форматах для звіту та короткого підсумку.
23
+ 4. Для storybook-набору підключає перевірку історій із каталогом Storybook, вмикає browser-mode, працює без віконного інтерфейсу та обмежується Chromium.
24
+ 5. Для storybook-набору підвантажує окрему підготовку середовища з сусідньої storybook-налаштування.
25
+ 6. Не запускає browser-mode для unit-набору, щоб зберегти сумісність із окремим ізольованим mutant-testing сценарієм, який використовує той самий unit-набір без browser projects.
26
+
27
+ ## Гарантії поведінки
28
+
29
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,31 @@
1
+ ---
2
+ type: JS Module
3
+ title: vitest.stryker.config.baseline.mjs
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs
5
+ docgen:
6
+ crc: 5cb4ea5e
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
+ Окремий vitest-конфіг для набору тестів, який використовують у перевірках мутацій. Він повторює базові налаштування основного середовища, але додає власні обмеження для цього сценарію. Тут також задаються параметри покриття, щоб результат був придатний для подальшої автоматичної обробки та швидкого перегляду.
17
+
18
+ ## Поведінка
19
+
20
+ 1. Формує окремий vitest-конфіг для запуску Stryker поверх уже зібраного базового тестового набору.
21
+ 2. Зберігає сумісність із unit-тестами основного середовища, але відсікає storybook-частину, яка для Stryker непридатна через browser-mode.
22
+ 3. Вмикає виконання тестів у DOM-подібному середовищі, щоб мутування перевірялося в близьких до браузера умовах без справжнього браузера.
23
+ 4. Обмежує охоплення лише unit-подібними тестами проєкту; storybook-проєкти та інші browser-mode сценарії тут свідомо не проходять.
24
+ 5. Налаштовує збір покриття так, щоб результат був придатний і для машинної обробки, і для швидкого людського перегляду.
25
+ 6. Призначений саме для окремого файлу конфігурації Stryker; основний vitest-конфіг має лишатися джерелом спільних налаштувань.
26
+
27
+ Changelog: not run
28
+
29
+ ## Гарантії поведінки
30
+
31
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -0,0 +1,22 @@
1
+ // Канонічний вигляд запису `storybook`-проєкту у `test.projects` (storybook.mdc,
2
+ // vitest-config-концерн, ADR Кластер 5): browser-mode, лише chromium, stories-glob
3
+ // (`__STORYBOOK_STORIES_GLOB__` — токен, `detectStoriesGlob` зі scaffold-концерна
4
+ // без префіксу `../`). Сам файл — валідний модуль лише для JS-лінту репозиторію
5
+ // правил; `fix-vitest-config.mjs` зчитує лише вміст `export default {...}`.
6
+ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
7
+
8
+ export default {
9
+ extends: true,
10
+ plugins: [storybookTest({ configDir: '.storybook' })],
11
+ test: {
12
+ name: 'storybook',
13
+ include: ['__STORYBOOK_STORIES_GLOB__'],
14
+ browser: {
15
+ enabled: true,
16
+ headless: true,
17
+ provider: 'playwright',
18
+ instances: [{ browser: 'chromium' }]
19
+ },
20
+ setupFiles: ['.storybook/vitest.setup.js']
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ // Канонічний вигляд запису `unit`-проєкту у `test.projects` (storybook.mdc,
2
+ // vitest-config-концерн). Сам файл — валідний модуль лише для того, щоб
3
+ // пройти JS-лінт репозиторію правил; `fix-vitest-config.mjs` зчитує лише
4
+ // вміст `export default {...}` і вставляє його як елемент масиву `projects`.
5
+ export default { extends: true, test: { name: 'unit' } }
@@ -0,0 +1,37 @@
1
+ import { defineConfig, mergeConfig } from 'vitest/config'
2
+ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
3
+ import viteConfig from './__VITE_CONFIG_IMPORT__'
4
+
5
+ // Канонічний vitest-конфіг Vue-компонентної бібліотеки у скоупі Storybook
6
+ // (storybook.mdc, vitest-config-концерн, ADR Кластер 5): named projects
7
+ // `unit` + `storybook`. Ізольований `vitest.stryker.config` (поруч, той
8
+ // самий concern генерує) — той самий unit-набір БЕЗ browser-mode
9
+ // `projects`, бо @stryker-mutator/vitest-runner крашиться на них.
10
+ export default mergeConfig(
11
+ viteConfig,
12
+ defineConfig({
13
+ test: {
14
+ include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}'],
15
+ environment: 'happy-dom',
16
+ coverage: { provider: 'v8', reporter: ['lcov', 'text-summary'] },
17
+ projects: [
18
+ { extends: true, test: { name: 'unit' } },
19
+ {
20
+ extends: true,
21
+ plugins: [storybookTest({ configDir: '.storybook' })],
22
+ test: {
23
+ name: 'storybook',
24
+ include: ['__STORYBOOK_STORIES_GLOB__'],
25
+ browser: {
26
+ enabled: true,
27
+ headless: true,
28
+ provider: 'playwright',
29
+ instances: [{ browser: 'chromium' }]
30
+ },
31
+ setupFiles: ['.storybook/vitest.setup.js']
32
+ }
33
+ }
34
+ ]
35
+ }
36
+ })
37
+ )
@@ -0,0 +1,19 @@
1
+ import { defineConfig, mergeConfig } from 'vitest/config'
2
+ import viteConfig from './__VITE_CONFIG_IMPORT__'
3
+
4
+ // Ізольований vitest-конфіг лише для Stryker (test.mdc, storybook.mdc, ADR
5
+ // Кластер 5): @stryker-mutator/vitest-runner крашиться на browser-mode
6
+ // `projects` основного vitest.config (canonical `unit`+`storybook`) — тому
7
+ // Stryker отримує окремий конфіг з тим самим unit-набором і без
8
+ // storybook-project/browser-mode. `stryker.config.mjs#vitest.configFile`
9
+ // пакета має вказувати саме сюди.
10
+ export default mergeConfig(
11
+ viteConfig,
12
+ defineConfig({
13
+ test: {
14
+ include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}'],
15
+ environment: 'happy-dom',
16
+ coverage: { provider: 'v8', reporter: ['lcov', 'text-summary'] }
17
+ }
18
+ })
19
+ )
@@ -0,0 +1,33 @@
1
+ ## Vitest-конфіг Storybook-пакета: named projects `unit`+`storybook` та ізоляція Stryker
2
+
3
+ Джерело рішення: ADR `канон-storybook-для-vue-компонентних-бібліотек.md`, Кластер 5. Стосується лише пакетів у скоупі `storybook` (`scope/main.mjs#collectInScopeVuePackages`).
4
+
5
+ ### Канон `test.projects`
6
+
7
+ Vue-компонентна бібліотека вже має `vitest.config.{mjs,js,ts}` за каноном `vue/testing.mdc` (`mergeConfig(viteConfig, defineConfig({ test: {...} }))` з `environment: 'happy-dom'`). Storybook-хвиля **дописує** до цього `test`-блоку named-project `unit`+`storybook` — існуючі поля (`include`, `environment`, `coverage`) лишаються без змін, `projects` — нова властивість поруч:
8
+
9
+ - `{ extends: true, test: { name: 'unit' } }` — успадковує решту `test`-опцій пакета як є.
10
+ - `storybook`-проєкт — **browser-mode, лише chromium** (`browser: { enabled: true, provider: 'playwright', instances: [{ browser: 'chromium' }] }`) і `include` на stories-glob пакета (та сама layout-детекція, що й `scaffold/main.mjs#detectStoriesGlob`, лише без префіксу `../` — vitest-конфіг лежить у корені пакета, а не в `.storybook/`).
11
+
12
+ Firefox/webkit свідомо не додаються — ADR і main.mdc хвилі 1 фіксують лише chromium; розширення на інші браузери — поза обсягом цього концерну.
13
+
14
+ ### Чому окремий `vitest.stryker.config`
15
+
16
+ `@stryker-mutator/vitest-runner` крашиться, коли конфіг, який він завантажує, містить browser-mode `projects` (сам факт присутності `storybook`-проєкту в основному vitest-конфізі ламає mutation-прогін, незалежно від того, чи Stryker фактично торкається storybook-файлів). Тому концерн генерує **сусідній** ізольований `vitest.stryker.config.<ext>` — той самий `mergeConfig(viteConfig, defineConfig({ test: {...} }))`, але **без** `projects`/browser-mode: лише unit-набір (`include`, `happy-dom`, `coverage`), на якому mutation-testing працює штатно.
17
+
18
+ Концерн лише генерує/підтримує цей файл поруч з основним vitest-конфігом. Він **не** переналаштовує `stryker.config.mjs#vitest.configFile` пакета (це відповідальність `test/stryker_config`, який поки що не знає про Storybook-канон) — до появи такого зв'язку вказати `configFile: 'vitest.stryker.config.<ext>'` для пакетів у скоупі Storybook потрібно вручну.
19
+
20
+ ### Що перевіряє й що виправляє концерн
21
+
22
+ Перевірка (`main.mjs`) — AST-аналіз (`oxc-parser`) наявного vitest-конфіга: знаходить `test`-блок (незалежно від того, чи він огорнутий у `defineConfig`/`mergeConfig`, чи це простий об'єкт), дивиться на `test.projects` і класифікує елементи за `name: 'unit'`/`name: 'storybook'`; для `storybook`-елемента перевіряє маркери `chromium`/`browser`/stories-glob текстовим пошуком. Стійка до форматування й порядку полів — не вимагає конкретного стилю запису, лише семантичну присутність маркерів.
23
+
24
+ Автофікс (`fix-vitest-config.mjs`, fixability `config`) виправляє й дописує **лише відсутнє**:
25
+
26
+ - Немає vitest-конфіга взагалі → створює повний файл з canonical `unit`+`storybook` projects.
27
+ - Є test-блок без `projects` → вставляє нову property `projects: [...]` з обома записами, не займаючи `include`/`environment`/`coverage`.
28
+ - Є `projects`, але без одного із записів → дописує відсутній запис у кінець масиву.
29
+ - Немає `vitest.stryker.config.*` → генерує з canonical baseline.
30
+
31
+ Як і в `test/stryker_config`, правка — точкові string-splice-и (insert-only) поверх існуючого тексту, а не переписаний AST-serializer, щоб не зачепити форматування й коментарі решти файлу; результат повторно парситься, і при невалідному виводі запис скасовується (fail-closed).
32
+
33
+ **Свідомо без автофіксу:** `test.projects` — не статичний масив (spread/змінна), test-блок не вдалось знайти структурно, або `storybook`-запис уже присутній, але без якогось із маркерів (chromium/browser/stories-glob) — надійно виправити частковий вручну написаний запис програмно ризиковано, тож це лишається порушенням для ручного review (той самий компроміс, що й marker-порушення в `scaffold`).