@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,112 @@
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 { collectInScopeVuePackages } from '../scope/main.mjs'
8
+ import { missingMarkers } from '../scaffold/main.mjs'
9
+
10
+ /** Repo-relative шлях канонічного composite action (не per-package — один на репозиторій). */
11
+ export const PLAYWRIGHT_ACTION_REL = '.github/actions/setup-playwright-chromium/action.yml'
12
+
13
+ /** Repo-relative шлях канонічного workflow, що запускає `vitest --project=storybook`. */
14
+ export const STORYBOOK_WORKFLOW_REL = '.github/workflows/lint-storybook.yml'
15
+
16
+ /**
17
+ * Маркери канону composite action `setup-playwright-chromium` (ADR Кластер 5): кеш
18
+ * `ms-playwright` через `actions/cache`, ключ від версії playwright, install лише chromium.
19
+ * Текстовий пошук — той самий підхід, що й `MAIN_JS_MARKERS`/`PREVIEW_JS_MARKERS` у `scaffold`.
20
+ */
21
+ export const PLAYWRIGHT_ACTION_MARKERS = [
22
+ { token: 'ms-playwright', hint: 'кеш каталогу ms-playwright' },
23
+ { token: 'actions/cache@', hint: 'actions/cache для Playwright-браузерів' },
24
+ { token: 'playwright install chromium', hint: 'install лише chromium (не всі браузери)' }
25
+ ]
26
+
27
+ /**
28
+ * Маркери канону `.github/workflows/lint-storybook.yml`: композитний Playwright-кеш ПІСЛЯ
29
+ * setup-bun-deps, і швидкий `vitest --project=storybook` (ADR Кластер 5 — nightly-only
30
+ * `@7n/test coverage`/mutation-testing на PR не запускається, лише цей швидкий шлях).
31
+ */
32
+ export const STORYBOOK_WORKFLOW_MARKERS = [
33
+ { token: './.github/actions/setup-bun-deps', hint: 'setup-bun-deps перед Playwright-кроком' },
34
+ { token: './.github/actions/setup-playwright-chromium', hint: 'композитний Playwright-кеш' },
35
+ { token: '--project=storybook', hint: 'швидкий прогін лише storybook-проєкту (не повний coverage)' }
36
+ ]
37
+
38
+ /**
39
+ * Перевіряє один репо-рівневий канонічний файл (composite action чи workflow): відсутність →
40
+ * `missingReason`-порушення з посиланням на `npx \@7n/rules fix storybook`; присутність без
41
+ * якогось маркера → `markerReason`-порушення на конкретний маркер. Той самий патерн, що й
42
+ * `checkCanonFile` у `scaffold/main.mjs`, але без per-package `rootDir` — файл один на репо.
43
+ * @param {string} cwd абсолютний корінь репозиторію
44
+ * @param {string} relFile posix-relative шлях файлу від кореня репозиторію
45
+ * @param {{ token: string, hint: string }[]} markers канонічні маркери файлу
46
+ * @param {string} missingReason reason для порушення "файл відсутній"
47
+ * @param {string} markerReason reason для порушення "маркер відсутній"
48
+ * @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
49
+ * @returns {Promise<void>}
50
+ */
51
+ async function checkRepoCanonFile(cwd, relFile, markers, missingReason, markerReason, reporter) {
52
+ const abs = join(cwd, relFile)
53
+ if (existsSync(abs)) {
54
+ const content = await readFile(abs, 'utf8')
55
+ for (const m of missingMarkers(content, markers)) {
56
+ reporter.fail(`${relFile} не відповідає канону — бракує: ${m.hint} (storybook.mdc, ADR Кластер 5)`, {
57
+ reason: markerReason,
58
+ file: relFile
59
+ })
60
+ }
61
+ return
62
+ }
63
+ reporter.fail(
64
+ `Відсутній ${relFile} — канонічний Playwright-кеш для vitest storybook-проєкту: npx @7n/rules fix storybook (storybook.mdc, ADR Кластер 5)`,
65
+ { reason: missingReason, file: relFile }
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Detector concern-а `storybook/ci` (ADR Кластер 5, CI-частина): для репозиторіїв з бодай
71
+ * одним Vue component library пакетом у скоупі Storybook (`collectInScopeVuePackages`) —
72
+ * канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише
73
+ * chromium) і канонічний `.github/workflows/lint-storybook.yml`, що запускає швидкий
74
+ * `vitest --project=storybook` на PR. Гейтований `requires.capability: ci:github` — спить
75
+ * у репозиторіях без плагіна `@7n/rules-ci-github` (немає `.github/workflows`).
76
+ *
77
+ * Nightly-only `@7n/test coverage` (mutation testing) — поза обсягом цього concern-а: ADR
78
+ * Кластер 5 явно розділяє швидкий PR-шлях (цей concern) і nightly mutation-прогін, який
79
+ * лишається окремою інфраструктурою `test/stryker_config`.
80
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
81
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
82
+ */
83
+ export async function lint(ctx) {
84
+ const reporter = createViolationReporter(ctx)
85
+ const cwd = ctx.cwd
86
+
87
+ const pkgs = await collectInScopeVuePackages(cwd)
88
+ if (pkgs.length === 0) {
89
+ reporter.pass('storybook/ci: немає Vue component library пакетів у скоупі (storybook.mdc)')
90
+ return reporter.result()
91
+ }
92
+
93
+ await checkRepoCanonFile(
94
+ cwd,
95
+ PLAYWRIGHT_ACTION_REL,
96
+ PLAYWRIGHT_ACTION_MARKERS,
97
+ 'missing-playwright-action',
98
+ 'playwright-action-marker-missing',
99
+ reporter
100
+ )
101
+
102
+ await checkRepoCanonFile(
103
+ cwd,
104
+ STORYBOOK_WORKFLOW_REL,
105
+ STORYBOOK_WORKFLOW_MARKERS,
106
+ 'missing-storybook-workflow',
107
+ 'storybook-workflow-marker-missing',
108
+ reporter
109
+ )
110
+
111
+ return reporter.result()
112
+ }
@@ -0,0 +1,41 @@
1
+ name: Lint Storybook
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ paths:
9
+ - '**/*.stories.@(js|ts)'
10
+ - '.storybook/**'
11
+ pull_request:
12
+ branches:
13
+ - dev
14
+ - main
15
+
16
+ concurrency:
17
+ group: ${{ github.ref }}-${{ github.workflow }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ storybook-test:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ package:
29
+ __STORYBOOK_CI_PACKAGE_DIRS__
30
+ steps:
31
+ - uses: actions/checkout@v6
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - uses: ./.github/actions/setup-bun-deps
36
+
37
+ - uses: ./.github/actions/setup-playwright-chromium
38
+
39
+ - name: Storybook vitest project (${{ matrix.package }})
40
+ working-directory: ${{ matrix.package }}
41
+ run: bunx vitest run --project=storybook
@@ -0,0 +1,34 @@
1
+ # yaml-language-server: $schema=https://json.schemastore.org/github-action.json
2
+
3
+ name: Setup Playwright Chromium
4
+ description: >-
5
+ Кеш Playwright-браузерів (лише chromium, storybook.mdc ADR Кластер 5) для vitest
6
+ browser-mode storybook-проєкту. Викликати ПІСЛЯ ./.github/actions/setup-bun-deps
7
+ (потрібен node_modules/bun.lock для визначення версії).
8
+
9
+ runs:
10
+ using: composite
11
+ steps:
12
+ - name: Resolve Playwright version (bun.lock)
13
+ id: playwright-version
14
+ shell: bash
15
+ run: |
16
+ VERSION=$(grep -oE '"playwright(-core)?@[0-9]+\.[0-9]+\.[0-9]+"' bun.lock | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
17
+ if [ -z "$VERSION" ]; then
18
+ VERSION=$(node -p "require('playwright-core/package.json').version" 2>/dev/null || echo unknown)
19
+ fi
20
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
21
+
22
+ - name: Cache Playwright browsers (chromium)
23
+ id: playwright-cache
24
+ uses: actions/cache@v5
25
+ with:
26
+ path: |
27
+ ~/.cache/ms-playwright
28
+ ~/Library/Caches/ms-playwright
29
+ key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
30
+
31
+ - name: Install Playwright chromium (лише chromium, storybook.mdc)
32
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
33
+ shell: bash
34
+ run: bunx playwright install chromium --with-deps
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "lint": { "scope": "full", "glob": ["package.json", "**/*.vue", ".storybook/**"] }
4
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/lang-js/rules/storybook/hygiene
4
+ resource: plugins/lang-js/rules/storybook/hygiene/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------- | --------- |
9
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,45 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/storybook/hygiene/main.mjs
5
+ docgen:
6
+ crc: 9e36e1a2
7
+ model: openai-codex/gpt-5.5
8
+ tier: cloud-avg
9
+ score: 85
10
+ issues: internal-name:collectInScopeVuePackages,anchor-miss:(storybook.mdc),judge:inaccurate:0.99
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Файл описує read-only lint-перевірку `lint` для Storybook-правил, що спираються на `package.json`. Перевірка потрібна, щоб виявляти порушення конфігурації без змін у файловій системі чи БД; помилки перехоплюються fail-safe і не виходять назовні як винятки.
17
+
18
+ ## Поведінка
19
+
20
+ 1. `lint` визначає Vue component library пакети, для яких діє Storybook-канон. Якщо таких пакетів немає, повертає успішний результат із повідомленням.
21
+
22
+ 2. Для кожного знайденого пакета читає його `package.json` і формує перелік дозволених third-party залежностей із `dependencies` та `peerDependencies`.
23
+
24
+ 3. Переглядає `.vue` файли пакета з урахуванням ignore-конфігурації та знаходить імпорти сторонніх пакетів. Відносні шляхи, alias-шляхи, Node builtin модулі та auto-import глобали не вважаються порушеннями.
25
+
26
+ 4. Якщо `.vue` файл імпортує сторонній пакет, якого немає в `package.json` поточного пакета, додає порушення `undeclared-import`. Для одного файлу один і той самий пакет повідомляється лише один раз.
27
+
28
+ 5. Перевіряє, чи має пакет глобальні Quasar SCSS-змінні. Якщо вони є, але `.storybook/main.js` не вмикає їх для Storybook, додає попередження `missing-sass-variables`.
29
+
30
+ 6. Не змінює файлову систему або зовнішній стан: перевірка працює read-only і лише повертає результат лінту.
31
+
32
+ 7. Некоректні або нерозбірні файли не зупиняють перевірку назовні: такі випадки обробляються fail-safe, щоб лінт міг продовжити роботу.
33
+
34
+ ## Публічний API
35
+
36
+ - lint — Detector concern-а `storybook/hygiene`: для кожного Vue component library пакета у скоупі
37
+ канону Storybook (`collectInScopeVuePackages`) — undeclared third-party imports у `.vue` та
38
+ auto-detect глобальних Quasar SCSS-змінних без `sassVariables` у `.storybook/main.js`
39
+ (storybook.mdc, ADR Кластер 6). Breaking-change guard при мажорному апгрейді
40
+ third-party-пакетів свідомо не автоматизується — людський пункт, hygiene.mdc.
41
+
42
+ ## Гарантії поведінки
43
+
44
+ - Read-only: не виконує операцій запису (ФС/БД).
45
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -0,0 +1,254 @@
1
+ /** @see ./docs/main.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join, relative } from 'node:path'
5
+
6
+ import { parseSync } from 'oxc-parser'
7
+
8
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
9
+ import { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
10
+ import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
11
+ import {
12
+ dynamicImportModule,
13
+ langFromPath,
14
+ requireCallModule,
15
+ walkAstWithAncestors
16
+ } from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
17
+ import { contentForVueImportScan } from '@7n/rules/scripts/lib/js-source-signals.mjs'
18
+ import { isNodeBuiltinSpecifier } from '../../vue/lib/vue-forbidden-imports.mjs'
19
+ import { collectInScopeVuePackages } from '../scope/main.mjs'
20
+
21
+ const VUE_EXT_RE = /\.vue$/u
22
+
23
+ // Quasar CLI-конвенція за замовчуванням (quasar.dev/style/sass-scss-variables): плагін шукає
24
+ // саме цей файл, якщо `sassVariables` не задає власний шлях — .scss першим, .sass fallback-ом.
25
+ const SASS_VARIABLES_CANDIDATES = ['src/css/quasar.variables.scss', 'src/css/quasar.variables.sass']
26
+
27
+ // quasar({ sassVariables: true }) або quasar({ sassVariables: 'шлях' }) — обидві форми вмикають
28
+ // підключення SCSS-змінних; boolean false/відсутність поля — ні.
29
+ const SASS_VARIABLES_MARKER_RE = /sassVariables\s*:\s*(?:true|['"])/u
30
+
31
+ /**
32
+ * Віртуальний шлях для oxc-парсера: `.vue` розбирається як `.ts` (після витягу `<script>`-блоку),
33
+ * решта — за власним розширенням.
34
+ * @param {string} relPath шлях файлу (posix, відносно пакета)
35
+ * @returns {string} шлях для вибору `lang` парсером
36
+ */
37
+ function virtualPathForParse(relPath) {
38
+ return relPath.endsWith('.vue') ? relPath.replace(VUE_EXT_RE, '.ts') : relPath
39
+ }
40
+
41
+ /**
42
+ * Витягає import-specifier'и з `.vue` SFC (лише `<script>`-блоки) чи звичайного JS/TS-файлу:
43
+ * static import + dynamic `import()` + `require()`. Той самий oxc-parser pipeline, що й
44
+ * `js/dep-policy` і `vue/lib/vue-forbidden-imports.mjs` — лише для довільного specifier-а,
45
+ * не для конкретного заборонного списку.
46
+ * @param {string} content сирий вміст файлу
47
+ * @param {string} relPath шлях файлу (для вибору мови/віртуального шляху парсера)
48
+ * @returns {string[]} список import-specifier'ів (можуть повторюватись)
49
+ */
50
+ function extractImportSpecifiers(content, relPath) {
51
+ const scan = contentForVueImportScan(content, relPath)
52
+ const virtualPath = virtualPathForParse(relPath)
53
+ let parsed
54
+ try {
55
+ parsed = parseSync(virtualPath, scan, { lang: langFromPath(virtualPath), sourceType: 'module' })
56
+ } catch {
57
+ return []
58
+ }
59
+ if (parsed.errors?.length) return []
60
+
61
+ const out = []
62
+ for (const imp of parsed.module?.staticImports ?? []) {
63
+ if (typeof imp?.moduleRequest?.value === 'string') out.push(imp.moduleRequest.value)
64
+ }
65
+ const program = parsed.program
66
+ if (program && typeof program === 'object') {
67
+ walkAstWithAncestors(program, [], node => {
68
+ const dyn = dynamicImportModule(node)
69
+ if (dyn !== null) out.push(dyn)
70
+ const req = requireCallModule(node)
71
+ if (req !== null) out.push(req)
72
+ })
73
+ }
74
+ return out
75
+ }
76
+
77
+ /**
78
+ * Чи є specifier відносним імпортом чи псевдонімом шляху (не сторонній пакет): `./x`, `../x`,
79
+ * абсолютний шлях, чи типові Vite-аліаси `@/...`/`~/...` на `src/`. Автоімпорт-глобали
80
+ * (`ref`, `computed`, Quasar-композаблі через `unplugin-auto-import`) сюди не потрапляють —
81
+ * вони не є import-специфікаторами взагалі, AST їх не бачить.
82
+ * @param {string} spec значення `moduleRequest.value`
83
+ * @returns {boolean} `true`, якщо це не сторонній пакет
84
+ */
85
+ function isRelativeOrAliasSpecifier(spec) {
86
+ return spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('~/') || spec.startsWith('@/')
87
+ }
88
+
89
+ /**
90
+ * Ім'я пакета верхнього рівня зі specifier-а: враховує scoped-пакети (`@scope/name`) і
91
+ * subpath-імпорти (`pkg/sub/path` → `pkg`, `@scope/name/sub` → `@scope/name`).
92
+ * @param {string} spec сторонній import-specifier
93
+ * @returns {string} ім'я пакета для звірки з package.json deps
94
+ */
95
+ function topLevelPackageName(spec) {
96
+ if (spec.startsWith('@')) {
97
+ const parts = spec.split('/')
98
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec
99
+ }
100
+ const idx = spec.indexOf('/')
101
+ return idx === -1 ? spec : spec.slice(0, idx)
102
+ }
103
+
104
+ /**
105
+ * Множина задекларованих пакетів (`dependencies` + `peerDependencies`) — workspace-пакети
106
+ * (`@nitra/*`, `@7n/*`) не потребують окремої обробки: вони так само оголошуються тут
107
+ * (workspace-протокол), як і звичайні npm-залежності.
108
+ * @param {Record<string, unknown>} pkg розпарсений package.json пакета
109
+ * @returns {Set<string>} імена задекларованих пакетів
110
+ */
111
+ function collectDeclaredDeps(pkg) {
112
+ const names = new Set()
113
+ for (const field of ['dependencies', 'peerDependencies']) {
114
+ const obj = pkg?.[field]
115
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
116
+ for (const name of Object.keys(obj)) names.add(name)
117
+ }
118
+ }
119
+ return names
120
+ }
121
+
122
+ /**
123
+ * Збирає абсолютні шляхи всіх `.vue`-файлів у дереві пакета.
124
+ * @param {string} absDir абсолютний шлях кореня пакета
125
+ * @param {string[]} ignorePaths абсолютні шляхи, повністю виключені з обходу
126
+ * @returns {Promise<string[]>} відсортовані абсолютні шляхи `.vue`-файлів
127
+ */
128
+ async function collectVueFiles(absDir, ignorePaths) {
129
+ const files = []
130
+ await walkDir(
131
+ absDir,
132
+ p => {
133
+ if (p.endsWith('.vue')) files.push(p)
134
+ },
135
+ ignorePaths
136
+ )
137
+ return files
138
+ }
139
+
140
+ /**
141
+ * Будує posix-relative шлях від `cwd` для violation.file — `entry.rootDir` уже relative до `cwd`
142
+ * (`.` для кореня монорепо), `relFromPkg` — relative до `entry.absDir`.
143
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
144
+ * @param {string} relFromPkg posix-relative шлях від кореня пакета
145
+ * @returns {string} posix-relative шлях від `cwd`
146
+ */
147
+ function fileRelFromCwd(entry, relFromPkg) {
148
+ return entry.rootDir === '.' ? relFromPkg : `${entry.rootDir}/${relFromPkg}`
149
+ }
150
+
151
+ /**
152
+ * Перевіряє один пакет на undeclared third-party imports у `.vue`-файлах: import стороннього
153
+ * пакета, якого немає в `dependencies`/`peerDependencies` package.json цього ж пакета (реальний
154
+ * кейс ADR — зламаний default-export `@vuepic/vue-datepicker` v14, silent breakage без цієї
155
+ * перевірки). Відносні імпорти, аліаси (`@/`, `~/`), Node-builtin і auto-import глобали
156
+ * пропускаються.
157
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
158
+ * @param {string[]} ignorePaths абсолютні шляхи, виключені з обходу
159
+ * @param {ReturnType<typeof createViolationReporter>} reporter репортер порушень
160
+ * @returns {Promise<void>}
161
+ */
162
+ async function checkUndeclaredImportsForPackage(entry, ignorePaths, reporter) {
163
+ const declared = collectDeclaredDeps(entry.pkg)
164
+ const vueFiles = await collectVueFiles(entry.absDir, ignorePaths)
165
+
166
+ for (const absFile of vueFiles) {
167
+ const content = await readFile(absFile, 'utf8')
168
+ const relFromPkg = relative(entry.absDir, absFile).split('\\').join('/')
169
+ const specifiers = extractImportSpecifiers(content, relFromPkg)
170
+
171
+ const reportedForFile = new Set()
172
+ for (const spec of specifiers) {
173
+ if (isRelativeOrAliasSpecifier(spec) || isNodeBuiltinSpecifier(spec)) continue
174
+ const pkgName = topLevelPackageName(spec)
175
+ if (declared.has(pkgName) || reportedForFile.has(pkgName)) continue
176
+ reportedForFile.add(pkgName)
177
+
178
+ const fileRel = fileRelFromCwd(entry, relFromPkg)
179
+ reporter.fail(
180
+ `[undeclared-import] ${fileRel}: import '${spec}' — пакет '${pkgName}' відсутній у dependencies/peerDependencies ${entry.rootDir === '.' ? 'кореня монорепо' : entry.rootDir} (storybook.mdc hygiene)`,
181
+ {
182
+ reason: 'undeclared-import',
183
+ file: fileRel,
184
+ data: { rootDir: entry.rootDir, package: pkgName, specifier: spec }
185
+ }
186
+ )
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Чи має пакет глобальні Quasar SCSS-змінні — canonical шлях за замовчуванням
193
+ * (quasar.dev/style/sass-scss-variables): `src/css/quasar.variables.scss` (fallback `.sass`).
194
+ * @param {string} absDir абсолютний шлях кореня пакета
195
+ * @returns {boolean} `true`, якщо знайдено файл глобальних SCSS-змінних
196
+ */
197
+ function hasGlobalSassVariables(absDir) {
198
+ return SASS_VARIABLES_CANDIDATES.some(f => existsSync(join(absDir, f)))
199
+ }
200
+
201
+ /**
202
+ * Перевіряє один пакет на auto-detect глобальних SCSS-змінних: якщо в пакеті є
203
+ * `quasar.variables.scss`/`.sass`, а `.storybook/main.js` не вмикає `sassVariables` у
204
+ * `quasar()`-плагіні, глобальні SCSS-змінні недоступні у Storybook (тихий розсинхрон зі
205
+ * звичайним build). Рівень — `warn` (м'який сигнал, не гейт хвилі 1, аналогічно
206
+ * `onUnhandledRequest` у `preview.js`). Відсутність самого `.storybook/main.js` вже покриває
207
+ * `storybook/scaffold` — тут не дублюється.
208
+ * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
209
+ * @param {ReturnType<typeof createViolationReporter>} reporter репортер порушень
210
+ * @returns {Promise<void>}
211
+ */
212
+ async function checkSassVariablesForPackage(entry, reporter) {
213
+ if (!hasGlobalSassVariables(entry.absDir)) return
214
+
215
+ const mainJsPath = join(entry.absDir, '.storybook/main.js')
216
+ if (!existsSync(mainJsPath)) return
217
+
218
+ const content = await readFile(mainJsPath, 'utf8')
219
+ if (SASS_VARIABLES_MARKER_RE.test(content)) return
220
+
221
+ const fileRel = fileRelFromCwd(entry, '.storybook/main.js')
222
+ reporter.fail(
223
+ `[sass-variables] ${fileRel}: пакет має глобальні Quasar SCSS-змінні (${SASS_VARIABLES_CANDIDATES.join(' | ')}), але quasar({ sassVariables }) не задано в .storybook/main.js (storybook.mdc hygiene)`,
224
+ { reason: 'missing-sass-variables', file: fileRel, severity: 'warn', data: { rootDir: entry.rootDir } }
225
+ )
226
+ }
227
+
228
+ /**
229
+ * Detector concern-а `storybook/hygiene`: для кожного Vue component library пакета у скоупі
230
+ * канону Storybook (`collectInScopeVuePackages`) — undeclared third-party imports у `.vue` та
231
+ * auto-detect глобальних Quasar SCSS-змінних без `sassVariables` у `.storybook/main.js`
232
+ * (storybook.mdc, ADR Кластер 6). Breaking-change guard при мажорному апгрейді
233
+ * third-party-пакетів свідомо не автоматизується — людський пункт, hygiene.mdc.
234
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
235
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
236
+ */
237
+ export async function lint(ctx) {
238
+ const reporter = createViolationReporter(ctx)
239
+ const cwd = ctx.cwd
240
+
241
+ const pkgs = await collectInScopeVuePackages(cwd)
242
+ if (pkgs.length === 0) {
243
+ reporter.pass('storybook hygiene: немає Vue component library пакетів у скоупі (storybook.mdc)')
244
+ return reporter.result()
245
+ }
246
+
247
+ const ignorePaths = await loadCursorIgnorePaths(cwd)
248
+ for (const entry of pkgs) {
249
+ await checkUndeclaredImportsForPackage(entry, ignorePaths, reporter)
250
+ await checkSassVariablesForPackage(entry, reporter)
251
+ }
252
+
253
+ return reporter.result()
254
+ }
@@ -0,0 +1 @@
1
+ { "auto": { "glob": ["package.json", "**/*.vue", ".storybook/**"] } }
@@ -0,0 +1,66 @@
1
+ ---
2
+ description: Канонічний Storybook для Vue-компонентних бібліотек — скафолд, vitest/Stryker, гігієна залежностей, рецепти мокання
3
+ version: '1.0'
4
+ globs: ["package.json", "**/*.vue", ".storybook/**"]
5
+ alwaysApply: false
6
+ ---
7
+
8
+ # Канон Storybook для Vue-компонентних бібліотек — хвиля 1
9
+
10
+ Джерело рішення: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`. Storybook впроваджувався вручну двічі в різних nitra-репо в одній сесії — обидва рази з тими самими невидимими заздалегідь проблемами (конфлікт `@vitejs/plugin-vue`/`quasar()`, недоступні внутрішні Quasar-іконки без `iconSet`+`iconMapFn`, ручне мокання мережі) і одним реальним merge-конфліктом між двома паралельними ручними реалізаціями тієї самої фічі. Це правило робить канонічний скафолд стандартом, а не одноразовим рішенням кожного агента.
11
+
12
+ **Хвильовий rollout:** `alwaysApply: false` — правило не гейтить CI, доки не ввімкнене явно в `.n-rules.json` консюмер-репо (`rules: ["storybook"]`). Хвиля 1 (це правило) покриває детекцію скоупу, канонічний скафолд `.storybook/`, vitest/Stryker-конфіг (Кластер 5), гігієну сторонніх залежностей (Кластер 6), рецепти мокання (docs-only, Кластер 3) і `--adopt`-режим rollout-у для вже впроваджених вручну пакетів (Кластер 8); **LLM-генерація args для stories — хвиля 2**, свідомо відкладена (ADR: ризик одночасного org-wide CI-блоку на необкатаному generation pipeline, на відміну від production-proven doc-files).
13
+
14
+ Усі concern-и хвилі 1 (`scope`/`scaffold`/`vitest-config`/`hygiene`) мають `lint`-поверхню в `concern.json` і виконуються під `npx @7n/rules lint storybook` — `scope`/`scaffold`/`vitest-config` раніше декларували лише `check: true` без `lint`-блоку і тому мовчки не підхоплювались unified lint-рушієм (`run-detectors.mjs` виконує лише concern-и з явним `lint` чи `policy` блоком); виправлено разом із введенням `adopt`-режиму.
15
+
16
+ ## Скоуп
17
+
18
+ Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати. Пакети без розпізнаваного `vite.config.{js,ts,mjs}` (нестандартний build) пропускаються мовчки — автоматичний скафолд спирається на `viteConfigPath` пакета.
19
+
20
+ Опційно (вимкнено за замовчуванням) — app-проєкти (`vue` у `dependencies`, не бібліотека, + `src/pages/`). Детекція реалізована в `scope/main.mjs`, але не викликається доки в `.n-rules.json` не задано прапорець хвилі 2 — рішення розширювати скоуп на app-проєкти залишається відкритим питанням ADR.
21
+
22
+ **Opt-out:** окремий пакет можна виключити зі скоупу через `.n-rules.json` → `storybook.optOut: string[]` (root dir пакета, той самий формат що й у виводі workspace-роутингу — `.` для кореня, `packages/ui` тощо). Виняток — намір, не помилка: якщо в `optOut` вказано неіснуючий workspace-пакет, `scope`-концерн репортує це як застаріле налаштування.
23
+
24
+ Логіка детекції (поріг, opt-out, нестандартний build, app-проєкти) — у `scope/main.mjs`, не дублюється тут.
25
+
26
+ ## Канонічний скафолд
27
+
28
+ Для кожного пакета в скоупі обов'язкові: `.storybook/main.js`, `.storybook/preview.js`, `package.json#scripts.storybook`. Канонічні шаблони — `scaffold/template/`.
29
+
30
+ Фіксовані рішення (деталі — ADR, Кластер 2):
31
+
32
+ - **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
33
+ - **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
34
+ - **`viteFinal`** зчитує `vite.config` самого пакета (той самий, що й для звичайного білду) і мерджить його плагіни в конфіг Storybook, **знімаючи** роутинг-плагіни файлової маршрутизації додатка-споживача (`vite-plugin-pages`, `unplugin-vue-router`, `vite-plugin-vue-layouts`, `vite-plugin-vue-layouts-next`) — вони не мають сенсу в ізольованому рендері компонента. Плагіни `plugins`-масиву можуть бути `Promise`/вкладеними масивами (`VueMacros({ plugins: { vue: Vue() } })` — реальний стек пілотного консюмера повертає `Promise`, що резолвиться в масив плагінів) — фільтр спершу resolve/flatten-ить їх, інакше порівняння імені з `Promise`, що ще не резолвився, мовчки пропускає дублікат `@vitejs/plugin-vue`-трансформу (подвійна SFC-трансформація). Vue-трансформери фільтруються за сімейством імені (`vite:vue`-префікс або підрядок `vue-macros`), не лише буквальним `@vitejs/plugin-vue` — деталі й обґрунтування коментарем у `scaffold/template/main.js`.
35
+ - **`staticDirs`** покриває `.storybook/public` — статичний asset для msw service worker (`preview.js`).
36
+ - **`preview.js`**: повний `Quasar`-install (не тільки окремі компоненти) + `iconSet`+`iconMapFn`-комбо — без цієї пари внутрішні Quasar-компоненти (напр. стрілка `QSelect`) не резолвлять вбудовані іконки поза full CLI build. `msw-storybook-addon` ініціалізується з `onUnhandledRequest`-фільтром: **same-origin GET мовчки пропускається** (Vite HMR/asset-шум), решта — попередження (не білд-помилка — навмисно м'яко для хвилі 1).
37
+ - **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
38
+ - **`package.json#scripts.storybook`** — уніфікований скрипт, однаковий для всіх пакетів у скоупі (значення — `scaffold/main.mjs`, не дублюється тут).
39
+
40
+ Перевірка присутності й ключових маркерів канону — `scaffold/main.mjs`; детерміноване відтворення відсутніх файлів із `scaffold/template/` — `scaffold/fix-scaffold.mjs` (fixability: `config` — канонічна форма одна, LLM у ланцюжку фіксу не потрібен).
41
+
42
+ ## Vitest-конфіг і Stryker-ізоляція (Кластер 5)
43
+
44
+ Канонічний `test.projects` (`unit`+`storybook`, browser-mode лише chromium) і ізольований `vitest.stryker.config` (той самий unit-набір, без browser-mode — `@stryker-mutator/vitest-runner` крашиться на browser-mode `projects`) — `vitest-config/main.mjs` (перевірка, AST через `oxc-parser`) і `vitest-config/fix-vitest-config.mjs` (точкові insert-only правки наявного конфіга, fixability: `config`). Деталі канону, чому саме chromium і межі автофіксу — `vitest-config/vitest-config.mdc`, не дублюється тут.
45
+
46
+ ### CI: Playwright-кеш і швидкий PR-прогін (Кластер 5, CI-частина)
47
+
48
+ `ci/main.mjs` + `ci/fix-ci.mjs` (fixability: `config`, `requires.capability: ci:github` — спить у репозиторіях без плагіна `@7n/rules-ci-github`): для кожного репозиторію з бодай одним пакетом у скоупі — канонічний composite action `.github/actions/setup-playwright-chromium/action.yml` (кеш `~/.cache/ms-playwright`/`~/Library/Caches/ms-playwright` за версією playwright з `bun.lock`, install **лише** `chromium --with-deps` при cache miss) і `.github/workflows/lint-storybook.yml` (матриця `strategy.matrix.package` — фактичні пакети у скоупі, `checkout` → `setup-bun-deps` → `setup-playwright-chromium` → `vitest run --project=storybook`). Композитний action і workflow — репо-рівневі файли (не per-package), перевірка й автофікс не per-package.
49
+
50
+ Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.
51
+
52
+ ## Гігієна сторонніх залежностей (Кластер 6)
53
+
54
+ `hygiene/main.mjs`: (1) undeclared third-party imports у `.vue`-файлах пакета — import стороннього пакета, якого немає в `dependencies`/`peerDependencies` (реальний кейс ADR — зламаний default-export `@vuepic/vue-datepicker` v14, silent breakage без цієї перевірки); (2) наявність `src/css/quasar.variables.{scss,sass}` без відповідного `quasar({ sassVariables: true })` у `.storybook/main.js` — глобальні Quasar SCSS-змінні пакета інакше не резолвляться в ізольованому Storybook-рендері. Docs-only детальний виклад не потрібен — концерн самодостатній, без окремого `.mdc`.
55
+
56
+ ## Мокання (Кластер 3, docs-only)
57
+
58
+ Рецепти router/`@nitra/tfm`/Apollo-GraphQL(MSW)/Pinia/сторінкових stories — `mocking/mocking.mdc`. Свідомо без механічної перевірки (`concern.json` без `check`/`policy`/`lint`-блоку, як і решта чисто-документаційних concern-ів репозиторію) — кожен пакет мокає свій набір залежностей по-своєму, детермінований чек дав би або хибні спрацювання, або нульове покриття.
59
+
60
+ ## Adopt-режим і скіл `n-storybook` (Кластер 8)
61
+
62
+ Скіл `npm/skills/storybook/` (`.cursor/skills/n-storybook/` після синку) — тонка обгортка запуску: звичайний режим — `npx @7n/rules lint storybook`; `--adopt` — окремий діагностичний JS-модуль `adopt/main.mjs` для пакетів, де ВЖЕ є ручний `.storybook/`, що не збігається з каноном. Adopt діагностує diff по секціях проти `template/` (main.js/preview.js/mocks/gql-sse.js/package.json#scripts.storybook/vitest test.projects/vitest.stryker.config) — статус `match`/`differ`/`missing` на секцію, **без сліпого перезапису** розбіжних файлів; автофікс (`--fix-missing`) генерує лише секції зі статусом `missing`. Circuit breaker: збій діагностики одного пакета деградує до `status: 'broken'` для нього, решта пакетів прогону обробляються далі. Викликається напряму (`bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs`), без окремого CLI-прапорця в ядрі `n-rules.js` — деталі й приклади звіту в `SKILL.md` скіла.
63
+
64
+ ## Що свідомо поза хвилею 1
65
+
66
+ LLM-генерація `args` для stories з `defineProps`/`defineEmits`/slots (Кластер 4 ADR) — хвиля 2, свідомо відкладена. Governance-винятки в `n-npm-module.mdc`/`n-bun.mdc` (Кластер 7 — Storybook-devDeps у `npm/package.json`, canonical version pin, review-гейт лише для нових stories) — окремий трек, не в обсязі цього правила.
@@ -0,0 +1,3 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json"
3
+ }