@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,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: preview.js
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/preview.js
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 5590ef55
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл задає канонічний preview для Vue-компонентних бібліотек відповідно до `storybook.mdc` та ADR `канон-storybook-для-vue-компонентних-бібліотек`. Він існує, щоб Storybook мав відтворювану базову конфігурацію для перегляду компонентів у бібліотеках.
|
|
17
|
+
|
|
18
|
+
Файл згенеровано правилом `storybook`: команда `npx @7n/rules fix storybook` відтворює його, якщо файл видалено або зламано канон.
|
|
19
|
+
|
|
20
|
+
Файл є read-only: не пише у ФС чи БД.
|
|
21
|
+
|
|
22
|
+
## Поведінка
|
|
23
|
+
|
|
24
|
+
1. Готує Storybook як канонічне середовище для Vue-компонентних бібліотек, щоб stories працювали наближено до реального застосунку-споживача.
|
|
25
|
+
|
|
26
|
+
2. Підключає базові стилі Quasar, щоб компоненти відображалися з очікуваним виглядом у Storybook.
|
|
27
|
+
|
|
28
|
+
3. Вмикає перехоплення мережевих запитів у Storybook, щоб компоненти могли працювати з контрольованими mock-відповідями.
|
|
29
|
+
|
|
30
|
+
4. Ігнорує очікувані same-origin GET-запити dev-сервера, зокрема для HMR і статичних файлів, щоб вони не створювали зайвого шуму.
|
|
31
|
+
|
|
32
|
+
5. Позначає інші мережеві запити, які не підпадають під тихий пропуск, попередженням, а не помилкою збірки, щоб дати м’який сигнал без блокування роботи Storybook.
|
|
33
|
+
|
|
34
|
+
6. Встановлює Quasar у Storybook повністю, щоб бібліотечні компоненти мали доступ до глобальних Quasar-примітивів так само, як у реальному застосунку.
|
|
35
|
+
|
|
36
|
+
7. Додає Quasar-плагіни для повідомлень і діалогів, щоб stories могли відтворювати сценарії з користувацькими сповіщеннями та модальними взаємодіями.
|
|
37
|
+
|
|
38
|
+
8. Налаштовує набір іконок Quasar, щоб компоненти коректно показували матеріальні SVG-іконки поза повним Quasar CLI-середовищем.
|
|
39
|
+
|
|
40
|
+
9. Передає Storybook декоратор для mock service worker, щоб мережеве оточення застосовувалося до stories централізовано.
|
|
41
|
+
|
|
42
|
+
10. Налаштовує автоматичне розпізнавання controls-панелі Storybook для кольорів і дат, щоб інтерфейс керування props був зручнішим для перегляду компонентів.
|
|
43
|
+
|
|
44
|
+
## Гарантії поведінки
|
|
45
|
+
|
|
46
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний конфіг Storybook для Vue-компонентних бібліотек (storybook.mdc, ADR
|
|
3
|
+
* канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
|
|
4
|
+
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
|
+
*/
|
|
6
|
+
import { loadConfigFromFile, mergeConfig } from 'vite'
|
|
7
|
+
import vue from '@vitejs/plugin-vue'
|
|
8
|
+
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
|
9
|
+
|
|
10
|
+
// Плагіни власного Vite-збирача Storybook — порядок ФІКСОВАНИЙ: @vitejs/plugin-vue
|
|
11
|
+
// ПЕРЕД quasar() (інакше Quasar-плагін не бачить SFC, уже скомпільований plugin-vue).
|
|
12
|
+
const OWN_PLUGINS = [vue({ template: { transformAssetUrls } }), quasar({ sassVariables: true })]
|
|
13
|
+
|
|
14
|
+
// Плагіни файлової маршрутизації додатка-споживача — не мають сенсу в ізольованому
|
|
15
|
+
// рендері одного компонента, знімаються при обʼєднанні з vite.config пакета. Обидві
|
|
16
|
+
// версії layouts-пакета (старий і `-next`-форк) та pages-роутери — реальні консюмери
|
|
17
|
+
// мігрують поступово, старий і новий пакет можуть співіснувати в різних vite.config.
|
|
18
|
+
const INCOMPATIBLE_PLUGIN_NAMES = new Set([
|
|
19
|
+
'vite-plugin-pages',
|
|
20
|
+
'unplugin-vue-router',
|
|
21
|
+
'vite-plugin-vue-layouts',
|
|
22
|
+
'vite-plugin-vue-layouts-next'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Плагін належить до сімейства Vue SFC-трансформерів, які ДУБЛЮЮТЬ `OWN_PLUGINS`'
|
|
27
|
+
* `vue()`. Реальний стек консюмерів (components/npm/vite.config.js) обгортає
|
|
28
|
+
* `@vitejs/plugin-vue` через `VueMacros({ plugins: { vue: Vue() } })` — після
|
|
29
|
+
* резолву цей виклик повертає МАСИВ плагінів: сам `vite:vue` (той самий transform,
|
|
30
|
+
* що й `OWN_PLUGINS`' `vue()` — дублювання дає ПОДВІЙНУ SFC-трансформацію) плюс
|
|
31
|
+
* службові `vue-macros-*` (devtools/exclude-dep-optimize). Плагіни macro-синтаксису
|
|
32
|
+
* (`unplugin-vue-define-props`/`define-emit`/`reactivity-transform` тощо) — НЕ
|
|
33
|
+
* фільтруються: вони не дублюються `OWN_PLUGINS` і потрібні для macro-фіч пакета;
|
|
34
|
+
* Vite впорядковує їх через власний `enforce: 'pre'|'post'` незалежно від позиції
|
|
35
|
+
* в підсумковому масиві `plugins`, тож порядок відносно `OWN_PLUGINS` тут не важливий.
|
|
36
|
+
* @param {string | undefined} name ім'я плагіна
|
|
37
|
+
* @returns {boolean} true — плагін дублює `OWN_PLUGINS`' `vue()`
|
|
38
|
+
*/
|
|
39
|
+
function isVueTransformFamily(name) {
|
|
40
|
+
return typeof name === 'string' && (name.startsWith('vite:vue') || name.includes('vue-macros'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Резолвить один запис `config.plugins` у плаский масив реальних плагінів. Vite
|
|
45
|
+
* офіційно підтримує `Plugin | Promise<Plugin> | (Plugin | Promise<Plugin>)[]`
|
|
46
|
+
* (довільна вкладеність) — `VueMacros(...)` сам повертає `Promise`, що резолвиться
|
|
47
|
+
* в масив плагінів. `loadConfigFromFile` читає файл конфіга як є й НЕ виконує це
|
|
48
|
+
* resolve/flatten (це робить лише повний `resolveConfig` пізніше у власному циклі
|
|
49
|
+
* Vite) — без ручного resolve/flatten тут фільтр порівнював би ім'я з
|
|
50
|
+
* `Promise`-об'єктом, що ще не резолвився (`undefined`), і пропускав би дублікат далі.
|
|
51
|
+
* @param {unknown} entry один елемент/Promise/масив із `config.plugins`
|
|
52
|
+
* @returns {Promise<object[]>} плаский масив плагінів після resolve
|
|
53
|
+
*/
|
|
54
|
+
async function resolvePluginEntry(entry) {
|
|
55
|
+
const resolved = await entry
|
|
56
|
+
if (Array.isArray(resolved)) {
|
|
57
|
+
const nested = await Promise.all(resolved.map(resolvePluginEntry))
|
|
58
|
+
return nested.flat(Infinity)
|
|
59
|
+
}
|
|
60
|
+
return resolved ? [resolved] : []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @type {import('@storybook/vue3-vite').StorybookConfig} */
|
|
64
|
+
const config = {
|
|
65
|
+
stories: ['__STORYBOOK_STORIES_GLOB__'],
|
|
66
|
+
framework: {
|
|
67
|
+
name: '@storybook/vue3-vite',
|
|
68
|
+
options: {}
|
|
69
|
+
},
|
|
70
|
+
// Публічний asset для msw service worker, який ініціалізує preview.js.
|
|
71
|
+
staticDirs: ['./public'],
|
|
72
|
+
async viteFinal(storybookConfig) {
|
|
73
|
+
const loaded = await loadConfigFromFile({ command: 'serve', mode: 'development' }, undefined, process.cwd())
|
|
74
|
+
const rawPlugins = loaded?.config?.plugins ?? []
|
|
75
|
+
const resolvedNested = await Promise.all(rawPlugins.map(resolvePluginEntry))
|
|
76
|
+
const userPlugins = resolvedNested
|
|
77
|
+
.flat(Infinity)
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.filter(p => !INCOMPATIBLE_PLUGIN_NAMES.has(p.name))
|
|
80
|
+
// vue()/quasar() пакета замінюємо власними екземплярами у фіксованому порядку вище —
|
|
81
|
+
// не дублюємо; решта плагінів пакета (auto-import, VueMacros macro-sugar тощо) лишається.
|
|
82
|
+
.filter(p => !isVueTransformFamily(p.name) && p.name !== 'quasar')
|
|
83
|
+
|
|
84
|
+
return mergeConfig(storybookConfig, {
|
|
85
|
+
resolve: loaded?.config?.resolve,
|
|
86
|
+
css: loaded?.config?.css,
|
|
87
|
+
plugins: [...OWN_PLUGINS, ...userPlugins]
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default config
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: gql-sse.js
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/mocks/gql-sse.js
|
|
5
|
+
docgen:
|
|
6
|
+
crc: fe63fba2
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.96
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Надає єдиний MSW-хелпер `sseSubscription` для мокання Apollo GraphQL-підписок у Storybook через `graphql-sse`. Кожна подія виконання підписки надсилається як окремий SSE `next`-запис у distinct-connection режимі: `event: next\ndata: <JSON>\n\n`, щоб пакети не дублювали формат цього wire-протоколу.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. `sseSubscription` приймає послідовність готових GraphQL execution-result повідомлень для однієї підписки.
|
|
21
|
+
|
|
22
|
+
2. Для кожного повідомлення створює окрему SSE-подію типу `next`, сумісну з wire-протоколом `graphql-sse` у distinct-connection режимі.
|
|
23
|
+
|
|
24
|
+
3. Повертає потік тіла відповіді, який MSW-хендлер може використати для мокання Apollo GraphQL-підписки у Storybook.
|
|
25
|
+
|
|
26
|
+
4. Завершує потік після надсилання всіх повідомлень, щоб сценарій підписки мав детермінований кінець.
|
|
27
|
+
|
|
28
|
+
5. Централізує формат тестових subscription-подій, щоб пакети не дублювали власну реалізацію цього протоколу.
|
|
29
|
+
|
|
30
|
+
## Публічний API
|
|
31
|
+
|
|
32
|
+
- sseSubscription — Формує SSE-стрім тіла відповіді для MSW-хендлера підписки graphql-sse.
|
|
33
|
+
|
|
34
|
+
## Гарантії поведінки
|
|
35
|
+
|
|
36
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/rules/storybook/scaffold/template/mocks
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scaffold/template/mocks/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ------------------------ | --------- |
|
|
9
|
+
| [gql-sse.js](gql-sse.md) | JS Module |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний MSW-хелпер для мокання Apollo GraphQL-підписок через `graphql-sse` (storybook.mdc,
|
|
3
|
+
* ADR канон-storybook-для-vue-компонентних-бібліотек, Кластер 3). Wire-протокол
|
|
4
|
+
* `graphql-sse` у distinct-connection режимі — кожна подія виконання підписки надсилається
|
|
5
|
+
* як окремий SSE `next`-запис: `event: next\ndata: <JSON>\n\n`. Одне джерело істини для цього
|
|
6
|
+
* протоколу — не переносити копію логіки в кожен пакет окремо.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Формує SSE-стрім тіла відповіді для MSW-хендлера підписки graphql-sse.
|
|
11
|
+
* @param {Array<object>} payloads послідовність GraphQL execution-result payload-ів — кожен
|
|
12
|
+
* елемент те, що `graphql-sse` надіслав би як окрему `next`-подію одного виконання підписки
|
|
13
|
+
* @returns {ReadableStream<Uint8Array>} SSE body-стрім для MSW-response (`HttpResponse` body)
|
|
14
|
+
*/
|
|
15
|
+
export function sseSubscription(payloads) {
|
|
16
|
+
const encoder = new TextEncoder()
|
|
17
|
+
return new ReadableStream({
|
|
18
|
+
start(controller) {
|
|
19
|
+
for (const payload of payloads) {
|
|
20
|
+
controller.enqueue(encoder.encode(`event: next\ndata: ${JSON.stringify(payload)}\n\n`))
|
|
21
|
+
}
|
|
22
|
+
controller.close()
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонічний preview для Vue-компонентних бібліотек (storybook.mdc, ADR
|
|
3
|
+
* канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
|
|
4
|
+
* `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
|
|
5
|
+
*/
|
|
6
|
+
import { setup } from '@storybook/vue3'
|
|
7
|
+
import { Dialog, iconSet as quasarBuiltinIconSet, Notify, Quasar } from 'quasar'
|
|
8
|
+
import iconSet from 'quasar/icon-set/svg-material-icons'
|
|
9
|
+
import { initialize, mswDecorator } from 'msw-storybook-addon'
|
|
10
|
+
|
|
11
|
+
import 'quasar/dist/quasar.css'
|
|
12
|
+
|
|
13
|
+
// msw-storybook-addon: перехоплює мережеві запити компонентів у Storybook. Same-origin GET
|
|
14
|
+
// (Vite HMR, статичні файли dev-сервера) — очікуваний шум, мовчки пропускаємо; усе інше —
|
|
15
|
+
// warn (не білд-помилка навмисно, хвиля 1 — м'який сигнал, не гейт).
|
|
16
|
+
initialize({
|
|
17
|
+
onUnhandledRequest(request, print) {
|
|
18
|
+
const url = new URL(request.url)
|
|
19
|
+
if (request.method === 'GET' && url.origin === globalThis.location?.origin) return
|
|
20
|
+
print.warning()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
setup(app => {
|
|
25
|
+
// Повний install Quasar (не окремі компоненти) — бібліотека компонентів очікує
|
|
26
|
+
// глобально зареєстровані Quasar-примітиви (QBtn, QCard тощо) так само, як у реальному
|
|
27
|
+
// додатку-споживачі.
|
|
28
|
+
app.use(Quasar, {
|
|
29
|
+
plugins: { Notify, Dialog },
|
|
30
|
+
iconSet,
|
|
31
|
+
// iconMapFn — без нього внутрішні Quasar-компоненти (напр. стрілка QSelect) не
|
|
32
|
+
// резолвлять власні вбудовані іконки поза full Quasar CLI build.
|
|
33
|
+
iconMapFn(iconName) {
|
|
34
|
+
return quasarBuiltinIconSet.iconMapFn?.(iconName)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** @type {import('@storybook/vue3').Preview} */
|
|
40
|
+
const preview = {
|
|
41
|
+
decorators: [mswDecorator],
|
|
42
|
+
parameters: {
|
|
43
|
+
controls: { matchers: { color: /(background|color)$/iu, date: /Date$/u } }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default preview
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/scope/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 04c71162
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 90
|
|
10
|
+
issues: internal-name:isVueComponentLibraryPkg,judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл визначає, які workspace-пакети мають потрапити в скоуп Storybook: спирається на `.n-rules.json`, `.n-cursor.json` і `package.json`, враховує opt-out, поріг Vue-файлів, стандартний build та окремий прапорець для app-проєктів. Також він fail-safe перевіряє, що `storybook.optOut` не містить посилань на неіснуючі пакети.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `VUE_FILE_THRESHOLD` задає мінімальну кількість Vue-файлів, після якої пакет може потрапити в скоуп Storybook.
|
|
21
|
+
- `readStorybookOptOut` читає з `.n-rules.json` або legacy `.n-cursor.json` перелік workspace-пакетів, які свідомо виключені зі скоупу Storybook; за відсутності або пошкодженості конфіга повертає порожній перелік.
|
|
22
|
+
- `readDetectAppsFlag` читає з `.n-rules.json` або legacy `.n-cursor.json` явний прапорець включення app-проєктів у детекцію Storybook; за замовчуванням лишає їх поза скоупом.
|
|
23
|
+
- `countVueFiles` рахує Vue-файли в дереві пакета з урахуванням шляхів, виключених конфігурацією і правилами ігнорування.
|
|
24
|
+
- `hasStandardBuild` визначає, чи має пакет підтримуваний Vite build-конфіг у корені, потрібний для канонічного Storybook setup.
|
|
25
|
+
- `isVueAppPkg` визначає, чи виглядає `package.json` як Vue app-проєкт, а не компонентна бібліотека.
|
|
26
|
+
- `collectInScopeVuePackages` збирає workspace-пакети зі стандартним build, достатньою кількістю Vue-файлів і без `storybook.optOut`; app-проєкти додає лише після явного увімкнення відповідного прапорця.
|
|
27
|
+
- `lint` перевіряє гігієну `storybook.optOut`: кожен запис має посилатися на наявний workspace-пакет, інакше звітує про застаріле налаштування.
|
|
28
|
+
|
|
29
|
+
## Публічний API
|
|
30
|
+
|
|
31
|
+
- VUE_FILE_THRESHOLD — Поріг кількості `.vue`-файлів для скоупу канону Storybook (ADR Кластер 1).
|
|
32
|
+
- readStorybookOptOut — Читає `storybook.optOut` з `.n-rules.json` (fallback — legacy `.n-cursor.json`). Толерантно до
|
|
33
|
+
відсутнього файлу/поля/невалідного JSON — повертає порожній масив (open-by-default, як
|
|
34
|
+
`read-n-rules-config-lite.mjs`). Значення — root dir пакетів (`.` для кореня, `packages/ui` тощо),
|
|
35
|
+
той самий формат, що повертає `getMonorepoPackageRootDirs`.
|
|
36
|
+
- readDetectAppsFlag — Читає прапорець хвилі 2 `storybook.detectApps` з `.n-rules.json`. За замовчуванням `false` —
|
|
37
|
+
детекція app-проєктів (`vue` у dependencies + `src/pages/`) лишається відкритим питанням ADR
|
|
38
|
+
і не впливає на скоуп, доки консюмер-репо не увімкне прапорець явно.
|
|
39
|
+
- countVueFiles — Рахує `.vue`-файли в дереві пакета (поважає `.gitignore` й `ignore` з `.n-rules.json` через
|
|
40
|
+
`walkDir`/`ignorePaths` — той самий обхід, що й `vue/packages`).
|
|
41
|
+
- hasStandardBuild — Чи має пакет "стандартний" build — розпізнаваний `vite.config.{js,ts,mjs}` у корені пакета.
|
|
42
|
+
Канонічний `.storybook/main.js` спирається саме на цей файл (`viteFinal` мерджить його
|
|
43
|
+
плагіни) — без нього автоматичний скафолд неможливий, і пакет пропускається мовчки
|
|
44
|
+
(ADR Кластер 1: "skip пакетів із нестандартним build").
|
|
45
|
+
- isVueAppPkg — Чи є пакет app-проєктом (не бібліотекою) для хвилі 2: `vue` у `dependencies` (не лише
|
|
46
|
+
`peerDependencies`) і не бібліотека компонентів. Реалізовано зараз (щоб не переписувати
|
|
47
|
+
модуль пізніше), але результат впливає на скоуп лише за прапорця `storybook.detectApps`.
|
|
48
|
+
- collectInScopeVuePackages — Збирає workspace-пакети у скоупі канону Storybook хвилі 1: Vue-компонентна бібліотека
|
|
49
|
+
(`vue` у `peerDependencies`, маркер `isVueComponentLibraryPkg` — той самий, що й `vue.mdc`)
|
|
50
|
+
з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`, зі
|
|
51
|
+
стандартним build (`vite.config.*`). Хвиля 2 (app-проєкти) додається лише за явного
|
|
52
|
+
прапорця `storybook.detectApps` у `.n-rules.json`.
|
|
53
|
+
- lint — Self-check конфігурації: `.n-rules.json` → `storybook.optOut` не має посилатись на
|
|
54
|
+
неіснуючі workspace-пакети (застаріле налаштування — пакет перейменували/видалили, а
|
|
55
|
+
opt-out лишився). Сама детекція скоупу (поріг, build, app-проєкти) — pure-функції вище,
|
|
56
|
+
покриті тестами напряму; тут лише конфіг-гігієна.
|
|
57
|
+
|
|
58
|
+
## Гарантії поведінки
|
|
59
|
+
|
|
60
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
61
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
62
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** @see ./docs/main.md */
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
|
|
7
|
+
import { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
|
|
8
|
+
import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
|
|
9
|
+
import { getMonorepoPackageRootDirs } from '@7n/rules/scripts/lib/workspaces.mjs'
|
|
10
|
+
import { isVueComponentLibraryPkg } from '../../vue/packages/main.mjs'
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILE = '.n-rules.json'
|
|
13
|
+
const LEGACY_CONFIG_FILE = '.n-cursor.json'
|
|
14
|
+
const VITE_CONFIG_FILES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
15
|
+
|
|
16
|
+
/** Поріг кількості `.vue`-файлів для скоупу канону Storybook (ADR Кластер 1). */
|
|
17
|
+
export const VUE_FILE_THRESHOLD = 3
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} InScopePackage
|
|
21
|
+
* @property {string} rootDir відносний (posix) корінь пакета, `.` для кореня монорепо
|
|
22
|
+
* @property {string} absDir абсолютний шлях кореня пакета
|
|
23
|
+
* @property {Record<string, unknown>} pkg розпарсений `package.json` пакета
|
|
24
|
+
* @property {number} vueFileCount кількість знайдених `.vue`-файлів
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Читає `storybook.optOut` з `.n-rules.json` (fallback — legacy `.n-cursor.json`). Толерантно до
|
|
29
|
+
* відсутнього файлу/поля/невалідного JSON — повертає порожній масив (open-by-default, як
|
|
30
|
+
* `read-n-rules-config-lite.mjs`). Значення — root dir пакетів (`.` для кореня, `packages/ui` тощо),
|
|
31
|
+
* той самий формат, що повертає `getMonorepoPackageRootDirs`.
|
|
32
|
+
* @param {string} cwd абсолютний корінь репозиторію
|
|
33
|
+
* @returns {Promise<string[]>} перелік opt-out root dir-ів
|
|
34
|
+
*/
|
|
35
|
+
export async function readStorybookOptOut(cwd) {
|
|
36
|
+
let file = join(cwd, CONFIG_FILE)
|
|
37
|
+
if (!existsSync(file)) file = join(cwd, LEGACY_CONFIG_FILE)
|
|
38
|
+
if (!existsSync(file)) return []
|
|
39
|
+
let raw
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(await readFile(file, 'utf8'))
|
|
42
|
+
} catch {
|
|
43
|
+
return []
|
|
44
|
+
}
|
|
45
|
+
const list = raw?.storybook?.optOut
|
|
46
|
+
if (!Array.isArray(list)) return []
|
|
47
|
+
return list.filter(v => typeof v === 'string' && v.trim().length > 0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Читає прапорець хвилі 2 `storybook.detectApps` з `.n-rules.json`. За замовчуванням `false` —
|
|
52
|
+
* детекція app-проєктів (`vue` у dependencies + `src/pages/`) лишається відкритим питанням ADR
|
|
53
|
+
* і не впливає на скоуп, доки консюмер-репо не увімкне прапорець явно.
|
|
54
|
+
* @param {string} cwd абсолютний корінь репозиторію
|
|
55
|
+
* @returns {Promise<boolean>} true, якщо app-проєкти теж треба зібрати у скоуп
|
|
56
|
+
*/
|
|
57
|
+
export async function readDetectAppsFlag(cwd) {
|
|
58
|
+
let file = join(cwd, CONFIG_FILE)
|
|
59
|
+
if (!existsSync(file)) file = join(cwd, LEGACY_CONFIG_FILE)
|
|
60
|
+
if (!existsSync(file)) return false
|
|
61
|
+
let raw
|
|
62
|
+
try {
|
|
63
|
+
raw = JSON.parse(await readFile(file, 'utf8'))
|
|
64
|
+
} catch {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
return raw?.storybook?.detectApps === true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Рахує `.vue`-файли в дереві пакета (поважає `.gitignore` й `ignore` з `.n-rules.json` через
|
|
72
|
+
* `walkDir`/`ignorePaths` — той самий обхід, що й `vue/packages`).
|
|
73
|
+
* @param {string} absPkgDir абсолютний шлях кореня пакета
|
|
74
|
+
* @param {string[]} ignorePaths абсолютні шляхи, повністю виключені з обходу
|
|
75
|
+
* @returns {Promise<number>} кількість `.vue`-файлів
|
|
76
|
+
*/
|
|
77
|
+
export async function countVueFiles(absPkgDir, ignorePaths) {
|
|
78
|
+
let count = 0
|
|
79
|
+
await walkDir(
|
|
80
|
+
absPkgDir,
|
|
81
|
+
absPath => {
|
|
82
|
+
if (absPath.endsWith('.vue')) count += 1
|
|
83
|
+
},
|
|
84
|
+
ignorePaths
|
|
85
|
+
)
|
|
86
|
+
return count
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Чи має пакет "стандартний" build — розпізнаваний `vite.config.{js,ts,mjs}` у корені пакета.
|
|
91
|
+
* Канонічний `.storybook/main.js` спирається саме на цей файл (`viteFinal` мерджить його
|
|
92
|
+
* плагіни) — без нього автоматичний скафолд неможливий, і пакет пропускається мовчки
|
|
93
|
+
* (ADR Кластер 1: "skip пакетів із нестандартним build").
|
|
94
|
+
* @param {string} absPkgDir абсолютний шлях кореня пакета
|
|
95
|
+
* @returns {boolean} true, якщо знайдено відомий `vite.config.*`
|
|
96
|
+
*/
|
|
97
|
+
export function hasStandardBuild(absPkgDir) {
|
|
98
|
+
return VITE_CONFIG_FILES.some(f => existsSync(join(absPkgDir, f)))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Чи є пакет app-проєктом (не бібліотекою) для хвилі 2: `vue` у `dependencies` (не лише
|
|
103
|
+
* `peerDependencies`) і не бібліотека компонентів. Реалізовано зараз (щоб не переписувати
|
|
104
|
+
* модуль пізніше), але результат впливає на скоуп лише за прапорця `storybook.detectApps`.
|
|
105
|
+
* @param {{ dependencies?: Record<string, unknown>, peerDependencies?: Record<string, unknown> }} pkg розпарсений package.json
|
|
106
|
+
* @returns {boolean} true — app-проєкт (кандидат хвилі 2)
|
|
107
|
+
*/
|
|
108
|
+
export function isVueAppPkg(pkg) {
|
|
109
|
+
return Boolean(pkg?.dependencies?.vue) && !isVueComponentLibraryPkg(pkg)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Перевіряє один workspace-корінь на відповідність предикату скоупу (бібліотека чи app) і,
|
|
114
|
+
* за успіху, повертає його `InScopePackage`-запис.
|
|
115
|
+
* @param {string} rootDir відносний корінь пакета
|
|
116
|
+
* @param {string} cwd абсолютний корінь репозиторію
|
|
117
|
+
* @param {(pkg: Record<string, unknown>) => boolean} matches предикат скоупу (бібліотека/app)
|
|
118
|
+
* @param {string[]} ignorePaths абсолютні шляхи, повністю виключені з обходу
|
|
119
|
+
* @returns {Promise<InScopePackage|null>} запис пакета або `null`, якщо поза скоупом
|
|
120
|
+
*/
|
|
121
|
+
async function evaluateCandidate(rootDir, cwd, matches, ignorePaths) {
|
|
122
|
+
const absDir = rootDir === '.' ? cwd : join(cwd, rootDir)
|
|
123
|
+
const pkgPath = join(absDir, 'package.json')
|
|
124
|
+
if (!existsSync(pkgPath)) return null
|
|
125
|
+
let pkg
|
|
126
|
+
try {
|
|
127
|
+
pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
if (!matches(pkg)) return null
|
|
132
|
+
if (!hasStandardBuild(absDir)) return null
|
|
133
|
+
const vueFileCount = await countVueFiles(absDir, ignorePaths)
|
|
134
|
+
if (vueFileCount < VUE_FILE_THRESHOLD) return null
|
|
135
|
+
return { rootDir, absDir, pkg, vueFileCount }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Збирає workspace-пакети у скоупі канону Storybook хвилі 1: Vue-компонентна бібліотека
|
|
140
|
+
* (`vue` у `peerDependencies`, маркер `isVueComponentLibraryPkg` — той самий, що й `vue.mdc`)
|
|
141
|
+
* з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`, зі
|
|
142
|
+
* стандартним build (`vite.config.*`). Хвиля 2 (app-проєкти) додається лише за явного
|
|
143
|
+
* прапорця `storybook.detectApps` у `.n-rules.json`.
|
|
144
|
+
* @param {string} cwd абсолютний корінь репозиторію
|
|
145
|
+
* @returns {Promise<InScopePackage[]>} пакети у скоупі
|
|
146
|
+
*/
|
|
147
|
+
export async function collectInScopeVuePackages(cwd) {
|
|
148
|
+
const roots = await getMonorepoPackageRootDirs(cwd)
|
|
149
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
150
|
+
const optOut = new Set(await readStorybookOptOut(cwd))
|
|
151
|
+
const candidateRoots = roots.filter(r => !optOut.has(r))
|
|
152
|
+
|
|
153
|
+
/** @type {InScopePackage[]} */
|
|
154
|
+
const result = []
|
|
155
|
+
for (const rootDir of candidateRoots) {
|
|
156
|
+
const found = await evaluateCandidate(rootDir, cwd, isVueComponentLibraryPkg, ignorePaths)
|
|
157
|
+
if (found) result.push(found)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (await readDetectAppsFlag(cwd)) {
|
|
161
|
+
for (const rootDir of candidateRoots) {
|
|
162
|
+
if (result.some(r => r.rootDir === rootDir)) continue
|
|
163
|
+
const absDir = rootDir === '.' ? cwd : join(cwd, rootDir)
|
|
164
|
+
if (!existsSync(join(absDir, 'src/pages'))) continue
|
|
165
|
+
const found = await evaluateCandidate(rootDir, cwd, isVueAppPkg, ignorePaths)
|
|
166
|
+
if (found) result.push(found)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Self-check конфігурації: `.n-rules.json` → `storybook.optOut` не має посилатись на
|
|
175
|
+
* неіснуючі workspace-пакети (застаріле налаштування — пакет перейменували/видалили, а
|
|
176
|
+
* opt-out лишився). Сама детекція скоупу (поріг, build, app-проєкти) — pure-функції вище,
|
|
177
|
+
* покриті тестами напряму; тут лише конфіг-гігієна.
|
|
178
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
|
|
179
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
|
|
180
|
+
*/
|
|
181
|
+
export async function lint(ctx) {
|
|
182
|
+
const reporter = createViolationReporter(ctx)
|
|
183
|
+
const cwd = ctx.cwd
|
|
184
|
+
|
|
185
|
+
const optOut = await readStorybookOptOut(cwd)
|
|
186
|
+
if (optOut.length === 0) {
|
|
187
|
+
reporter.pass('storybook: storybook.optOut порожній або не заданий')
|
|
188
|
+
return reporter.result()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const roots = new Set(await getMonorepoPackageRootDirs(cwd))
|
|
192
|
+
for (const rootDir of optOut) {
|
|
193
|
+
if (!roots.has(rootDir)) {
|
|
194
|
+
reporter.fail(
|
|
195
|
+
`.n-rules.json storybook.optOut містить '${rootDir}' — такого workspace-пакета немає (застаріле opt-out, storybook.mdc)`,
|
|
196
|
+
'stale-opt-out'
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return reporter.result()
|
|
202
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: fix-vitest-config.mjs
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/vitest-config/fix-vitest-config.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 35b10da8
|
|
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
|
+
Файл детерміновано приводить `vitest.config.mjs` у Vue-компонентних пакетах із Storybook до канону `test.projects`: окремі проєкти `unit` і `storybook`, а browser-mode обмежує лише `chromium`. Для Stryker він генерує ізольований `vitest.stryker.config.*` у межах того ж пакета, згідно з підходом з `ADR Кластер 5`.
|
|
16
|
+
|
|
17
|
+
Для вже наявного `vitest.config.mjs` зміни вносяться точковими `string-splice`-ами лише на вставку, щоб не переписувати решту форматування й коментарів; після цього конфіг повторно парситься, і зміни відкочуються, якщо результат невалідний. Логіка побудови покладена на `buildFreshVitestConfig`, `buildStrykerConfig` і `patterns`, а читання й аналіз виконуються через `oxc-parser`. Детектор `main.mjs` працює read-only, тоді як запис зосереджений у `fix-stryker_config.mjs`.
|
|
18
|
+
|
|
19
|
+
## Поведінка
|
|
20
|
+
|
|
21
|
+
- `buildFreshVitestConfig` — створює новий `vitest.config.mjs` для пакета з канонічним `test.projects` для `unit` і `storybook`, підставляє шлях до локального Vite-конфіга та сторінковий glob для Storybook.
|
|
22
|
+
- `buildStrykerConfig` — генерує ізольований `vitest.stryker.config.*` на основі canonical baseline-шаблону для того самого пакета, підставляючи шлях до локального Vite-конфіга.
|
|
23
|
+
- `patterns` — описує autofix-сценарій, який реагує на відсутній або неканонічний vitest-конфіг, доповнює наявний файл без переписування зайвого форматування та створює `vitest.stryker.config.*` у відповідному package root.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- buildFreshVitestConfig — Генерує повністю новий `vitest.config.mjs` (unit+storybook projects) для
|
|
28
|
+
пакета без жодного наявного vitest-конфіга. Експортовано — переюз у
|
|
29
|
+
`adopt/main.mjs` (генерація лише для повністю відсутніх файлів).
|
|
30
|
+
- buildStrykerConfig — Генерує ізольований `vitest.stryker.config.*` (той самий basename/ext що
|
|
31
|
+
й основний vitest-конфіг пакета) з canonical baseline-шаблону. Експортовано —
|
|
32
|
+
переюз у `adopt/main.mjs`.
|
|
33
|
+
- patterns — повертає набір шаблонів, які використовуються для зіставлення й обробки відповідних випадків у коді
|
|
34
|
+
|
|
35
|
+
## Гарантії поведінки
|
|
36
|
+
|
|
37
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: plugins/lang-js/rules/storybook/vitest-config
|
|
4
|
+
resource: plugins/lang-js/rules/storybook/vitest-config/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| --------------------------------------------- | --------- |
|
|
9
|
+
| [fix-vitest-config.mjs](fix-vitest-config.md) | JS Module |
|
|
10
|
+
| [main.mjs](main.md) | JS Module |
|