@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.
- package/CHANGELOG.md +23 -0
- package/package.json +1 -1
- package/rules/bun/package_json/package_json.mdc +3 -1
- package/rules/bun/package_json/package_json.rego +36 -1
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +18 -3
- package/rules/npm-module/npm_package_json/npm_package_json.rego +42 -5
- package/rules/storybook/adopt/docs/index.md +9 -0
- package/rules/storybook/adopt/docs/main.md +45 -0
- package/rules/storybook/adopt/main.mjs +382 -0
- package/rules/storybook/ci/concern.json +15 -0
- package/rules/storybook/ci/docs/fix-ci.md +39 -0
- package/rules/storybook/ci/docs/index.md +10 -0
- package/rules/storybook/ci/docs/main.md +50 -0
- package/rules/storybook/ci/fix-ci.mjs +105 -0
- package/rules/storybook/ci/main.mjs +112 -0
- package/rules/storybook/ci/template/lint-storybook.yml.snippet.yml +41 -0
- package/rules/storybook/ci/template/setup-playwright-chromium.action.yml +34 -0
- package/rules/storybook/hygiene/concern.json +4 -0
- package/rules/storybook/hygiene/docs/index.md +9 -0
- package/rules/storybook/hygiene/docs/main.md +45 -0
- package/rules/storybook/hygiene/main.mjs +254 -0
- package/rules/storybook/main.json +1 -0
- package/rules/storybook/main.mdc +66 -0
- package/rules/storybook/mocking/concern.json +3 -0
- package/rules/storybook/mocking/mocking.mdc +132 -0
- package/rules/storybook/scaffold/concern.json +5 -0
- package/rules/storybook/scaffold/docs/fix-scaffold.md +29 -0
- package/rules/storybook/scaffold/docs/index.md +10 -0
- package/rules/storybook/scaffold/docs/main.md +36 -0
- package/rules/storybook/scaffold/fix-scaffold.mjs +150 -0
- package/rules/storybook/scaffold/main.mjs +196 -0
- package/rules/storybook/scaffold/template/docs/index.md +10 -0
- package/rules/storybook/scaffold/template/docs/main.md +29 -0
- package/rules/storybook/scaffold/template/docs/preview.md +46 -0
- package/rules/storybook/scaffold/template/main.js +92 -0
- package/rules/storybook/scaffold/template/mocks/docs/gql-sse.md +36 -0
- package/rules/storybook/scaffold/template/mocks/docs/index.md +9 -0
- package/rules/storybook/scaffold/template/mocks/gql-sse.js +25 -0
- package/rules/storybook/scaffold/template/preview.js +47 -0
- package/rules/storybook/scope/concern.json +4 -0
- package/rules/storybook/scope/docs/index.md +9 -0
- package/rules/storybook/scope/docs/main.md +62 -0
- package/rules/storybook/scope/main.mjs +202 -0
- package/rules/storybook/vitest-config/concern.json +8 -0
- package/rules/storybook/vitest-config/docs/fix-vitest-config.md +37 -0
- package/rules/storybook/vitest-config/docs/index.md +10 -0
- package/rules/storybook/vitest-config/docs/main.md +71 -0
- package/rules/storybook/vitest-config/fix-vitest-config.mjs +356 -0
- package/rules/storybook/vitest-config/main.mjs +389 -0
- package/rules/storybook/vitest-config/template/docs/index.md +12 -0
- package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +33 -0
- package/rules/storybook/vitest-config/template/docs/unit-project-entry.md +30 -0
- package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +28 -0
- package/rules/storybook/vitest-config/template/docs/vitest.stryker.config.baseline.md +31 -0
- package/rules/storybook/vitest-config/template/storybook-project-entry.js +26 -0
- package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
- package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +38 -0
- package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
- package/rules/storybook/vitest-config/vitest-config.mdc +37 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopt-режим канону Storybook (ADR канон-storybook-для-vue-компонентних-бібліотек,
|
|
3
|
+
* Кластер 8; main.mdc). Для пакетів, де ВЖЕ є ручний `.storybook/`, що не збігається з
|
|
4
|
+
* каноном, — діагностика diff по секціях проти канонічних `template/` (main.js,
|
|
5
|
+
* preview.js, mocks/gql-sse.js, package.json#scripts.storybook, vitest test.projects,
|
|
6
|
+
* vitest.stryker.config), БЕЗ сліпого перезапису розбіжних файлів. Автофікс
|
|
7
|
+
* (`--fix-missing`) — лише для секцій, яких немає ВЗАГАЛІ (той самий рендер, що й
|
|
8
|
+
* T0-фікс concern-ів `scaffold`/`vitest-config` — переюз, не дублювання шаблонування).
|
|
9
|
+
*
|
|
10
|
+
* Circuit breaker (ADR): збій діагностики чи фіксу одного пакета деградує до
|
|
11
|
+
* `status: 'broken'` для ЦЬОГО пакета — решта пакетів прогону обробляються далі,
|
|
12
|
+
* увесь прогін ніколи не падає через один зламаний пакет.
|
|
13
|
+
*
|
|
14
|
+
* Викликається зі скіла (`npm/skills/storybook/SKILL.md`, `--adopt`):
|
|
15
|
+
* bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs [--fix-missing] [--cwd <path>] [rootDir...]
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
import { isRunAsCli } from '@7n/rules/scripts/cli-entry.mjs'
|
|
21
|
+
|
|
22
|
+
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
23
|
+
import { MAIN_JS_MARKERS, missingMarkers, PREVIEW_JS_MARKERS, STORYBOOK_SCRIPT } from '../scaffold/main.mjs'
|
|
24
|
+
import { renderMainJs, renderMocksGqlSse, renderPreviewJs } from '../scaffold/fix-scaffold.mjs'
|
|
25
|
+
import { buildFreshVitestConfig, buildStrykerConfig } from '../vitest-config/fix-vitest-config.mjs'
|
|
26
|
+
import {
|
|
27
|
+
BROWSER_KEY_RE,
|
|
28
|
+
CHROMIUM_RE,
|
|
29
|
+
classifyProjects,
|
|
30
|
+
findProperty,
|
|
31
|
+
findTestObject,
|
|
32
|
+
hasStoriesMarker,
|
|
33
|
+
parseModule,
|
|
34
|
+
PROVIDER_FACTORY_RE,
|
|
35
|
+
resolveVitestConfigPath,
|
|
36
|
+
strykerConfigPathFor
|
|
37
|
+
} from '../vitest-config/main.mjs'
|
|
38
|
+
|
|
39
|
+
/** Статуси однієї секції діагностики (діагностика ≠ lint-violation — тут завжди 4 значення). */
|
|
40
|
+
export const STATUS = Object.freeze({ MATCH: 'match', DIFFER: 'differ', MISSING: 'missing' })
|
|
41
|
+
|
|
42
|
+
/** Канонічні назви секцій (стабільні — на них зав'язаний `--fix-missing`-switch). */
|
|
43
|
+
export const SECTION = Object.freeze({
|
|
44
|
+
MAIN_JS: 'main.js',
|
|
45
|
+
PREVIEW_JS: 'preview.js',
|
|
46
|
+
MOCKS_GQL_SSE: 'mocks/gql-sse.js',
|
|
47
|
+
PACKAGE_SCRIPT: 'package.json#scripts.storybook',
|
|
48
|
+
VITEST_PROJECTS: 'vitest test.projects (unit+storybook)',
|
|
49
|
+
STRYKER_CONFIG: 'vitest.stryker.config'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* posix-relative шлях файлу пакета від кореня репозиторію (для звіту/сообщений).
|
|
54
|
+
* @param {{ rootDir: string }} entry запис пакета
|
|
55
|
+
* @param {string} suffix шлях файлу відносно кореня пакета
|
|
56
|
+
* @returns {string} відносний шлях від кореня репозиторію
|
|
57
|
+
*/
|
|
58
|
+
function relFileFor(entry, suffix) {
|
|
59
|
+
return entry.rootDir === '.' ? suffix : `${entry.rootDir}/${suffix}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Діагностика файлу, канон якого перевіряється текстовими маркерами (main.js/preview.js).
|
|
64
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
65
|
+
* @param {string} relPath шлях файлу відносно кореня пакета (`.storybook/main.js`)
|
|
66
|
+
* @param {string} sectionName ім'я секції звіту
|
|
67
|
+
* @param {{ token: string, hint: string }[]} markers канонічні маркери файлу
|
|
68
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} одна секція діагностики
|
|
69
|
+
*/
|
|
70
|
+
function diagnoseMarkerFile(entry, relPath, sectionName, markers) {
|
|
71
|
+
const file = relFileFor(entry, relPath)
|
|
72
|
+
const abs = join(entry.absDir, relPath)
|
|
73
|
+
if (!existsSync(abs)) return { name: sectionName, file, status: STATUS.MISSING }
|
|
74
|
+
const content = readFileSync(abs, 'utf8')
|
|
75
|
+
const missing = missingMarkers(content, markers)
|
|
76
|
+
if (missing.length === 0) return { name: sectionName, file, status: STATUS.MATCH }
|
|
77
|
+
return {
|
|
78
|
+
name: sectionName,
|
|
79
|
+
file,
|
|
80
|
+
status: STATUS.DIFFER,
|
|
81
|
+
detail: `бракує: ${missing.map(m => m.hint).join(', ')}`
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Діагностика `.storybook/mocks/gql-sse.js` — verbatim-порівняння з канонічним helper-ом
|
|
87
|
+
* (одне джерело істини протоколу graphql-sse, mocking.mdc — не переносити копію в пакет).
|
|
88
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
89
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
90
|
+
*/
|
|
91
|
+
function diagnoseMocksSection(entry) {
|
|
92
|
+
const relPath = '.storybook/mocks/gql-sse.js'
|
|
93
|
+
const file = relFileFor(entry, relPath)
|
|
94
|
+
const abs = join(entry.absDir, relPath)
|
|
95
|
+
if (!existsSync(abs)) return { name: SECTION.MOCKS_GQL_SSE, file, status: STATUS.MISSING }
|
|
96
|
+
const actual = readFileSync(abs, 'utf8')
|
|
97
|
+
const canonical = renderMocksGqlSse()
|
|
98
|
+
if (actual === canonical) return { name: SECTION.MOCKS_GQL_SSE, file, status: STATUS.MATCH }
|
|
99
|
+
return {
|
|
100
|
+
name: SECTION.MOCKS_GQL_SSE,
|
|
101
|
+
file,
|
|
102
|
+
status: STATUS.DIFFER,
|
|
103
|
+
detail: 'вміст відрізняється від канонічного helper-а (mocking.mdc) — одне джерело істини, не дублюй логіку вручну'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Діагностика `package.json#scripts.storybook`.
|
|
109
|
+
* @param {{ pkg: Record<string, unknown>, rootDir: string }} entry запис пакета
|
|
110
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
111
|
+
*/
|
|
112
|
+
function diagnoseScriptSection(entry) {
|
|
113
|
+
const file = relFileFor(entry, 'package.json')
|
|
114
|
+
const current = /** @type {{ scripts?: Record<string, unknown> }} */ (entry.pkg)?.scripts?.storybook
|
|
115
|
+
if (current === undefined) return { name: SECTION.PACKAGE_SCRIPT, file, status: STATUS.MISSING }
|
|
116
|
+
if (current === STORYBOOK_SCRIPT) return { name: SECTION.PACKAGE_SCRIPT, file, status: STATUS.MATCH }
|
|
117
|
+
return {
|
|
118
|
+
name: SECTION.PACKAGE_SCRIPT,
|
|
119
|
+
file,
|
|
120
|
+
status: STATUS.DIFFER,
|
|
121
|
+
detail: `зараз '${current}', канон '${STORYBOOK_SCRIPT}'`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Діагностика `test.projects` наявного vitest-конфіга (unit+storybook, browser-mode маркери).
|
|
127
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
128
|
+
* @returns {{ name: string, file: string, status: string, detail?: string }} секція діагностики
|
|
129
|
+
*/
|
|
130
|
+
function diagnoseVitestProjectsSection(entry) {
|
|
131
|
+
const name = SECTION.VITEST_PROJECTS
|
|
132
|
+
const vitestConfigPath = resolveVitestConfigPath(entry.absDir)
|
|
133
|
+
if (!vitestConfigPath) {
|
|
134
|
+
return { name, file: relFileFor(entry, 'vitest.config.mjs'), status: STATUS.MISSING }
|
|
135
|
+
}
|
|
136
|
+
const file = relFileFor(entry, vitestConfigPath.slice(entry.absDir.length + 1))
|
|
137
|
+
const src = readFileSync(vitestConfigPath, 'utf8')
|
|
138
|
+
|
|
139
|
+
let parsed
|
|
140
|
+
try {
|
|
141
|
+
parsed = parseModule(vitestConfigPath, src)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { name, file, status: STATUS.DIFFER, detail: `не парситься (${error.message}) — перевір вручну` }
|
|
144
|
+
}
|
|
145
|
+
if (parsed.errors?.length) {
|
|
146
|
+
return { name, file, status: STATUS.DIFFER, detail: 'syntax error — перевір вручну' }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const testObj = findTestObject(parsed.program)
|
|
150
|
+
if (!testObj) {
|
|
151
|
+
return { name, file, status: STATUS.DIFFER, detail: 'немає test-блоку (defineConfig({ test: {...} }))' }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const projectsProp = findProperty(testObj, 'projects')
|
|
155
|
+
if (!projectsProp) {
|
|
156
|
+
return { name, file, status: STATUS.MISSING, detail: 'test.projects відсутній' }
|
|
157
|
+
}
|
|
158
|
+
if (projectsProp.value?.type !== 'ArrayExpression') {
|
|
159
|
+
return { name, file, status: STATUS.DIFFER, detail: 'test.projects не статичний масив (spread/змінна)' }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { hasUnit, storybookSlice } = classifyProjects(src, projectsProp.value)
|
|
163
|
+
if (!hasUnit || !storybookSlice) {
|
|
164
|
+
const missingParts = [hasUnit ? null : "'unit'", storybookSlice ? null : "'storybook'"].filter(Boolean)
|
|
165
|
+
return { name, file, status: STATUS.DIFFER, detail: `бракує ${missingParts.join(' і ')} у test.projects` }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const missingHints = []
|
|
169
|
+
if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
|
|
170
|
+
if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
|
|
171
|
+
if (!hasStoriesMarker(storybookSlice)) missingHints.push('stories-джерело (include або storybookTest({ configDir }))')
|
|
172
|
+
if (!PROVIDER_FACTORY_RE.test(storybookSlice))
|
|
173
|
+
missingHints.push('provider-factory (playwright() з @vitest/browser-playwright)')
|
|
174
|
+
if (missingHints.length > 0) {
|
|
175
|
+
return { name, file, status: STATUS.DIFFER, detail: `storybook-project без: ${missingHints.join(', ')}` }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { name, file, status: STATUS.MATCH }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Діагностика ізольованого `vitest.stryker.config.*` — байтове порівняння з канонічним
|
|
183
|
+
* baseline-рендером (fix-vitest-config.mjs, той самий генератор).
|
|
184
|
+
* @param {{ absDir: string, rootDir: string }} entry запис пакета
|
|
185
|
+
* @returns {Promise<{ name: string, file: string, status: string, detail?: string }>} секція діагностики
|
|
186
|
+
*/
|
|
187
|
+
async function diagnoseStrykerSection(entry) {
|
|
188
|
+
const name = SECTION.STRYKER_CONFIG
|
|
189
|
+
const vitestConfigPath = resolveVitestConfigPath(entry.absDir)
|
|
190
|
+
if (!vitestConfigPath) {
|
|
191
|
+
return { name, file: relFileFor(entry, 'vitest.stryker.config.mjs'), status: STATUS.MISSING }
|
|
192
|
+
}
|
|
193
|
+
const strykerPath = strykerConfigPathFor(vitestConfigPath)
|
|
194
|
+
const file = relFileFor(entry, strykerPath.slice(entry.absDir.length + 1))
|
|
195
|
+
if (!existsSync(strykerPath)) {
|
|
196
|
+
return { name, file, status: STATUS.MISSING }
|
|
197
|
+
}
|
|
198
|
+
const actual = readFileSync(strykerPath, 'utf8')
|
|
199
|
+
const canonical = await buildStrykerConfig(entry.absDir)
|
|
200
|
+
if (actual === canonical) return { name, file, status: STATUS.MATCH }
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
file,
|
|
204
|
+
status: STATUS.DIFFER,
|
|
205
|
+
detail:
|
|
206
|
+
'вміст відрізняється від канонічного baseline (vitest-config.mdc) — @stryker-mutator/vitest-runner крашиться на browser-mode projects, перевір вручну'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Діагностика одного пакета в скоупі по секціях. Ніколи не кидає — збій окремої
|
|
212
|
+
* секції (парсинг/IO) відображається як секція `differ`, а не виняток, що впав
|
|
213
|
+
* би на весь пакет; лишається на розсуд `diagnosePackage` (circuit breaker рівня
|
|
214
|
+
* пакета — тут не потрібен, бо секції вже ізольовані одна від одної).
|
|
215
|
+
* @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
|
|
216
|
+
* @returns {Promise<{ rootDir: string, status: 'canonical'|'missing-files'|'differs', sections: object[] }>} діагностика пакета
|
|
217
|
+
*/
|
|
218
|
+
export async function diagnosePackage(entry) {
|
|
219
|
+
const sections = [
|
|
220
|
+
diagnoseMarkerFile(entry, '.storybook/main.js', SECTION.MAIN_JS, MAIN_JS_MARKERS),
|
|
221
|
+
diagnoseMarkerFile(entry, '.storybook/preview.js', SECTION.PREVIEW_JS, PREVIEW_JS_MARKERS),
|
|
222
|
+
diagnoseMocksSection(entry),
|
|
223
|
+
diagnoseScriptSection(entry),
|
|
224
|
+
diagnoseVitestProjectsSection(entry),
|
|
225
|
+
await diagnoseStrykerSection(entry)
|
|
226
|
+
]
|
|
227
|
+
let status = 'canonical'
|
|
228
|
+
if (sections.some(s => s.status === STATUS.DIFFER)) status = 'differs'
|
|
229
|
+
else if (sections.some(s => s.status === STATUS.MISSING)) status = 'missing-files'
|
|
230
|
+
return { rootDir: entry.rootDir, status, sections }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Генерує канонічний вміст лише для секцій зі статусом `missing` одного пакета
|
|
235
|
+
* (adopt-автофікс НІКОЛИ не чіпає секції зі статусом `differ` — інструкція
|
|
236
|
+
* для агента/людини, не сліпий перезапис). Кожна секція фіксується незалежно;
|
|
237
|
+
* збій однієї не блокує решту (той самий circuit-breaker принцип, лише на дрібнішому рівні).
|
|
238
|
+
* @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
|
|
239
|
+
* @param {object[]} sections секції діагностики пакета (`diagnosePackage().sections`)
|
|
240
|
+
* @returns {Promise<string[]>} абсолютні шляхи записаних файлів
|
|
241
|
+
*/
|
|
242
|
+
export async function fixMissingSections(entry, sections) {
|
|
243
|
+
const written = []
|
|
244
|
+
const writeFileEnsureDir = (absPath, content) => {
|
|
245
|
+
mkdirSync(dirname(absPath), { recursive: true })
|
|
246
|
+
writeFileSync(absPath, content, 'utf8')
|
|
247
|
+
written.push(absPath)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const byName = new Map(sections.map(s => [s.name, s]))
|
|
251
|
+
|
|
252
|
+
if (byName.get(SECTION.MAIN_JS)?.status === STATUS.MISSING) {
|
|
253
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/main.js'), renderMainJs(entry.absDir))
|
|
254
|
+
}
|
|
255
|
+
if (byName.get(SECTION.PREVIEW_JS)?.status === STATUS.MISSING) {
|
|
256
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/preview.js'), renderPreviewJs())
|
|
257
|
+
}
|
|
258
|
+
if (byName.get(SECTION.MOCKS_GQL_SSE)?.status === STATUS.MISSING) {
|
|
259
|
+
writeFileEnsureDir(join(entry.absDir, '.storybook/mocks/gql-sse.js'), renderMocksGqlSse())
|
|
260
|
+
}
|
|
261
|
+
if (byName.get(SECTION.PACKAGE_SCRIPT)?.status === STATUS.MISSING) {
|
|
262
|
+
const pkgPath = join(entry.absDir, 'package.json')
|
|
263
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
264
|
+
pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
265
|
+
pkg.scripts.storybook = STORYBOOK_SCRIPT
|
|
266
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
267
|
+
written.push(pkgPath)
|
|
268
|
+
}
|
|
269
|
+
if (byName.get(SECTION.VITEST_PROJECTS)?.status === STATUS.MISSING && !resolveVitestConfigPath(entry.absDir)) {
|
|
270
|
+
// test.projects відсутній ЛИШЕ через відсутність усього vitest-конфіга — augment
|
|
271
|
+
// наявного (без відсутнього test-блоку) свідомо поза adopt-автофіксом: секція вже
|
|
272
|
+
// позначена `differ` (не `missing`) для такого випадку, сюди він не потрапляє.
|
|
273
|
+
const fresh = await buildFreshVitestConfig(entry.absDir)
|
|
274
|
+
writeFileEnsureDir(fresh.path, fresh.content)
|
|
275
|
+
}
|
|
276
|
+
if (byName.get(SECTION.STRYKER_CONFIG)?.status === STATUS.MISSING) {
|
|
277
|
+
const vitestConfigPath = resolveVitestConfigPath(entry.absDir) ?? join(entry.absDir, 'vitest.config.mjs')
|
|
278
|
+
const strykerPath = strykerConfigPathFor(vitestConfigPath)
|
|
279
|
+
writeFileEnsureDir(strykerPath, await buildStrykerConfig(entry.absDir))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return written
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Adopt-прогін усіх (чи обраних) пакетів у скоупі. Circuit breaker (ADR Кластер 8):
|
|
287
|
+
* збій діагностики/фіксу ОДНОГО пакета деградує до `status: 'broken'` для нього —
|
|
288
|
+
* решта пакетів обробляються далі, весь прогін ніколи не падає через один зламаний.
|
|
289
|
+
* @param {string} cwd абсолютний корінь консюмер-репо
|
|
290
|
+
* @param {{ fixMissing?: boolean, rootDirs?: string[] }} [opts] `fixMissing` — генерувати
|
|
291
|
+
* відсутні секції; `rootDirs` — звузити прогін до цих коренів пакетів (порожньо/відсутнє → усі в скоупі)
|
|
292
|
+
* @returns {Promise<{ rootDir: string, status: string, sections?: object[], written?: string[], error?: string }[]>} результати за пакетами
|
|
293
|
+
*/
|
|
294
|
+
export async function runAdopt(cwd, opts = {}) {
|
|
295
|
+
const fixMissing = opts.fixMissing === true
|
|
296
|
+
const wantedRoots = Array.isArray(opts.rootDirs) && opts.rootDirs.length > 0 ? new Set(opts.rootDirs) : null
|
|
297
|
+
|
|
298
|
+
const allPkgs = await collectInScopeVuePackages(cwd)
|
|
299
|
+
const pkgs = wantedRoots ? allPkgs.filter(p => wantedRoots.has(p.rootDir)) : allPkgs
|
|
300
|
+
|
|
301
|
+
const results = []
|
|
302
|
+
for (const entry of pkgs) {
|
|
303
|
+
try {
|
|
304
|
+
const diagnosis = await diagnosePackage(entry)
|
|
305
|
+
const written = fixMissing ? await fixMissingSections(entry, diagnosis.sections) : []
|
|
306
|
+
results.push({ ...diagnosis, written })
|
|
307
|
+
} catch (error) {
|
|
308
|
+
results.push({
|
|
309
|
+
rootDir: entry.rootDir,
|
|
310
|
+
status: 'broken',
|
|
311
|
+
error: error instanceof Error ? error.message : String(error)
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return results
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Іконка статусу пакета (`package.status`) для рядка звіту. */
|
|
319
|
+
const PACKAGE_STATUS_ICONS = Object.freeze({ canonical: '✅', 'missing-files': '➕' })
|
|
320
|
+
/** Іконка статусу секції (`STATUS.*`) для рядка звіту. */
|
|
321
|
+
const SECTION_STATUS_ICONS = Object.freeze({ [STATUS.MATCH]: ' ✓', [STATUS.MISSING]: ' +' })
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Рядки звіту одного НЕ зламаного пакета (статус + секції + перелік згенерованого).
|
|
325
|
+
* @param {{ rootDir: string, status: string, sections: object[], written?: string[] }} r результат одного пакета
|
|
326
|
+
* @returns {string[]} рядки звіту
|
|
327
|
+
*/
|
|
328
|
+
function formatPackageLines(r) {
|
|
329
|
+
const label = r.rootDir === '.' ? 'корінь' : r.rootDir
|
|
330
|
+
const lines = [`${PACKAGE_STATUS_ICONS[r.status] ?? '⚠️ '} [${label}] ${r.status}`]
|
|
331
|
+
for (const s of r.sections) {
|
|
332
|
+
const sIcon = SECTION_STATUS_ICONS[s.status] ?? ' ✗'
|
|
333
|
+
const suffix = s.detail ? ` — ${s.detail}` : ''
|
|
334
|
+
lines.push(`${sIcon} ${s.name} (${s.file}): ${s.status}${suffix}`)
|
|
335
|
+
}
|
|
336
|
+
if (r.written && r.written.length > 0) {
|
|
337
|
+
lines.push(` згенеровано: ${r.written.join(', ')}`)
|
|
338
|
+
}
|
|
339
|
+
return lines
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Форматує людський звіт по результатах `runAdopt` (українською, для виводу скіла в CLI).
|
|
344
|
+
* @param {Awaited<ReturnType<typeof runAdopt>>} results результати `runAdopt`
|
|
345
|
+
* @returns {string} багаторядковий текстовий звіт
|
|
346
|
+
*/
|
|
347
|
+
export function formatReport(results) {
|
|
348
|
+
if (results.length === 0) {
|
|
349
|
+
return 'storybook adopt: немає Vue component library пакетів у скоупі (storybook.mdc → scope/main.mjs)'
|
|
350
|
+
}
|
|
351
|
+
const lines = []
|
|
352
|
+
for (const r of results) {
|
|
353
|
+
if (r.status === 'broken') {
|
|
354
|
+
const label = r.rootDir === '.' ? 'корінь' : r.rootDir
|
|
355
|
+
lines.push(`⚠️ [${label}] діагностика впала (circuit breaker, пропущено): ${r.error}`)
|
|
356
|
+
continue
|
|
357
|
+
}
|
|
358
|
+
lines.push(...formatPackageLines(r))
|
|
359
|
+
}
|
|
360
|
+
return lines.join('\n')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* CLI-вхід: `bun .../storybook/adopt/main.mjs [--fix-missing] [--cwd <path>] [rootDir...]`.
|
|
365
|
+
* @returns {Promise<void>} завершення прогону (друкує звіт, exit-код — кількість `differs`/`broken`)
|
|
366
|
+
*/
|
|
367
|
+
async function runCli() {
|
|
368
|
+
const args = process.argv.slice(2)
|
|
369
|
+
const fixMissing = args.includes('--fix-missing')
|
|
370
|
+
const cwdIdx = args.indexOf('--cwd')
|
|
371
|
+
const cwd = cwdIdx !== -1 && args[cwdIdx + 1] ? args[cwdIdx + 1] : process.cwd()
|
|
372
|
+
const rootDirs = args.filter((a, i) => !a.startsWith('--') && args[i - 1] !== '--cwd')
|
|
373
|
+
|
|
374
|
+
const results = await runAdopt(cwd, { fixMissing, rootDirs })
|
|
375
|
+
console.log(formatReport(results))
|
|
376
|
+
const hasIssues = results.some(r => r.status === 'differs' || r.status === 'broken')
|
|
377
|
+
process.exitCode = hasIssues ? 1 : 0
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (isRunAsCli(import.meta.url)) {
|
|
381
|
+
await runCli()
|
|
382
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
|
|
3
|
+
"fixability": "config",
|
|
4
|
+
"requires": { "capability": "ci:github" },
|
|
5
|
+
"lint": {
|
|
6
|
+
"scope": "full",
|
|
7
|
+
"glob": [
|
|
8
|
+
"package.json",
|
|
9
|
+
"**/*.vue",
|
|
10
|
+
".storybook/**",
|
|
11
|
+
".github/actions/setup-playwright-chromium/action.yml",
|
|
12
|
+
".github/workflows/lint-storybook.yml"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: fix-ci.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/ci/fix-ci.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 877b9feb
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 90
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Порожній шаблон `storybook/ci`, з якого відтворюються репо-рівневі CI-артефакти: `setup-playwright-chromium` як verbatim-копія composite action для Playwright Chromium і `.github/workflows/lint-storybook.yml` із матрицею `strategy.matrix.package`, згенерованою з фактичного списку пакетів через `collectInScopeVuePackages`. Публічні anchors: `TEMPLATE_DIR`, `renderPlaywrightAction`, `renderPackageDirsYaml`, `renderStorybookWorkflow`, `patterns`.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- **TEMPLATE_DIR** — вказує на каталог `template/` цього concern-а як джерело канонічних CI-шаблонів.
|
|
20
|
+
- **renderPlaywrightAction** — читає канонічний composite action `setup-playwright-chromium` із шаблону без змін.
|
|
21
|
+
- **renderPackageDirsYaml** — перетворює список коренів пакетів у YAML-рядки для матриці Storybook CI.
|
|
22
|
+
- **renderStorybookWorkflow** — збирає канонічний `.github/workflows/lint-storybook.yml`, підставляючи в нього матрицю пакетів зі скоупу репозиторію.
|
|
23
|
+
- **patterns** — описує T0-правила автофіксу для відновлення відсутніх CI-файлів цього concern-а.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- TEMPLATE_DIR — Каталог `template/` цього concern-а.
|
|
28
|
+
- renderPlaywrightAction — Вміст канонічного composite action `setup-playwright-chromium` — verbatim з template.
|
|
29
|
+
- renderPackageDirsYaml — Рендерить YAML-фрагмент `matrix.package` списку — по одному `- <rootDir>` на рядок, з тим
|
|
30
|
+
самим відступом, що й токен-рядок у шаблоні (10 пробілів — рівень елемента списку під
|
|
31
|
+
`strategy.matrix.package:`). `rootDir === '.'` (корінь монорепо) лишається `.` — валідний
|
|
32
|
+
`working-directory` для GitHub Actions.
|
|
33
|
+
- renderStorybookWorkflow — Вміст канонічного `.github/workflows/lint-storybook.yml` — template з підставленою
|
|
34
|
+
матрицею пакетів у скоупі.
|
|
35
|
+
- patterns — повертає набір шаблонів, за якими далі шукають або зіставляють потрібні значення.
|
|
36
|
+
|
|
37
|
+
## Гарантії поведінки
|
|
38
|
+
|
|
39
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/rules/storybook/ci
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/ci/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ----------------------- | --------- |
|
|
9
|
+
| [fix-ci.mjs](fix-ci.md) | JS Module |
|
|
10
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/ci/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 65df5791
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 85
|
|
10
|
+
issues: internal-name:collectInScopeVuePackages,anchor-miss:(storybook.mdc),judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл фіксує канонічні репо-шляхи й маркери для storybook CI, щоб у Vue component library пакетах у scope швидко знаходити правильні `.github/actions/setup-playwright-chromium/action.yml` і `.github/workflows/lint-storybook.yml` та зіставляти їх із повідомленнями в `storybook.mdc`. Експортовані константи-рядки: PLAYWRIGHT_ACTION_REL=".github/actions/setup-playwright-chromium/action.yml" — канонічний шлях до Playwright action; STORYBOOK_WORKFLOW_REL=".github/workflows/lint-storybook.yml" — канонічний шлях до workflow linting. Маркери повідомлень: PLAYWRIGHT_ACTION_MARKERS і STORYBOOK_WORKFLOW_MARKERS, які прив’язують перевірки до повідомлень у `storybook.mdc`. Це потрібно, щоб підтримувати єдиний storybook-канон без зайвого обходу `.github` і `.git`, із кешуванням результату в межах одного прогону.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- PLAYWRIGHT_ACTION_REL — задає репо-рівневий шлях до канонічного composite action `.github/actions/setup-playwright-chromium/action.yml`, який перевіряється як частина storybook CI.
|
|
21
|
+
- STORYBOOK_WORKFLOW_REL — задає репо-рівневий шлях до канонічного workflow `.github/workflows/lint-storybook.yml`, що відповідає за швидкий storybook-лінт.
|
|
22
|
+
- PLAYWRIGHT_ACTION_MARKERS — описує канонічні ознаки composite action для storybook CI, щоб гарантувати кеш Playwright і встановлення лише chromium; у повідомленнях використовується прив’язка до `storybook.mdc`.
|
|
23
|
+
- STORYBOOK_WORKFLOW_MARKERS — описує канонічні ознаки workflow для storybook CI, щоб гарантувати запуск швидкого `vitest --project=storybook`; у повідомленнях використовується прив’язка до `storybook.mdc`.
|
|
24
|
+
- lint — перевіряє наявність storybook CI-канону лише для репозиторіїв із Vue component library пакетами в scope; якщо таких пакетів немає, завершується без зауважень. Свідомо не зачіпає `.github` і `.git` як окремі області обходу; кешує результат у межах одного прогону.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- PLAYWRIGHT_ACTION_REL — Repo-relative шлях канонічного composite action (не per-package — один на репозиторій).
|
|
29
|
+
- STORYBOOK_WORKFLOW_REL — Repo-relative шлях канонічного workflow, що запускає `vitest --project=storybook`.
|
|
30
|
+
- PLAYWRIGHT_ACTION_MARKERS — Маркери канону composite action `setup-playwright-chromium` (ADR Кластер 5): кеш
|
|
31
|
+
`ms-playwright` через `actions/cache`, ключ від версії playwright, install лише chromium.
|
|
32
|
+
Текстовий пошук — той самий підхід, що й `MAIN_JS_MARKERS`/`PREVIEW_JS_MARKERS` у `scaffold`.
|
|
33
|
+
- STORYBOOK_WORKFLOW_MARKERS — Маркери канону `.github/workflows/lint-storybook.yml`: композитний Playwright-кеш ПІСЛЯ
|
|
34
|
+
setup-bun-deps, і швидкий `vitest --project=storybook` (ADR Кластер 5 — nightly-only
|
|
35
|
+
`@7n/test coverage`/mutation-testing на PR не запускається, лише цей швидкий шлях).
|
|
36
|
+
- lint — Detector concern-а `storybook/ci` (ADR Кластер 5, CI-частина): для репозиторіїв з бодай
|
|
37
|
+
одним Vue component library пакетом у скоупі Storybook (`collectInScopeVuePackages`) —
|
|
38
|
+
канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише
|
|
39
|
+
chromium) і канонічний `.github/workflows/lint-storybook.yml`, що запускає швидкий
|
|
40
|
+
`vitest --project=storybook` на PR. Гейтований `requires.capability: ci:github` — спить
|
|
41
|
+
у репозиторіях без плагіна `@7n/rules-ci-github` (немає `.github/workflows`).
|
|
42
|
+
|
|
43
|
+
Nightly-only `@7n/test coverage` (mutation testing) — поза обсягом цього concern-а: ADR
|
|
44
|
+
Кластер 5 явно розділяє швидкий PR-шлях (цей concern) і nightly mutation-прогін, який
|
|
45
|
+
лишається окремою інфраструктурою `test/stryker_config`.
|
|
46
|
+
|
|
47
|
+
## Гарантії поведінки
|
|
48
|
+
|
|
49
|
+
- Кешує результати в межах одного прогону.
|
|
50
|
+
- Свідомо пропускає шляхи: `.github`, `.git`.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-autofix для concern-а `storybook/ci` (ADR Кластер 5, CI-частина): відтворює канонічний
|
|
3
|
+
* composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише chromium) і
|
|
4
|
+
* канонічний `.github/workflows/lint-storybook.yml` з `template/` цього concern-а.
|
|
5
|
+
*
|
|
6
|
+
* Обидва файли — репо-рівневі (не per-package): composite action — verbatim-копія; workflow —
|
|
7
|
+
* матриця `strategy.matrix.package` генерується з фактичного списку пакетів у скоупі
|
|
8
|
+
* (`collectInScopeVuePackages`), щоб CI реально покривав усі Storybook-пакети репозиторію.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { readFile } from 'node:fs/promises'
|
|
14
|
+
|
|
15
|
+
import { collectInScopeVuePackages } from '../scope/main.mjs'
|
|
16
|
+
|
|
17
|
+
/** Каталог `template/` цього concern-а. */
|
|
18
|
+
export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'template')
|
|
19
|
+
|
|
20
|
+
/** Токен у `lint-storybook.yml.snippet.yml`, що заміщається списком пакетів матриці. */
|
|
21
|
+
const PACKAGE_DIRS_TOKEN = '__STORYBOOK_CI_PACKAGE_DIRS__'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Вміст канонічного composite action `setup-playwright-chromium` — verbatim з template.
|
|
25
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
26
|
+
* @returns {Promise<string>} вміст `action.yml`
|
|
27
|
+
*/
|
|
28
|
+
export function renderPlaywrightAction(templateDir = TEMPLATE_DIR) {
|
|
29
|
+
return readFile(join(templateDir, 'setup-playwright-chromium.action.yml'), 'utf8')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Рендерить YAML-фрагмент `matrix.package` списку — по одному `- <rootDir>` на рядок, з тим
|
|
34
|
+
* самим відступом, що й токен-рядок у шаблоні (10 пробілів — рівень елемента списку під
|
|
35
|
+
* `strategy.matrix.package:`). `rootDir === '.'` (корінь монорепо) лишається `.` — валідний
|
|
36
|
+
* `working-directory` для GitHub Actions.
|
|
37
|
+
* @param {string[]} rootDirs відносні (posix) корені пакетів у скоупі
|
|
38
|
+
* @returns {string} YAML-рядки (без завершального переносу)
|
|
39
|
+
*/
|
|
40
|
+
export function renderPackageDirsYaml(rootDirs) {
|
|
41
|
+
return rootDirs.map(dir => ` - ${dir}`).join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Вміст канонічного `.github/workflows/lint-storybook.yml` — template з підставленою
|
|
46
|
+
* матрицею пакетів у скоупі.
|
|
47
|
+
* @param {string[]} rootDirs відносні корені пакетів у скоупі (`collectInScopeVuePackages`)
|
|
48
|
+
* @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
|
|
49
|
+
* @returns {Promise<string>} вміст `lint-storybook.yml`
|
|
50
|
+
*/
|
|
51
|
+
export async function renderStorybookWorkflow(rootDirs, templateDir = TEMPLATE_DIR) {
|
|
52
|
+
const raw = await readFile(join(templateDir, 'lint-storybook.yml.snippet.yml'), 'utf8')
|
|
53
|
+
const tokenLineRe = new RegExp(`^[ \\t]*${PACKAGE_DIRS_TOKEN}[ \\t]*$`, 'mu')
|
|
54
|
+
// Replacer-функція (не рядок) — уникає unicorn/no-unsafe-string-replacement:
|
|
55
|
+
// рядок-заміна мав би трактувати `$`-послідовності як спецсимволи `String#replace`.
|
|
56
|
+
return raw.replace(tokenLineRe, () => renderPackageDirsYaml(rootDirs))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Записує файл, створюючи батьківські каталоги й реєструючи запис для rollback (той самий
|
|
61
|
+
* патерн, що й `writeScaffoldFile` у `scaffold/fix-scaffold.mjs`).
|
|
62
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').FixContext} ctx fix-контекст рунга
|
|
63
|
+
* @param {string} absPath абсолютний шлях цільового файлу
|
|
64
|
+
* @param {string} content вміст файлу
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
function writeCiFile(ctx, absPath, content) {
|
|
68
|
+
mkdirSync(dirname(absPath), { recursive: true })
|
|
69
|
+
ctx.recordWrite?.(absPath)
|
|
70
|
+
writeFileSync(absPath, content, 'utf8')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
74
|
+
export const patterns = [
|
|
75
|
+
{
|
|
76
|
+
id: 'storybook-ci-playwright-action',
|
|
77
|
+
test: violations => violations.some(v => v.reason === 'missing-playwright-action' && v.file),
|
|
78
|
+
apply: async (violations, ctx) => {
|
|
79
|
+
const target = violations.find(v => v.reason === 'missing-playwright-action' && v.file)
|
|
80
|
+
if (!target || !ctx.concernDir) return { touchedFiles: [] }
|
|
81
|
+
|
|
82
|
+
const content = await renderPlaywrightAction(join(ctx.concernDir, 'template'))
|
|
83
|
+
const abs = join(ctx.cwd, target.file)
|
|
84
|
+
writeCiFile(ctx, abs, content)
|
|
85
|
+
return { touchedFiles: [abs], message: `${target.file}: створено composite action setup-playwright-chromium` }
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'storybook-ci-workflow',
|
|
90
|
+
test: violations => violations.some(v => v.reason === 'missing-storybook-workflow' && v.file),
|
|
91
|
+
apply: async (violations, ctx) => {
|
|
92
|
+
const target = violations.find(v => v.reason === 'missing-storybook-workflow' && v.file)
|
|
93
|
+
if (!target || !ctx.concernDir) return { touchedFiles: [] }
|
|
94
|
+
|
|
95
|
+
const pkgs = await collectInScopeVuePackages(ctx.cwd)
|
|
96
|
+
const rootDirs = pkgs.map(p => p.rootDir)
|
|
97
|
+
if (rootDirs.length === 0) return { touchedFiles: [] }
|
|
98
|
+
|
|
99
|
+
const content = await renderStorybookWorkflow(rootDirs, join(ctx.concernDir, 'template'))
|
|
100
|
+
const abs = join(ctx.cwd, target.file)
|
|
101
|
+
writeCiFile(ctx, abs, content)
|
|
102
|
+
return { touchedFiles: [abs], message: `${target.file}: створено (${rootDirs.length} пакет(ів) у матриці)` }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|