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

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